Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Check if the .sugar.yaml file is valid according to the schema file #131

Merged
merged 3 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .makim.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: 1.0.0
backend: bash
groups:
clean:
Expand Down Expand Up @@ -56,7 +55,7 @@ groups:
args:
params:
help: Extra parameters for pytest
type: str
type: string
default: "-vv"
run: pytest -s ${{ args.params }} tests

Expand Down Expand Up @@ -97,8 +96,9 @@ groups:
sugar ${{ vars.SUGAR_FLAGS }} compose stop --all
sugar ${{ vars.SUGAR_FLAGS }} compose run --service service2-1 --options -T --cmd env
sugar ${{ vars.SUGAR_FLAGS }} compose down

smoke-services:
help:
help: Run smoke test for services
hooks:
pre-run:
- task: docker.killall
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
default_stages:
- commit
- pre-commit

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
12 changes: 3 additions & 9 deletions .sugar.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
version: 1.0
backend: compose
env-file: .env
defaults:
group: ${{ env.SUGAR_GROUP }}
project-name: sugar-${{ env.SUGAR_PROJECT_NAME }}
group: "${{ env.SUGAR_GROUP }}"
project-name: "sugar-${{ env.SUGAR_PROJECT_NAME }}"
groups:
group1:
project-name: project1 # optional
project-name: project1
config-path: tests/containers/group1/compose.yaml
env-file: .env
services:
Expand All @@ -17,23 +16,19 @@ groups:
- name: service1-3

group2:
project-name: null # optional
config-path: tests/containers/group2/compose.yaml
env-file: .env
services:
default: null
available:
- name: service2-1
- name: service2-2

group-mix:
project-name: null # optional
config-path:
- tests/containers/group1/compose.yaml
- tests/containers/group2/compose.yaml
env-file: .env
services:
default: null
available:
- name: service1-1
- name: service1-2
Expand All @@ -45,7 +40,6 @@ groups:
- tests/containers/group1/compose.yaml
env-file: .env
services:
# default: service1-1,service1-2
available:
- name: service1-1
- name: service1-2
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ First you need to place the config file `.sugar.yaml` in the root of your
project. This is an example of a configuration file:

