Skip to content

Commit

Permalink
feat(config): add more capabilities to config command
Browse files Browse the repository at this point in the history
  • Loading branch information
Toilal committed Feb 18, 2021
1 parent 5374233 commit 89d20d6
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 34 deletions.
4 changes: 4 additions & 0 deletions ddb/feature/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
121 changes: 97 additions & 24 deletions ddb/feature/core/actions.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
23 changes: 14 additions & 9 deletions ddb/feature/jsonnet/lib/ddb.docker.libjsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions tests/it/test_config_it.data/more-properties/ddb.yml
Original file line number Diff line number Diff line change
@@ -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
127 changes: 127 additions & 0 deletions tests/it/test_config_it.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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()

0 comments on commit 89d20d6

Please sign in to comment.