예제 #1
0
def decrypt_docker_compose_files(
    cli_context: CliContext,
    docker_compose_file_relative_path: Path,
    docker_compose_override_directory_relative_path: Path,
) -> List[Path]:
    """Decrypt docker-compose and docker-compose override files.

    Args:
        cli_context (CliContext): The current CLI context.
        docker_compose_file_relative_path (Path): The relative path to the docker-compose file. Path is relative to the
            generated configuration directory.
        docker_compose_override_directory_relative_path (Path): The relative path to a directory containing
            docker-compose override files. Path is relative to the generated configuration directory.

    Returns:
        List[Path]: sorted list of absolute paths to decrypted docker-compose files. The first path is the decrypted
            docker-compose file, and the rest of the paths are the alphanumerically sorted docker compose override
            files in the docker compose override directory.
    """

    compose_files = []

    if docker_compose_file_relative_path is not None:
        docker_compose_file = cli_context.get_generated_configuration_dir().joinpath(
            docker_compose_file_relative_path
        )
        if os.path.isfile(docker_compose_file):
            compose_files.append(docker_compose_file)

    if docker_compose_override_directory_relative_path is not None:
        docker_compose_override_directory = (
            cli_context.get_generated_configuration_dir().joinpath(
                docker_compose_override_directory_relative_path
            )
        )
        if os.path.isdir(docker_compose_override_directory):
            docker_compose_override_files: List[Path] = [
                Path(os.path.join(docker_compose_override_directory, file))
                for file in os.listdir(docker_compose_override_directory)
                if os.path.isfile(os.path.join(docker_compose_override_directory, file))
            ]

            if len(docker_compose_override_files) > 0:
                docker_compose_override_files.sort()
                logger.debug(
                    "Detected docker compose override files [%s]",
                    docker_compose_override_files,
                )
                compose_files.extend(docker_compose_override_files)

    # decrypt files if key is available
    key_file = cli_context.get_key_file()
    decrypted_files = [
        decrypt_file(encrypted_file, key_file) for encrypted_file in compose_files
    ]
    return decrypted_files
예제 #2
0
def create_click_ctx(conf_dir, data_dir, backup_dir) -> click.Context:
    """
    Creates a click context object with the minimum set of values to enable backup & restore.

        Returns:
            ctx: (click.Context). A click library context object.
    """
    service_commands = type(
        "service_commands",
        (object,),
        {"commands": {"shutdown": lambda: None, "start": lambda: None}},
    )()
    commands = {"service": service_commands}

    ctx = click.Context(
        obj=CliContext(
            configuration_dir=conf_dir,
            data_dir=data_dir,
            backup_dir=backup_dir,
            app_name="test_app",
            additional_data_dirs=None,
            additional_env_variables=None,
            environment="test",
            docker_credentials_file=None,
            subcommand_args=None,
            debug=True,
            app_version="1.0",
            commands=commands,
        ),
        command=click.Command(
            name="backup", context_settings={"allow_extra_args": False}
        ),
    )
    return ctx
예제 #3
0
def test_version_cli():

    # Unfortunately, there's no easy way to get access to the main cli command,
    # so we have to test the version command directly. This requires initialising
    # the CliContext into the desired state, including setting the `app_version`.
    # Therefore this unit test doesn't deal with ensuring the `app_version` is
    # correctly populated from within the create_cli function.
    version = "1.2.3"
    cli_context = CliContext(
        configuration_dir=None,
        data_dir=None,
        additional_data_dirs=None,
        backup_dir=None,
        additional_env_variables=None,
        environment="test",
        docker_credentials_file=None,
        subcommand_args=None,
        debug=True,
        app_name="APP_NAME",
        app_version=f"{version}",
        commands=None,
    )
    runner = CliRunner()
    result = runner.invoke(VersionCli(None).commands["version"],
                           obj=cli_context)
    assert result.exit_code == 0
    assert result.output == f"{version}\n"
예제 #4
0
 def __docker_stack(
     self, cli_context: CliContext, subcommand: Iterable[str]
 ) -> CompletedProcess:
     command = ["docker", "stack"]
     command.extend(subcommand)
     command.append(cli_context.get_project_name())
     return self.__exec_command(command)
예제 #5
0
 def __docker_stack(self, cli_context: CliContext,
                    subcommand: Iterable[str]) -> CompletedProcess:
     # This is now broken since the Docker image no longer includes a Docker installation.
     # TODO: (GH-115) Replace this call with using the `docker` python library which only requires access to the docker.sock
     command = ["docker", "stack"]
     command.extend(subcommand)
     command.append(cli_context.get_project_name())
     return self.__exec_command(command)
