From 89d20d6cecfe7b9189cda46e61171c94c5998ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Alvergnat?= Date: Thu, 18 Feb 2021 22:25:36 +0100 Subject: [PATCH] feat(config): add more capabilities to config command --- ddb/feature/core/__init__.py | 4 + ddb/feature/core/actions.py | 121 +++++++++++++---- ddb/feature/jsonnet/lib/ddb.docker.libjsonnet | 23 ++-- docs/commands.md | 8 +- .../more-properties/ddb.yml | 25 ++++ tests/it/test_config_it.py | 127 ++++++++++++++++++ 6 files changed, 274 insertions(+), 34 deletions(-) create mode 100644 tests/it/test_config_it.data/more-properties/ddb.yml diff --git a/ddb/feature/core/__init__.py b/ddb/feature/core/__init__.py index 1491919f..4eea2e1c 100644 --- a/ddb/feature/core/__init__.py +++ b/ddb/feature/core/__init__.py @@ -74,8 +74,12 @@ def configure_parser(parser: ArgumentParser): help="Autofix supported deprecated warnings by modifying template sources.") def config_parser(parser: ArgumentParser): + parser.add_argument("property", nargs='?', + help="Property to read") parser.add_argument("--variables", action="store_true", help="Output as a flat list of variables available in template engines") + parser.add_argument("--value", action="store_true", + help="Output value of given property") parser.add_argument("--full", action="store_true", help="Output full configuration") parser.add_argument("--files", action="store_true", diff --git a/ddb/feature/core/actions.py b/ddb/feature/core/actions.py index 46592e90..e50c7a91 100644 --- a/ddb/feature/core/actions.py +++ b/ddb/feature/core/actions.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- import os +import platform import re import shutil import sys from datetime import date from pathlib import Path -import platform from tempfile import NamedTemporaryFile from typing import Optional from urllib.error import HTTPError +import distro import requests import yaml -import distro from dotty_dict import Dotty from progress.bar import IncrementalBar from semver import VersionInfo @@ -284,28 +284,102 @@ def execute(): """ Execute action """ - configuration, configuration_files = ConfigAction._get_configuration_files(config.args.full, config.args.files) + configuration, configuration_files = ConfigAction._get_configuration_files( + config.args.full, config.args.files + ) + + if config.args.property: + configuration, configuration_files = ConfigAction._handle_property(configuration, configuration_files) + + if config.args.value: + return ConfigAction._print_config_value(configuration, config.args.property) if config.args.variables: - if config.args.files and configuration_files: - for index, (file, configuration_file) in enumerate(configuration_files.items()): - if index > 0: - print() - print(f"# {file}") - flat = flatten(Dotty(configuration_file), keep_primitive_list=True) - for key in sorted(flat.keys()): - print(f"{key}: {flat[key]}") - else: - flat = flatten(Dotty(configuration), keep_primitive_list=True) + return ConfigAction._print_config_variables(configuration, configuration_files) + + return ConfigAction._print_config_yaml(configuration, configuration_files) + + @staticmethod + def _handle_property(configuration, configuration_files): + configuration, configuration_files = ConfigAction._get_configurations_for_prop( + config.args.property, + configuration, + configuration_files + ) + if configuration is None and not config.args.full: + configuration, configuration_files = ConfigAction._get_configuration_files( + True, config.args.files + ) + + configuration, configuration_files = ConfigAction._get_configurations_for_prop( + config.args.property, + configuration, + configuration_files + ) + if configuration is not None: + root_config = Dotty({}) + root_config[config.args.property] = configuration + configuration = dict(root_config) + if configuration_files: + root_configuration_files = {} + for k, configuration_file in configuration_files.items(): + root_configuration_file = Dotty({}) + root_configuration_file[config.args.property] = configuration_file + root_configuration_files[k] = dict(root_configuration_file) + configuration_files = root_configuration_files + return configuration, configuration_files + + @staticmethod + def _print_config_yaml(configuration, configuration_files): + if config.args.files and configuration_files: + for file, configuration_file in configuration_files.items(): + print(f"--- # {file}") + print(yaml.safe_dump(configuration_file)) + else: + if isinstance(configuration, (dict, list)): + print(yaml.safe_dump(configuration)) + elif configuration is not None: + print(configuration) + + @staticmethod + def _print_config_variables(configuration, configuration_files): + if config.args.files and configuration_files: + for index, (file, configuration_file) in enumerate(configuration_files.items()): + if index > 0: + print() + print(f"# {file}") + flat = flatten(Dotty(configuration_file), keep_primitive_list=True) for key in sorted(flat.keys()): print(f"{key}: {flat[key]}") else: - if config.args.files and configuration_files: - for file, configuration_file in configuration_files.items(): - print(f"--- # {file}") - print(yaml.safe_dump(configuration_file)) - else: - print(yaml.safe_dump(configuration)) + flat = flatten(Dotty(configuration), keep_primitive_list=True) + for key in sorted(flat.keys()): + print(f"{key}: {flat[key]}") + + @staticmethod + def _print_config_value(configuration, prop): + if configuration is None: + raise ValueError(f"{prop} not found in configuration.") + dotty_configuration = Dotty(configuration) + if prop and prop not in dotty_configuration: + raise ValueError(f"{prop} not found in configuration.") + value = dotty_configuration[prop] if prop and prop in dotty_configuration else configuration + print(value) + + @staticmethod + def _get_configurations_for_prop(prop, configuration, configuration_files): + prop_configuration = Dotty(configuration).get(prop) + + if configuration_files: + prop_configuration_files = {} + for file, configuration_file in configuration_files.items(): + prop_configuration_file = Dotty(configuration_file).get(prop) + if prop_configuration_file: + prop_configuration_files[file] = prop_configuration_file + else: + prop_configuration_files = configuration_files + + return prop_configuration, prop_configuration_files @staticmethod def _prune_default_configuration(default_configuration, flat_file_configuration): @@ -323,20 +397,19 @@ def _prune_default_configuration(default_configuration, flat_file_configuration) @staticmethod def _get_configuration_files(full, files): + default_configuration = dict(config.data.copy()) + if full and not files: - configuration = dict(config.data.copy()) configuration_files = None - return configuration, configuration_files + return default_configuration, configuration_files configuration, configuration_files = config.read() if full: - default_configuration = config.data.copy() - for file_configuration in configuration_files.values(): flat_file_configuration = flatten(file_configuration) ConfigAction._prune_default_configuration(default_configuration, flat_file_configuration) - tmp = {'default': dict(default_configuration)} + tmp = {'default': default_configuration} tmp.update(configuration_files) configuration_files = tmp return configuration, configuration_files diff --git a/ddb/feature/jsonnet/lib/ddb.docker.libjsonnet b/ddb/feature/jsonnet/lib/ddb.docker.libjsonnet index 8a463863..c9d389ef 100644 --- a/ddb/feature/jsonnet/lib/ddb.docker.libjsonnet +++ b/ddb/feature/jsonnet/lib/ddb.docker.libjsonnet @@ -81,17 +81,22 @@ local image_uri(image_name, image_tag=_docker_build_image_tag, registry_name=_do local image_uri = std.join('/', std.filter(function(s) s != null, [registry_name, registry_repository, image_name])); if image_tag != null then image_uri + ':' + image_tag else image_uri; -local NoExpose(container_port, host_port_suffix = null, protocol = null, port_prefix=null) = {}; -local Expose(container_port, host_port_suffix = null, protocol = null, port_prefix=_docker_expose_port_prefix) = +local NoExpose(container_port, host_port_suffix = null, protocol = null, port_prefix=null, expose=false) = {}; +local Expose(container_port, host_port_suffix = null, protocol = null, port_prefix=_docker_expose_port_prefix, expose=false) = local container_port_str = std.toString(container_port); - local host_port_suffix_str = if host_port_suffix == null then null else std.toString(host_port_suffix); - local effective_host_port_suffix = if host_port_suffix_str == null then std.substr(container_port_str, std.length(container_port_str) - 2, 2) else host_port_suffix_str; local effective_protocol = if protocol == null then "" else "/" + protocol; - { - ports+: [ - port_prefix + effective_host_port_suffix + ":" + container_port + effective_protocol - ] - }; + if std.length(std.findSubstr(":", container_port_str)) > 0 then + { + ports+: [container_port_str + effective_protocol] + } + else + local host_port_suffix_str = if host_port_suffix == null then null else std.toString(host_port_suffix); + local effective_host_port_suffix = if host_port_suffix_str == null then std.substr(container_port_str, std.length(container_port_str) - 2, 2) else host_port_suffix_str; + { + ports+: [ + port_prefix + effective_host_port_suffix + ":" + container_port + effective_protocol + ] + }; local Build(name, image=name, diff --git a/docs/commands.md b/docs/commands.md index 7102bbce..7f776330 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -50,12 +50,18 @@ Those are placed **after the command name**. `ddb config --help` ddb config --help - usage: ddb config [-h] [--variables] + usage: ddb config [-h] [--variables] [--value] [--full] [--files] [property] + + positional arguments: + property Property to read optional arguments: -h, --help show this help message and exit --variables Output as a flat list of variables available in template engines + --value Output value of given property + --full Output full configuration + --files Group by loaded configuration file !!! info "Watch mode" When setting up a project, you have to execute `ddb configure` many times while trying to configure the project diff --git a/tests/it/test_config_it.data/more-properties/ddb.yml b/tests/it/test_config_it.data/more-properties/ddb.yml new file mode 100644 index 00000000..37a6c6c2 --- /dev/null +++ b/tests/it/test_config_it.data/more-properties/ddb.yml @@ -0,0 +1,25 @@ +core: + project: + name: custom + required_version: 1.10.0 +jsonnet: + docker: + compose: + project_name: yo-custom + registry: + name: gfiorleans.azurecr.io + repository: yo-custom + virtualhost: + redirect_to_https: true +file: + excludes: + - '**/_*' + - '**/.git' + - '**/node_modules' + - '**/vendor' + - 'frontend/public/workbox*' + - 'frontend/public/dist' +sonar: + host: http://sonarqube.test + token: ~ + project_key: yo-custom diff --git a/tests/it/test_config_it.py b/tests/it/test_config_it.py index 597a6131..124077f5 100644 --- a/tests/it/test_config_it.py +++ b/tests/it/test_config_it.py @@ -77,6 +77,33 @@ def test_config_output_extra_filenames_files_option(self, project_loader, capsys assert configurations['another.config.file.yaml']['app.value'] == 'config' assert configurations['ddb.local.yml']['app.value'] == 'local' + def test_config_output_extra_filenames_some_files_option(self, project_loader, capsys: CaptureFixture): + project_loader("extra-filenames") + + main(["config", "some", "--files"]) + + reset() + + output = capsys.readouterr().out + + parts = [part.lstrip() for part in output.split('---') if part.strip()] + assert len(parts) == 1 + + configurations = {} + + for part in parts: + filename, config = part.split('\n', 1) + assert filename.startswith('# ') + filename = filename[2:] + filename = os.path.relpath(filename, os.getcwd()) + configurations[filename] = Dotty(yaml.safe_load(config)) + + assert ('some.custom.yml',) == \ + tuple(configurations.keys()) + + assert configurations['some.custom.yml']['some'] is True + assert 'app.value' not in configurations['some.custom.yml'] + def test_config_local_falsy(self, project_loader): project_loader("local-falsy") @@ -179,3 +206,103 @@ def test_config_merge_insert_strategy3(self, project_loader): 'dev'] reset() + + def test_config_more_properties_jsonnet_docker(self, project_loader, capsys: CaptureFixture): + project_loader("more-properties") + + main(["config", "jsonnet.docker"]) + + configuration = Dotty(yaml.safe_load(capsys.readouterr().out)) + + assert configuration['jsonnet.docker.compose.project_name'] == 'yo-custom' + assert configuration['jsonnet.docker.registry.name'] == 'gfiorleans.azurecr.io' + assert configuration['jsonnet.docker.registry.repository'] == 'yo-custom' + assert configuration['jsonnet.docker.virtualhost.redirect_to_https'] is True + + assert 'docker' not in configuration + assert 'core' not in configuration + + reset() + + def test_config_more_properties_jsonnet_docker_compose(self, project_loader, capsys: CaptureFixture): + project_loader("more-properties") + + main(["config", "jsonnet.docker.compose"]) + + configuration = Dotty(yaml.safe_load(capsys.readouterr().out)) + + assert configuration['jsonnet.docker.compose.project_name'] == 'yo-custom' + assert 'jsonnet.docker.registry.name' not in configuration + assert 'jsonnet.docker.registry.repository' not in configuration + assert 'jsonnet.docker.virtualhost.redirect_to_https' not in configuration + assert 'docker' not in configuration + assert 'core' not in configuration + + reset() + + def test_config_more_properties_docker_variables_full(self, project_loader, capsys: CaptureFixture): + project_loader("more-properties") + + main(["config", "docker", "--variables", "--full"], reset_disabled=True) + + out = capsys.readouterr().out + + docker_ip = config.data.get('docker.ip') + docker_interface = config.data.get('docker.interface') + docker_user_gid = config.data.get('docker.user.gid') + docker_user_uid = config.data.get('docker.user.uid') + + assert out == f"""docker.disabled: False +docker.interface: {docker_interface} +docker.ip: {docker_ip} +docker.user.gid: {docker_user_gid} +docker.user.group: None +docker.user.name: None +docker.user.uid: {docker_user_uid} +""" + + reset() + + def test_config_more_properties_docker_user_variables(self, project_loader, capsys: CaptureFixture): + project_loader("more-properties") + + main(["config", "docker.user", "--variables"], reset_disabled=True) + + out = capsys.readouterr().out + + docker_user_gid = config.data.get('docker.user.gid') + docker_user_uid = config.data.get('docker.user.uid') + + assert out == f"""docker.user.gid: {docker_user_gid} +docker.user.group: None +docker.user.name: None +docker.user.uid: {docker_user_uid} +""" + + reset() + + def test_config_more_properties_docker_ip_value(self, project_loader, capsys: CaptureFixture): + project_loader("more-properties") + + main(["config", "docker.ip", "--value"], reset_disabled=True) + + out = capsys.readouterr().out + + docker_ip = config.data.get('docker.ip') + + assert out == f"{docker_ip}\n" + + reset() + + def test_config_more_properties_core_env_available_value(self, project_loader, capsys: CaptureFixture): + project_loader("more-properties") + + main(["config", "core.env.available", "--value"], reset_disabled=True) + + out = capsys.readouterr().out + + available = config.data.get('core.env.available') + + assert out == f"{available}\n" + + reset()