# CDS Artifact Manager

Artifact Manager is a very simple gRPC service that lets you upload, download and delete CBA archives. It can be ran as a standalone micro service (using `server.py`) or you can include it's methods in a service like `py-executor`.

## Configuration
Configuration is stored in `.ini` file, you can specify a path and name of a file using `CONFIGURATION` env variable.
For possible variables please see example below (with inline comments):
```
[artifactManagerServer]
port=50052                    # A port on which the server will be listening
logFile=server.log            # Path to a log file
maxWorkers=20                 # Max number of concurent workers
debug=true                    # Debug flag
logConfig=logging.yaml        # A special MDC logger config
fileRepositoryBasePath=/tmp/  # A FS path where we should store CBA files
```

## Methods
Below is a list of gRPC methods handled by the service. The `proto` files are available in `artifact-manager/manager/proto` directory.

All methods expect `CommonHeader` with:
* `timestamp` - datetime in UTC with this: `%Y-%m-%dT%H:%M:%S.%fZ` format
* `originatorId` - name of the component (eg. `CDS`)
* `requestId` - ID of the request
* `subRequestId` - Sub ID of the request
* `flag` - TBD

and an `ActionIdentifiers` with following fields:
* `blueprintName` - name of the blueprint
* `blueprintVersion` - version number of blueprint (as string)
* `actionName` - TBD
* `mode` - TBD

### Upload

Upload `CBA.zip` file for storage in artifact manager. File needs to be sent as a binary data in `fileChunk` field.

#### Example

```
stub: BlueprintManagementServiceStub = BlueprintManagementServiceStub(channel)
with  open(file_path, "rb") as cba_file:
    msg: BlueprintUploadInput = BlueprintUploadInput()
    msg.actionIdentifiers.blueprintName =  "Test"
    msg.actionIdentifiers.blueprintVersion =  "0.0.1"
    msg.fileChunk.chunk = cba_file.read()
return  stub.uploadBlueprint(msg)
```

### Download

Download existing `CBA.zip` file.

#### Example

```
stub: BlueprintManagementServiceStub = BlueprintManagementServiceStub(channel)
msg: BlueprintDownloadInput = BlueprintDownloadInput()
msg.actionIdentifiers.blueprintName =  "Test"
msg.actionIdentifiers.blueprintVersion =  "0.0.1"
return  stub.downloadBlueprint(msg)
```
### Remove

Delete existing `CBA.zip` file.

#### Example

```
stub: BlueprintManagementServiceStub = BlueprintManagementServiceStub(channel)
msg: BlueprintRemoveInput = BlueprintRemoveInput()
msg.actionIdentifiers.blueprintName =  "Test"
msg.actionIdentifiers.blueprintVersion =  "0.0.1"
return  stub.removeBlueprint(msg)
```

## Full gRPC Client Example

```
import logging
import sys
from argparse import ArgumentParser, FileType, Namespace
from configparser import ConfigParser
from datetime import datetime
from pathlib import Path

import zipfile

from grpc import Channel, ChannelCredentials, insecure_channel, secure_channel, ssl_channel_credentials

from proto.BlueprintManagement_pb2 import (
    BlueprintDownloadInput,
    BlueprintRemoveInput,
    BlueprintUploadInput,
    BlueprintManagementOutput,
)
from proto.BlueprintManagement_pb2_grpc import BlueprintManagementServiceStub


logging.basicConfig(level=logging.DEBUG)


class ClientArgumentParser(ArgumentParser):
    """Client argument parser.

    It has two arguments:
     - config_file - provide a path to configuration file. Default is ./configuration-local.ini
     - actions - list of actions to do by client. It have to be a list of given values: upload, download, remove.
    """

    DEFAULT_CONFIG_PATH: str = str(Path(__file__).resolve().with_name("configuration-local.ini"))

    def __init__(self, *args, **kwargs):
        """Initialize argument parser."""
        super().__init__(*args, **kwargs)
        self.description: str = "Artifact Manager client example"

        self.add_argument(
            "--config_file",
            type=FileType("r"),
            default=self.DEFAULT_CONFIG_PATH,
            help="Path to the client configuration file. By default it's `configuration-local.ini` file from Artifact Manager directory",
        )
        self.add_argument(
            "--actions", nargs="+", default=["upload", "download", "remove"], choices=["upload", "download", "remove"]
        )


class Client:
    """Client class.

    Implements methods which can be called to server.
    """

    def __init__(self, channel: Channel, config: ConfigParser) -> None:
        """Initialize client class.

        :param channel: gprc channel object
        :param config: ConfigParser object with "client" section
        """
        self.channel: Channel = channel
        self.stub: BlueprintManagementServiceStub = BlueprintManagementServiceStub(self.channel)
        self.config = config

    def upload(self) -> BlueprintManagementOutput:
        """Prepare upload message and send it to server."""
        logging.info("Call upload client method")
        with open(self.config.get("client", "cba_file"), "rb") as cba_file:
            msg: BlueprintUploadInput = BlueprintUploadInput()
            msg.actionIdentifiers.blueprintName = "Test"
            msg.actionIdentifiers.blueprintVersion = "0.0.1"
            msg.fileChunk.chunk = cba_file.read()
        return self.stub.uploadBlueprint(msg)

    def download(self) -> BlueprintManagementOutput:
        """Prepare download message and send it to server."""
        logging.info("Call download client method")
        msg: BlueprintDownloadInput = BlueprintDownloadInput()
        msg.actionIdentifiers.blueprintName = "Test"
        msg.actionIdentifiers.blueprintVersion = "0.0.1"
        return self.stub.downloadBlueprint(msg)

    def remove(self) -> BlueprintManagementOutput:
        """Prepare remove message and send it to server."""
        logging.info("Call remove client method")
        msg: BlueprintRemoveInput = BlueprintRemoveInput()
        msg.actionIdentifiers.blueprintName = "Test"
        msg.actionIdentifiers.blueprintVersion = "0.0.1"
        return self.stub.removeBlueprint(msg)


if __name__ == "__main__":
    arg_parser: ClientArgumentParser = ClientArgumentParser()
    args: Namespace = arg_parser.parse_args()

    config_parser: ConfigParser = ConfigParser()
    config_parser.read_file(args.config_file)

    server_address: str = f"{config_parser.get('client', 'address')}:{config_parser.get('client', 'port')}"
    if config_parser.getboolean("client", "use_ssl", fallback=False):
        logging.info(f"Create secure connection on {server_address}")
        with open(config_parser.get("client", "private_key_file"), "rb") as private_key_file, open(
            config_parser.get("client", "certificate_chain_file"), "rb"
        ) as certificate_chain_file:
            ssl_credentials: ChannelCredentials = ssl_channel_credentials(
                private_key=private_key_file.read(), certificate_chain=certificate_chain_file.read()
            )
        channel: Channel = secure_channel(server_address, ssl_credentials)
    else:
        logging.info(f"Create insecure connection on {server_address}")
        channel: Channel = insecure_channel(server_address)

    with channel:
        client: Client = Client(channel, config_parser)
        for action in args.actions:
            logging.info("Get response")
            logging.info(getattr(client, action)())

```