예제 #6
0
def execute_compose(
    cli_context: CliContext,
    command: Iterable[str],
    docker_compose_file_relative_path: Path,
    docker_compose_override_directory_relative_path: Path,
    stdin_input: str = None,
) -> CompletedProcess:
    """Builds and executes a docker-compose command.

    Args:
        cli_context (CliContext): The current CLI context.
        command (Iterable[str]): The command to execute with docker-compose.
        docker_compose_file_relative_path (Path): The relative path to the docker-compose file. Path is relative to the
            generated configuration directory.
        docker_compose_override_directory_relative_path (Path): The relative path to a directory containing
            docker-compose override files. Path is relative to the generated configuration directory.
        stdin_input (str): Optional - defaults to None. String passed through to the subprocess via stdin.

    Returns:
        CompletedProcess: The completed process and its exit code.
    """
    docker_compose_command = [
        "docker-compose",
        "--project-name",
        cli_context.get_project_name(),
    ]

    compose_files = decrypt_docker_compose_files(
        cli_context,
        docker_compose_file_relative_path,
        docker_compose_override_directory_relative_path,
    )

    if len(compose_files) == 0:
        logger.error(
            "No valid docker compose files were found. Expected file [%s] or files in directory [%s]",
            docker_compose_file_relative_path,
            docker_compose_override_directory_relative_path,
        )
        return CompletedProcess(args=None, returncode=1)

    for compose_file in compose_files:
        docker_compose_command.extend(("--file", str(compose_file)))

    if command is not None:
        docker_compose_command.extend(command)

    logger.debug(docker_compose_command)
    logger.debug("Running [%s]", " ".join(docker_compose_command))
    encoded_input = stdin_input.encode("utf-8") if stdin_input is not None else None
    logger.debug(f"Encoded input: [{encoded_input}]")
    result = subprocess.run(
        docker_compose_command, capture_output=True, input=encoded_input
    )
    return result
    def __get_generated_configuration_metadata_file(
        self, cli_context: CliContext
    ) -> Path:
        """Get the path to the generated configuration's metadata file

        Args:
            cli_context (CliContext): The current CLI context.

        Returns:
            Path: the path to the metadata file
        """
        generated_configuration_dir = cli_context.get_generated_configuration_dir()
        return generated_configuration_dir.joinpath(METADATA_FILE_NAME)
예제 #8
0
def confirm_generated_config_dir_exists(cli_context: CliContext):
    """Confirm that the generated configuration repository exists.
    If this fails, it will raise a general Exception with the error message.

    Args:
        cli_context (CliContext): The current CLI context.

    Raises:
        Exception: Raised if the generated configuration repository does not exist.
    """
    if not __is_git_repo(cli_context.get_generated_configuration_dir()):
        raise Exception(
            f"Generated configuration does not exist at [{cli_context.get_generated_configuration_dir()}]. Please run `configure apply`."
        )
예제 #9
0
def execute_compose(
    cli_context: CliContext,
    command: Iterable[str],
    docker_compose_file_relative_path: Path,
    docker_compose_override_directory_relative_path: Path,
) -> CompletedProcess:
    """Builds and executes a docker-compose command.

    Args:
        cli_context (CliContext): The current CLI context.
        command (Iterable[str]): The command to execute with docker-compose.
        docker_compose_file_relative_path (Path): The relative path to the docker-compose file. Path is relative to the
            generated configuration directory.
        docker_compose_override_directory_relative_path (Path): The relative path to a directory containing
            docker-compose override files. Path is relative to the generated configuration directory.

    Returns:
        CompletedProcess: The completed process and its exit code.
    """
    docker_compose_command = [
        "docker-compose",
        "--project-name",
        cli_context.get_project_name(),
    ]

    compose_files = decrypt_docker_compose_files(
        cli_context,
        docker_compose_file_relative_path,
        docker_compose_override_directory_relative_path,
    )

    if len(compose_files) == 0:
        logger.error(
            "No valid docker compose files were found. Expected file [%s] or files in directory [%s]",
            docker_compose_file_relative_path,
            docker_compose_override_directory_relative_path,
        )
        return CompletedProcess(args=None, returncode=1)

    for compose_file in compose_files:
        docker_compose_command.extend(("--file", str(compose_file)))

    if command is not None:
        docker_compose_command.extend(command)

    logger.debug("Running [%s]", " ".join(docker_compose_command))
    result = run(docker_compose_command)
    return result
