Skip to content

Commit

Permalink
Carry over top level values from the extended files to the built dock…
Browse files Browse the repository at this point in the history
…er-compose.yml file, merging them. Carry over the highest value "version" number.
  • Loading branch information
lukasz-zaroda committed Apr 2, 2024
1 parent 61d05f2 commit b2d534a
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 17 deletions.
91 changes: 75 additions & 16 deletions core/dk/compose_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import yaml
from colorama import Fore, Style
from packaging import version

from dk.config_manager import ConfigManager

Expand Down Expand Up @@ -123,6 +124,10 @@ def __to_compose_dict(self, cleaned: bool = True):
compose_dict = copy.deepcopy(self.__content)
services = compose_dict['services']

extended_files: dict = self.__gather_extended_files(services)
compose_dict = self.__merge_top_level_values(compose_dict, extended_files)

# Handle services.
for service_name in services:
service_data = services[service_name]

Expand All @@ -132,29 +137,19 @@ def __to_compose_dict(self, cleaned: bool = True):
# Validate the basic structure.
if 'extends' in service_data:
extends = service_data['extends']
self.__validate_extends(service_name, extends)

remote_file_path = os.path.dirname(self.recipe_path) + os.sep + extends['file']
if not isinstance(remote_file_path, str):
raise ValueError(
f"Error in the '{service_name}' service. The 'file' value has to be a "
f"string."
)

if 'service' not in extends:
raise ValueError(
f"Error in the '{service_name}' service. The 'service' value is required "
f"if the service extends another service."
)
remote_file_service = extends['service']
if not isinstance(remote_file_service, str):
raise ValueError(
f"Error in the '{service_name}' service. The 'service' value has to be "
f"a string."
)

with open(remote_file_path, "r", encoding='utf8') as f:
remote_file_dict = yaml.safe_load(f)
if remote_file_path not in extended_files:
with open(remote_file_path, "r", encoding='utf8') as f:
extended_files[remote_file_path] = yaml.safe_load(f)

remote_file_dict = extended_files[remote_file_path]

self.__validate_service_in_extended_compose(remote_file_service, remote_file_dict)

Expand Down Expand Up @@ -189,6 +184,61 @@ def __to_compose_dict(self, cleaned: bool = True):

return compose_dict

def __gather_extended_files(self, services: dict) -> dict:
"""Gathers the values of all files extended in the recipe.
"""

extended_files: dict = {}

# Gather the extended files.
for service_name in services:
service_data = services[service_name]

# Validate the basic structure.
if 'extends' in service_data:
extends = service_data['extends']
self.__validate_extends(service_name, extends)

remote_file_path = os.path.dirname(self.recipe_path) + os.sep + extends['file']
if not isinstance(remote_file_path, str):
raise ValueError(
f"Error in the '{service_name}' service. The 'file' value has to be a "
f"string."
)

if 'service' not in extends:
raise ValueError(
f"Error in the '{service_name}' service. The 'service' value is required "
f"if the service extends another service."
)

if remote_file_path not in extended_files:
with open(remote_file_path, "r", encoding='utf8') as f:
extended_files[remote_file_path] = yaml.safe_load(f)

return extended_files

def __merge_top_level_values(self, compose: dict, extended_files: dict) -> dict:
"""Merges values from the extended files into the resulting compose file.
"""
# Merge other top level values from the extended files into the compose file.
for _, extended_file in extended_files.items():
for top_level_key in extended_file:
# Services have been already handled.
if top_level_key == 'services':
continue

top_level_value = extended_file[top_level_key]
if top_level_key not in compose:
compose[top_level_key] = top_level_value
else:
compose[top_level_key] = self.__merge_top_level_value(
top_level_key,
top_level_value,
compose[top_level_key],
)
return compose

def __volume_is_absolute(self, volume: str|dict) -> bool:
"""Checks if volume is absolute.
"""
Expand All @@ -202,7 +252,7 @@ def __volume_is_named(self, volume: str|dict, compose: dict) -> bool:
if isinstance(volume, str):
volume_splitted = volume.split(':')
if len(volume_splitted) == 0:
raise ValueError("")
raise ValueError("Incorrect volumes string format")
path = volume.split(':')[0]
else:
path = volume['source']
Expand Down Expand Up @@ -249,6 +299,15 @@ def __validate_service_in_extended_compose(self, service_name: str, compose: dic
raise ValueError(f"Error in the '{service_name}' service. It's missing in the "
f"extended compose file.")

def __merge_top_level_value(self, name: str, new_value, existing_value):
"""Merges top-level compose values.
"""
# We want to get the highest version.
if name == 'version':
return new_value if version.parse(new_value) > version.parse(existing_value)\
else existing_value
return existing_value | new_value

class ComposeManager:
"""This class is responsible for building a compose file based on the given recipe.
"""
Expand Down
3 changes: 2 additions & 1 deletion core/dk/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
colorama==0.4.6
python-dotenv==1.0.1
PyYAML==6.0.1
PyYAML==6.0.1
packaging==24.0
65 changes: 65 additions & 0 deletions tests/functional/tests.bats
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,71 @@ EOF
grep -q -- "- $NAMED_VOLUME_1:" "$COMPOSE_PATH"
}

@test "Build compose: named volumes are carried over from the extended files" {
_initialize_test_environment
NAMED_VOLUME_1=named_volume
# Create the recipe.
cat > "$RECIPE_PATH" << EOF
services:
php:
extends:
file: ../../services/php/services.yml
service: php
EOF
PHP_SERVICE_PATH="${TEST_PROJECT_PATH}/.draky/services/php"
mkdir -p ${PHP_SERVICE_PATH}
# Create an external service file.
cat > "${PHP_SERVICE_PATH}/services.yml" << EOF
services:
php:
image: php-image
volumes:
- ${NAMED_VOLUME_1}:/test-volume
volumes:
${NAMED_VOLUME_1}:
EOF
${DRAKY} env build
grep -q -- "- $NAMED_VOLUME_1:" "$COMPOSE_PATH"
}

@test "Build compose: the highest version is carried over from the extended files" {
_initialize_test_environment
NAMED_VOLUME_1=named_volume
# Create the recipe.
cat > "$RECIPE_PATH" << EOF
services:
php:
extends:
file: ../../services/php/services.yml
service: php
nginx:
extends:
file: ../../services/nginx/services.yml
service: nginx
EOF
PHP_SERVICE_PATH="${TEST_PROJECT_PATH}/.draky/services/php"
mkdir -p ${PHP_SERVICE_PATH}
# Create an external service file.
cat > "${PHP_SERVICE_PATH}/services.yml" << EOF
version: '3.4'
services:
php:
image: php-image
EOF
NGINX_SERVICE_PATH="${TEST_PROJECT_PATH}/.draky/services/nginx"
mkdir -p ${NGINX_SERVICE_PATH}
# Create an external service file.
cat > "${NGINX_SERVICE_PATH}/services.yml" << EOF
version: '3.1'
services:
nginx:
image: nginx-image
EOF

${DRAKY} env build
grep -q -- "version: '3.4'" "$COMPOSE_PATH"
}

@test "Build compose: variable substitution flag" {
_initialize_test_environment
# Create the recipe.
Expand Down

0 comments on commit b2d534a

Please sign in to comment.