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
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
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"
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)
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)
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)
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`." )
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
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={}, )
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, )
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", ), ), )
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))