Setup command execution #469
| @ -13,9 +13,10 @@ | ||||
| # You should have received a copy of the GNU Affero General Public License | ||||
| # along with this program.  If not, see <http:#www.gnu.org/licenses/>. | ||||
| 
 | ||||
| from dataclasses import dataclass | ||||
| from app.util import get_yaml | ||||
| from app.deploy_types import DeployCommandContext, DeploymentContext | ||||
| from app.stack_state import State | ||||
| from app.deploy_util import VolumeMapping, run_container_command | ||||
| 
 | ||||
| default_spec_file_content = """config: | ||||
|     node_moniker: my-node-name | ||||
| @ -25,38 +26,26 @@ default_spec_file_content = """config: | ||||
| init_help_text = """Add helpful text here on setting config variables. | ||||
| """ | ||||
| 
 | ||||
| @dataclass | ||||
| class VolumeMapping: | ||||
|     host_path: str | ||||
|     container_path: str | ||||
| 
 | ||||
| 
 | ||||
| # In order to make this, we need the ability to run the stack | ||||
| # In theory we can make this same way as we would run deploy up | ||||
| def run_container_command(ctx, ontainer, command, mounts): | ||||
|     deploy_context = ctx.obj | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| def setup(ctx): | ||||
| def setup(command_context: DeployCommandContext): | ||||
|     node_moniker = "dbdb-node" | ||||
|     chain_id = "laconic_81337-1" | ||||
|     mounts = [ | ||||
|         VolumeMapping("./path", "~/.laconicd") | ||||
|     ] | ||||
|     output, status = run_container_command(ctx, "laconicd", f"laconicd init {node_moniker} --chain-id {chain_id}", mounts) | ||||
|     output, status = run_container_command(command_context.cluster_context, "laconicd", f"laconicd init {node_moniker} --chain-id {chain_id}", mounts) | ||||
| 
 | ||||
| 
 | ||||
| def init(command_context): | ||||
| def init(command_context: DeployCommandContext): | ||||
|     print(init_help_text) | ||||
|     yaml = get_yaml() | ||||
|     return yaml.load(default_spec_file_content) | ||||
| 
 | ||||
| 
 | ||||
| def get_state(command_context): | ||||
| def get_state(command_context: DeployCommandContext): | ||||
|     print("Here we get state") | ||||
|     return State.CONFIGURED | ||||
| 
 | ||||
| 
 | ||||
| def change_state(command_context): | ||||
| def change_state(command_context: DeployCommandContext): | ||||
|     pass | ||||
|  | ||||
							
								
								
									
										54
									
								
								app/data/stacks/test/deploy/commands.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/data/stacks/test/deploy/commands.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| # Copyright © 2022, 2023 Cerc | ||||
| 
 | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU Affero General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| 
 | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU Affero General Public License for more details. | ||||
| 
 | ||||
| # You should have received a copy of the GNU Affero General Public License | ||||
| # along with this program.  If not, see <http:#www.gnu.org/licenses/>. | ||||
| 
 | ||||
| from app.util import get_yaml | ||||
| from app.deploy_types import DeployCommandContext, DeploymentContext | ||||
| from app.stack_state import State | ||||
| from app.deploy_util import VolumeMapping, run_container_command | ||||
| import os | ||||
| from pathlib import Path | ||||
| 
 | ||||
| default_spec_file_content = """config: | ||||
|     config_variable: test-value | ||||
| """ | ||||
| 
 | ||||
| init_help_text = """Add helpful text here on setting config variables. | ||||
| """ | ||||
| 
 | ||||
| # Output a known string to a know file in the bind mounted directory ./container-output-dir | ||||
| # for test purposes -- test checks that the file was written. | ||||
| def setup(command_context: DeployCommandContext, extra_args): | ||||
|     host_directory = "./container-output-dir" | ||||
|     host_directory_absolute = Path(extra_args[0]).absolute().joinpath(host_directory) | ||||
|     host_directory_absolute.mkdir(parents=True, exist_ok=True) | ||||
|     mounts = [ | ||||
|         VolumeMapping(host_directory_absolute, "/data") | ||||
|     ] | ||||
|     output, status = run_container_command(command_context, "test", "echo output-data > /data/output-file && echo success", mounts) | ||||
| 
 | ||||