예제 #10
0
def create_cli_context(tmpdir, app_version: str = "0.0.0") -> CliContext:
    conf_dir = Path(tmpdir, "conf")
    conf_dir.mkdir(exist_ok=True)
    data_dir = Path(tmpdir, "data")
    data_dir.mkdir(exist_ok=True)
    backup_dir = Path(tmpdir, "backup")
    backup_dir.mkdir(exist_ok=True)

    return CliContext(
        configuration_dir=conf_dir,
        data_dir=data_dir,
        additional_data_dirs=None,
        backup_dir=backup_dir,
        additional_env_variables=None,
        environment="test",
        docker_credentials_file=None,
        subcommand_args=None,
        debug=True,
        app_name=APP_NAME,
        app_version=app_version,
        commands={},
    )
예제 #11
0
    def _create_cli_context(self, tmpdir, config) -> CliContext:
        group_dir = str(tmpdir.mktemp("commands_test"))
        conf_dir = Path(group_dir, "conf")
        conf_dir.mkdir(exist_ok=True)
        data_dir = Path(group_dir, "data")
        data_dir.mkdir(exist_ok=True)
        backup_dir = Path(group_dir, "backup")
        backup_dir.mkdir(exist_ok=True)

        return CliContext(
            configuration_dir=conf_dir,
            data_dir=data_dir,
            additional_data_dirs=None,
            backup_dir=backup_dir,
            additional_env_variables=None,
            environment="test",
            docker_credentials_file=None,
            subcommand_args=None,
            debug=True,
            app_name=APP_NAME,
            app_version="0.0.0",
            commands=ConfigureCli(config).commands,
        )
예제 #12
0
    def cli(
        ctx,
        debug,
        configuration_dir,
        data_dir,
        environment,
        docker_credentials_file,
        additional_data_dir,
        additional_env_var,
        backup_dir,
    ):
        if debug:
            logger.info("Enabling debug logging")
            enable_debug_logging()

        ctx.obj = CliContext(
            configuration_dir=configuration_dir,
            data_dir=data_dir,
            additional_data_dirs=additional_data_dir,
            additional_env_variables=additional_env_var,
            environment=environment,
            docker_credentials_file=docker_credentials_file,
            subcommand_args=ctx.obj,
            debug=debug,
            app_name=APP_NAME,
            app_version=APP_VERSION,
            commands=default_commands,
            backup_dir=backup_dir,
        )

        if ctx.invoked_subcommand is None:
            click.echo(ctx.get_help())
            sys.exit(1)

        # attempt to set desired environment
        initialised_environment = {}
        for k, v in desired_environment.items():
            if v is None:
                logger.warning("Environment variable [%s] has not been set", k)
            else:
                logger.debug("Exporting environment variable [%s]", k)
                os.environ[k] = v
                initialised_environment[k] = v
        if len(initialised_environment) != len(desired_environment):
            error_and_exit(
                "Could not set desired environment. Please ensure specified environment variables are set."
            )

        # For the `installer`/`launcher` commands, no further output/checks required.
        if ctx.invoked_subcommand in ("launcher", "install"):
            # Don't execute this function any further, continue to run subcommand with the current CLI context
            return

        check_docker_socket()
        check_environment()

        # Table of configuration variables to print
        table = [
            ["Configuration directory", f"{ctx.obj.configuration_dir}"],
            [
                "Generated Configuration directory",
                f"{ctx.obj.get_generated_configuration_dir()}",
            ],
            ["Data directory", f"{ctx.obj.data_dir}"],
            ["Backup directory", f"{ctx.obj.backup_dir}"],
            ["Environment", f"{ctx.obj.environment}"],
        ]

        # Print out the configuration values as an aligned table
        logger.info(
            "%s (version: %s) CLI running with:\n\n%s\n",
            APP_NAME,
            APP_VERSION,
            tabulate(table, colalign=("right", )),
        )
        if additional_data_dir:
            logger.info(
                "Additional data directories:\n\n%s\n",
                tabulate(
                    additional_data_dir,
                    headers=["Environment Variable", "Path"],
                    colalign=("right", ),
                ),
            )
        if additional_env_var:
            logger.info(
                "Additional environment variables:\n\n%s\n",
                tabulate(
                    additional_env_var,
                    headers=["Environment Variable", "Value"],
                    colalign=("right", ),
                ),
            )
예제 #13
0
 def __init__(self, cli_context: CliContext):
     super().__init__(cli_context.get_generated_configuration_dir())
     self.rename_current_branch(
         self.generate_branch_name(cli_context.app_version))