```yaml
version: 1.0
backend: docker compose
default:
group: ${{ env.ENV }}
Expand Down
3 changes: 1 addition & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ First you need to place the config file `.sugar.yaml` in the root of your
project. This is an example of a configuration file:

```yaml
version: 1.0
backend: docker compose
backend: compose
default:
group: ${{ "${{ env.ENV }}" }}
groups:
Expand Down
935 changes: 437 additions & 498 deletions poetry.lock

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ license = "BSD 3 Clause"
packages = [
{include = "sugar", from="src"},
]
include = ["src/sugar/py.typed"]
include = [
"src/sugar/py.typed",
"src/sugar/schema.json",
]
exclude = [
".git/*",
".env*",
Expand All @@ -30,6 +33,7 @@ textual = { version = ">=0.48", optional = true }
plotille = { version = ">=5", optional = true }
compose-go = ">=1.27.0"
xonsh = ">=0.15.0"
jsonschema = ">=4"

[tool.poetry.extras]
tui = [
Expand Down Expand Up @@ -57,7 +61,7 @@ mkdocs-literate-nav = ">=0.6.0"
mkdocs-macros-plugin = ">=0.7.0,<1"
mkdocs-material = ">=9.1.15"
mkdocstrings = {version=">=0.19.0", extras=["python"]}
makim = ">=1.18.1"
makim = ">=1.19.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
78 changes: 65 additions & 13 deletions src/sugar/extensions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import io
import json
import os
import shlex
import sys
Expand All @@ -17,9 +18,10 @@
import yaml

from jinja2 import Environment
from jsonschema import ValidationError, validate

from sugar import __version__
from sugar.logs import SugarErrorType, SugarLogs
from sugar.logs import SugarError, SugarLogs
from sugar.utils import camel_to_snake

TEMPLATE = Environment(
Expand All @@ -28,6 +30,8 @@
variable_end_string='}}',
)

SUGAR_CURRENT_PATH = Path(__file__).parent.parent


class SugarBase:
"""SugarBase defined the base structure for the Sugar classes."""
Expand Down Expand Up @@ -153,12 +157,12 @@ def _call_backend_app(
try:
p.wait()
except sh.ErrorReturnCode as e:
SugarLogs.raise_error(str(e), SugarErrorType.SH_ERROR_RETURN_CODE)
SugarLogs.raise_error(str(e), SugarError.SH_ERROR_RETURN_CODE)
except KeyboardInterrupt:
pid = p.pid
p.kill()
SugarLogs.raise_error(
f'Process {pid} killed.', SugarErrorType.SH_KEYBOARD_INTERRUPT
f'Process {pid} killed.', SugarError.SH_KEYBOARD_INTERRUPT
)
self._execute_hooks('post-run', extension, action)

Expand Down Expand Up @@ -237,7 +241,7 @@ def _filter_service_group(self) -> None:
SugarLogs.raise_error(
'The service group parameter or default '
"group configuration weren't defined.",
SugarErrorType.SUGAR_INVALID_PARAMETER,
SugarError.SUGAR_INVALID_PARAMETER,
)
selected_group_name = default_group
else:
Expand Down Expand Up @@ -269,7 +273,7 @@ def _filter_service_group(self) -> None:
SugarLogs.raise_error(
f'The given group service "{selected_group_name}" was not found '
'in the configuration file.',
SugarErrorType.SUGAR_MISSING_PARAMETER,
SugarError.SUGAR_MISSING_PARAMETER,
)

def _load_config(self) -> None:
Expand All @@ -283,15 +287,17 @@ def _load_config(self) -> None:
if not (self.config.get('services') or self.config.get('groups')):
SugarLogs.raise_error(
'Either `services` OR `groups` flag must be given',
SugarErrorType.SUGAR_INVALID_CONFIGURATION,
SugarError.SUGAR_INVALID_CONFIGURATION,
)
# check if both services and groups are present
if self.config.get('services') and self.config.get('groups'):
SugarLogs.raise_error(
'`services` and `groups` flags given, only 1 is allowed.',
SugarErrorType.SUGAR_INVALID_CONFIGURATION,
SugarError.SUGAR_INVALID_CONFIGURATION,
)

self._validate_config()

# Load hooks
self.hooks = self.config.get('hooks', {})

Expand All @@ -303,7 +309,7 @@ def _load_backend_app(self) -> None:
SugarLogs.raise_error(
f'"{self.config["backend"]}" not supported yet.'
f' Supported backends are: {", ".join(supported_backends)}.',
SugarErrorType.SUGAR_COMPOSE_APP_NOT_SUPPORTED,
SugarError.SUGAR_COMPOSE_APP_NOT_SUPPORTED,
)

self.backend_app = sh.docker
Expand All @@ -328,7 +334,7 @@ def _load_backend_args(self) -> None:
'The attribute config-path` just supports the data '
f'types `string` or `list`, {type(backend_path_arg)} '
'received.',
SugarErrorType.SUGAR_INVALID_CONFIGURATION,
SugarError.SUGAR_INVALID_CONFIGURATION,
)

for p in config_path:
Expand Down Expand Up @@ -367,7 +373,7 @@ def _load_env(self) -> None:
if not Path(env_file).exists():
SugarLogs.raise_error(
'The given env-file was not found.',
SugarErrorType.SUGAR_INVALID_CONFIGURATION,
SugarError.SUGAR_INVALID_CONFIGURATION,
)
self.env.update(dotenv.dotenv_values(env_file)) # type: ignore

Expand Down Expand Up @@ -409,22 +415,68 @@ def _get_services_names(self, **kwargs: Any) -> list[str]:
SugarLogs.raise_error(
'If you want to execute the operation for all services, '
'use --all parameter.',
SugarErrorType.SUGAR_INVALID_PARAMETER,
SugarError.SUGAR_INVALID_PARAMETER,
)

return service_names

def _validate_config(self) -> None:
"""
Validate the .sugar.yaml against the predefined JSON Schema.

Raises
------
SugarError: If the configuration does not conform to the schema.
"""
try:
with open(SUGAR_CURRENT_PATH / 'schema.json', 'r') as schema_file:
schema = json.load(schema_file)

config_data = self.config

# Validate the configuration against the schema
validate(instance=config_data, schema=schema)

if self.verbose:
SugarLogs.print_info('Configuration validation successful.')

except ValidationError as ve:
error_message = f'Configuration validation error: {ve.message}'
SugarLogs.raise_error(
error_message, SugarError.CONFIG_VALIDATION_ERROR
)
except yaml.YAMLError as ye:
error_message = f'YAML parsing error: {ye}'
SugarLogs.raise_error(error_message, SugarError.YAML_PARSING_ERROR)
except json.JSONDecodeError as je:
error_message = f'JSON schema decoding error: {je}'
SugarLogs.raise_error(
error_message, SugarError.JSON_SCHEMA_DECODING_ERROR
)
except FileNotFoundError:
error_message = f'Configuration file {self.file} not found.'
SugarLogs.raise_error(
error_message, SugarError.SUGAR_CONFIG_FILE_NOT_FOUND
)
except Exception as e:
error_message = (
f'Unexpected error during configuration validation: {e}'
)
SugarLogs.raise_error(
error_message, SugarError.CONFIG_VALIDATION_UNEXPECTED_ERROR
)

def _verify_config(self) -> None:
if not self._check_config_file():
SugarLogs.raise_error(
'Config file .sugar.yaml not found.',
SugarErrorType.SUGAR_INVALID_CONFIGURATION,
SugarError.SUGAR_INVALID_CONFIGURATION,
)

if not len(self.config.get('groups', {})):
SugarLogs.raise_error(
'No service groups found.',
SugarErrorType.SUGAR_INVALID_CONFIGURATION,
SugarError.SUGAR_INVALID_CONFIGURATION,
)

def _version(self) -> None:
Expand Down
4 changes: 2 additions & 2 deletions src/sugar/extensions/compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from sugar.docs import docparams
from sugar.extensions.base import SugarBase
from sugar.logs import SugarErrorType, SugarLogs
from sugar.logs import SugarError, SugarLogs

doc_group = {
'group': 'Specify the group name of the services you want to use.'
Expand Down Expand Up @@ -351,7 +351,7 @@ def _cmd_run(
if not service:
SugarLogs.raise_error(
'`run` sub-command expected --service parameter.',
SugarErrorType.SUGAR_MISSING_PARAMETER,
SugarError.SUGAR_MISSING_PARAMETER,
)
services_names = self._get_service_name(service)
options_args = self._get_list_args(options)
Expand Down
4 changes: 2 additions & 2 deletions src/sugar/extensions/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
doc_common_services_no_options,
)
from sugar.inspect import get_container_name, get_container_stats
from sugar.logs import SugarErrorType, SugarLogs
from sugar.logs import SugarError, SugarLogs

CHART_WINDOW_DURATION = 60
CHART_TIME_INTERVAL = 1
Expand Down Expand Up @@ -296,7 +296,7 @@ def _cmd_plot(
service_names_txt = ', '.join(services_names)
SugarLogs.raise_error(
f'No container found for the services: {service_names_txt}',
SugarErrorType.SUGAR_NO_SERVICES_RUNNING,
SugarError.SUGAR_NO_SERVICES_RUNNING,
)

containers_ids = [cids for cids in raw_out.split('\n') if cids]
Expand Down
25 changes: 15 additions & 10 deletions src/sugar/logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,23 @@
from colorama import Fore


class SugarErrorType(Enum):
"""SugarErrorType group all error types handled by the system."""
class SugarError(Enum):
"""SugarError group all error types handled by the system."""

SH_ERROR_RETURN_CODE = 1
SH_KEYBOARD_INTERRUPT = 2
SUGAR_COMPOSE_APP_NOT_SUPPORTED = 3
SUGAR_COMPOSE_APP_NOT_FOUNDED = 4
SUGAR_INVALID_PARAMETER = 5
SUGAR_MISSING_PARAMETER = 6
SUGAR_INVALID_CONFIGURATION = 7
SUGAR_ACTION_NOT_IMPLEMENTED = 8
SUGAR_NO_SERVICES_RUNNING = 9
SUGAR_CONFIG_FILE_NOT_FOUND = 3
SUGAR_COMPOSE_APP_NOT_SUPPORTED = 4
SUGAR_COMPOSE_APP_NOT_FOUNDED = 5
SUGAR_INVALID_PARAMETER = 6
SUGAR_MISSING_PARAMETER = 7
SUGAR_INVALID_CONFIGURATION = 8
SUGAR_ACTION_NOT_IMPLEMENTED = 9
SUGAR_NO_SERVICES_RUNNING = 10
CONFIG_VALIDATION_ERROR = 11
YAML_PARSING_ERROR = 12
JSON_SCHEMA_DECODING_ERROR = 13
CONFIG_VALIDATION_UNEXPECTED_ERROR = 14


class SugarLogs:
Expand All @@ -29,7 +34,7 @@ class SugarLogs:
@staticmethod
def raise_error(
message: str,
message_type: SugarErrorType = SugarErrorType.SH_ERROR_RETURN_CODE,
message_type: SugarError = SugarError.SH_ERROR_RETURN_CODE,
) -> None:
"""Print error message and exit with given error code."""
print(Fore.RED, f'[EE] {message}', Fore.RESET)
Expand Down
Loading