| 
 | ||||
| def init(command_context: DeployCommandContext): | ||||
|     print(init_help_text) | ||||
|     yaml = get_yaml() | ||||
|     return yaml.load(default_spec_file_content) | ||||
| 
 | ||||
| 
 | ||||
| def get_state(command_context: DeployCommandContext): | ||||
|     print("Here we get state") | ||||
|     return State.CONFIGURED | ||||
| 
 | ||||
| 
 | ||||
| def change_state(command_context: DeployCommandContext): | ||||
|     pass | ||||
| @ -27,17 +27,12 @@ from python_on_whales import DockerClient, DockerException | ||||
| import click | ||||
| from pathlib import Path | ||||
| from app.util import include_exclude_check, get_parsed_stack_config, global_options2 | ||||
| from app.deploy_types import ClusterContext, DeployCommandContext | ||||
| from app.deployment_create import create as deployment_create | ||||
| from app.deployment_create import init as deployment_init | ||||
| from app.deployment_create import setup as deployment_setup | ||||
| 
 | ||||
| 
 | ||||
| class DeployCommandContext(object): | ||||
|     def __init__(self, cluster_context, docker): | ||||
|         self.cluster_context = cluster_context | ||||
|         self.docker = docker | ||||
| 
 | ||||
| 
 | ||||
| @click.group() | ||||
| @click.option("--include", help="only start these components") | ||||
| @click.option("--exclude", help="don\'t start these components") | ||||
| @ -58,7 +53,7 @@ def create_deploy_context(global_context, stack, include, exclude, cluster, env_ | ||||
|     # See: https://gabrieldemarmiesse.github.io/python-on-whales/sub-commands/compose/ | ||||
|     docker = DockerClient(compose_files=cluster_context.compose_files, compose_project_name=cluster_context.cluster, | ||||
|                           compose_env_file=cluster_context.env_file) | ||||
|     return DeployCommandContext(cluster_context, docker) | ||||
|     return DeployCommandContext(stack, cluster_context, docker) | ||||
| 
 | ||||
| 
 | ||||
| def up_operation(ctx, services_list, stay_attached=False): | ||||
| @ -313,17 +308,7 @@ def _make_cluster_context(ctx, stack, include, exclude, cluster, env_file): | ||||
|     if ctx.verbose: | ||||
|         print(f"files: {compose_files}") | ||||
| 
 | ||||
|     return cluster_context(cluster, compose_files, pre_start_commands, post_start_commands, cluster_config, env_file) | ||||
| 
 | ||||
| 
 | ||||
| class cluster_context: | ||||
|     def __init__(self, cluster, compose_files, pre_start_commands, post_start_commands, config, env_file) -> None: | ||||
|         self.cluster = cluster | ||||
|         self.compose_files = compose_files | ||||
|         self.pre_start_commands = pre_start_commands | ||||
|         self.post_start_commands = post_start_commands | ||||
|         self.config = config | ||||
|         self.env_file = env_file | ||||
|     return ClusterContext(cluster, compose_files, pre_start_commands, post_start_commands, cluster_config, env_file) | ||||
| 
 | ||||
| 
 | ||||
| def _convert_to_new_format(old_pod_array): | ||||
|  | ||||
							
								
								
									
										47
									
								
								app/deploy_types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								app/deploy_types.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| # Copyright © 2023 Cerc | ||||
| 
 | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU Affero General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| 
 | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU Affero General Public License for more details. | ||||
| 
 | ||||
| # You should have received a copy of the GNU Affero General Public License | ||||
| # along with this program.  If not, see <http:#www.gnu.org/licenses/>. | ||||
| 
 | ||||
| from typing import List | ||||
| from dataclasses import dataclass | ||||
| from pathlib import Path | ||||
| from python_on_whales import DockerClient | ||||
| 
 | ||||
| @dataclass | ||||
| class ClusterContext: | ||||
|     cluster: str | ||||
|     compose_files: List[str] | ||||
|     pre_start_commands: List[str] | ||||
|     post_start_commands: List[str] | ||||
|     config: str | ||||
|     env_file: str | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class DeployCommandContext: | ||||
|     stack: str | ||||
|     cluster_context: ClusterContext | ||||
|     docker: DockerClient | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class DeploymentContext: | ||||
|     deployment_dir: Path | ||||
|     command_context: DeployCommandContext | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class VolumeMapping: | ||||
|     host_path: str | ||||
|     container_path: str | ||||
							
								
								
									
										56
									
								
								app/deploy_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								app/deploy_util.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| # Copyright © 2022, 2023 Cerc | ||||
| 
 | ||||
| # This program is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU Affero General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| 
 | ||||
| # This program is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU Affero General Public License for more details. | ||||
| 
 | ||||
| # You should have received a copy of the GNU Affero General Public License | ||||
| # along with this program.  If not, see <http:#www.gnu.org/licenses/>. | ||||
| 
 | ||||
| import os | ||||
| from typing import List | ||||
| from dataclasses import dataclass | ||||
| from app.deploy_types import DeployCommandContext, VolumeMapping | ||||
| from app.util import get_parsed_stack_config, get_yaml, get_compose_file_dir | ||||
| 
 | ||||
| 
 | ||||
| def _container_image_from_service(stack:str, service: str): | ||||
|     # Parse the compose files looking for the image name of the specified service | ||||
|     image_name = None | ||||
|     parsed_stack = get_parsed_stack_config(stack) | ||||
|     pods = parsed_stack["pods"] | ||||
|     yaml = get_yaml() | ||||
|     for pod in pods: | ||||
|         pod_file_path = os.path.join(get_compose_file_dir(), f"docker-compose-{pod}.yml") | ||||
|         parsed_pod_file = yaml.load(open(pod_file_path, "r")) | ||||
|         if "services" in parsed_pod_file: | ||||
|             services = parsed_pod_file["services"] | ||||
|             if service in services: | ||||
|                 service_definition = services[service] | ||||
|                 if "image" in service_definition: | ||||
|                     image_name = service_definition["image"] | ||||
|     return image_name | ||||
| 
 | ||||
| 
 | ||||
| def _volumes_to_docker(mounts: List[VolumeMapping]): | ||||
| # Example from doc: [("/", "/host"), ("/etc/hosts", "/etc/hosts", "rw")] | ||||
|     result = [] | ||||
|     for mount in mounts: | ||||
|         docker_volume = (mount.host_path, mount.container_path) | ||||
|         result.append(docker_volume) | ||||
|     return result | ||||
| 
 | ||||
| 
 | ||||
| def run_container_command(ctx: DeployCommandContext, service: str, command: str, mounts: List[VolumeMapping]): | ||||
|     docker = ctx.docker | ||||
|     container_image = _container_image_from_service(ctx.stack, service) | ||||
|     docker_volumes = _volumes_to_docker(mounts) | ||||
|     docker_output = docker.run(container_image, ["-c", command], entrypoint="bash", volumes=docker_volumes) | ||||
|     # There doesn't seem to be a way to get an exit code from docker.run() | ||||
|     return (docker_output, 0) | ||||
| @ -20,26 +20,14 @@ import os | ||||
| from pathlib import Path | ||||
| from shutil import copyfile, copytree | ||||
| import sys | ||||
| from app.util import get_stack_file_path, get_parsed_deployment_spec, get_parsed_stack_config, global_options, get_yaml | ||||
| 
 | ||||
| @dataclass | ||||
| class DeploymentContext: | ||||
|     stack: str | ||||
|     deployment_dir: Path | ||||
| from app.util import get_stack_file_path, get_parsed_deployment_spec, get_parsed_stack_config, global_options, get_yaml, get_compose_file_dir | ||||
| from app.deploy_types import DeploymentContext, DeployCommandContext | ||||
| 
 | ||||
| 
 | ||||
| def _make_default_deployment_dir(): | ||||
|     return "deployment-001" | ||||
| 
 | ||||
| 
 | ||||
| def _get_compose_file_dir(): | ||||
|     # TODO: refactor to use common code with deploy command | ||||
|     # See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure | ||||
|     data_dir = Path(__file__).absolute().parent.joinpath("data") | ||||
|     source_compose_dir = data_dir.joinpath("compose") | ||||
|     return source_compose_dir | ||||
| 
 | ||||
| 
 | ||||
| def _get_named_volumes(stack): | ||||
|     # Parse the compose files looking for named volumes | ||||
|     named_volumes = [] | ||||
| @ -47,7 +35,7 @@ def _get_named_volumes(stack): | ||||
|     pods = parsed_stack["pods"] | ||||
|     yaml = get_yaml() | ||||
|     for pod in pods: | ||||
|         pod_file_path = os.path.join(_get_compose_file_dir(), f"docker-compose-{pod}.yml") | ||||
|         pod_file_path = os.path.join(get_compose_file_dir(), f"docker-compose-{pod}.yml") | ||||
|         parsed_pod_file = yaml.load(open(pod_file_path, "r")) | ||||
|         if "volumes" in parsed_pod_file: | ||||
|             volumes = parsed_pod_file["volumes"] | ||||
| @ -94,27 +82,27 @@ def _fixup_pod_file(pod, spec, compose_dir): | ||||
|                     pod["volumes"][volume] = new_volume_spec | ||||
| 
 | ||||
| 
 | ||||
| def call_stack_deploy_init(stack): | ||||
| def call_stack_deploy_init(deploy_command_context): | ||||
|     # Link with the python file in the stack | ||||
|     # Call a function in it | ||||
|     # If no function found, return None | ||||
|     python_file_path = get_stack_file_path(stack).parent.joinpath("deploy", "commands.py") | ||||
|     python_file_path = get_stack_file_path(deploy_command_context.stack).parent.joinpath("deploy", "commands.py") | ||||
|     spec = util.spec_from_file_location("commands", python_file_path) | ||||
|     imported_stack = util.module_from_spec(spec) | ||||
|     spec.loader.exec_module(imported_stack) | ||||
|     return imported_stack.init(None) | ||||
|     return imported_stack.init(deploy_command_context) | ||||
| 
 | ||||
| 
 | ||||
| # TODO: fold this with function above | ||||
| def call_stack_deploy_setup(stack): | ||||
| def call_stack_deploy_setup(deploy_command_context, extra_args): | ||||
|     # Link with the python file in the stack | ||||
|     # Call a function in it | ||||
|     # If no function found, return None | ||||
|     python_file_path = get_stack_file_path(stack).parent.joinpath("deploy", "commands.py") | ||||
|     python_file_path = get_stack_file_path(deploy_command_context.stack).parent.joinpath("deploy", "commands.py") | ||||
|     spec = util.spec_from_file_location("commands", python_file_path) | ||||
|     imported_stack = util.module_from_spec(spec) | ||||
|     spec.loader.exec_module(imported_stack) | ||||
|     return imported_stack.setup(None) | ||||
|     return imported_stack.setup(deploy_command_context, extra_args) | ||||
| 
 | ||||
| 
 | ||||
| # TODO: fold this with function above | ||||
| @ -154,7 +142,7 @@ def init(ctx, output): | ||||
|     yaml = get_yaml() | ||||
|     stack = global_options(ctx).stack | ||||
|     verbose = global_options(ctx).verbose | ||||
|     default_spec_file_content = call_stack_deploy_init(stack) | ||||
|     default_spec_file_content = call_stack_deploy_init(ctx.obj) | ||||
|     spec_file_content = {"stack": stack} | ||||
|     if default_spec_file_content: | ||||
|         spec_file_content.update(default_spec_file_content) | ||||
| @ -217,7 +205,7 @@ def create(ctx, spec_file, deployment_dir): | ||||
|                 if not os.path.exists(destination_config_dir): | ||||
|                     copytree(source_config_dir, destination_config_dir) | ||||
|     # Delegate to the stack's Python code | ||||
|     deployment_context = DeploymentContext(stack_name, Path(deployment_dir)) | ||||
|     deployment_context = DeploymentContext(Path(deployment_dir), ctx.obj) | ||||
|     call_stack_deploy_create(deployment_context) | ||||
| 
 | ||||
| 
 | ||||
| @ -227,7 +215,7 @@ def create(ctx, spec_file, deployment_dir): | ||||
| @click.option("--initialize-network", is_flag=True, default=False, help="Help goes here") | ||||
| @click.option("--join-network", is_flag=True, default=False, help="Help goes here") | ||||
| @click.option("--create-network", is_flag=True, default=False, help="Help goes here") | ||||
| @click.argument('extra_args', nargs=-1) | ||||
| @click.pass_context | ||||
| def setup(ctx, node_moniker, key_name, initialize_network, join_network, create_network): | ||||
|     stack = global_options(ctx).stack | ||||
|     call_stack_deploy_setup(stack) | ||||
| def setup(ctx, node_moniker, key_name, initialize_network, join_network, create_network, extra_args): | ||||
|     call_stack_deploy_setup(ctx.obj, extra_args) | ||||
|  | ||||
| @ -56,6 +56,14 @@ def get_parsed_stack_config(stack): | ||||
|         sys.exit(1) | ||||
| 
 | ||||
| 
 | ||||
| def get_compose_file_dir(): | ||||
|     # TODO: refactor to use common code with deploy command | ||||
|     # See: https://stackoverflow.com/questions/25389095/python-get-path-of-root-project-structure | ||||
|     data_dir = Path(__file__).absolute().parent.joinpath("data") | ||||
|     source_compose_dir = data_dir.joinpath("compose") | ||||
|     return source_compose_dir | ||||
| 
 | ||||
| 
 | ||||
| def get_parsed_deployment_spec(spec_file): | ||||
|     spec_file_path = Path(spec_file) | ||||
|     try: | ||||
|  | ||||
| @ -23,6 +23,26 @@ mkdir -p $CERC_REPO_BASE_DIR | ||||
| # with and without volume removal | ||||
| $TEST_TARGET_SO --stack test setup-repositories | ||||
| $TEST_TARGET_SO --stack test build-containers | ||||
| # Test deploy command execution | ||||
| $TEST_TARGET_SO --stack test deploy setup $CERC_REPO_BASE_DIR | ||||
| # Check that we now have the expected output directory | ||||
| if [ ! -d "$CERC_REPO_BASE_DIR/container-output-dir" ]; then | ||||
|     echo "deploy setup test: output directory not present" | ||||
|     echo "deploy setup test: FAILED" | ||||
|     exit 1 | ||||
| fi | ||||
| if [ ! -f "$CERC_REPO_BASE_DIR/container-output-dir/output-file" ]; then | ||||
|     echo "deploy setup test: output file not present" | ||||
|     echo "deploy setup test: FAILED" | ||||
|     exit 1 | ||||
| fi | ||||
| output_file_content=$(<$CERC_REPO_BASE_DIR/container-output-dir/output-file) | ||||
| if [ ! "$output_file_content" == "output-data"  ]; then | ||||
|     echo "deploy setup test: output file contents not correct" | ||||
|     echo "deploy setup test: FAILED" | ||||
|     exit 1 | ||||
| fi | ||||
| # Check that we now have the expected output file | ||||
| $TEST_TARGET_SO --stack test deploy up | ||||
| # Test deploy port command | ||||
| deploy_port_output=$( $TEST_TARGET_SO --stack test deploy port test 80 ) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user