-
Notifications
You must be signed in to change notification settings - Fork 16
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
Expand config validation to most of the config #73
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,68 +9,45 @@ | |
from typing import Dict, List, Optional, Set, Union | ||
|
||
# pylint: disable=no-name-in-module | ||
from pydantic import BaseModel, validator, ValidationError | ||
from pydantic import BaseModel, Field, validator, ValidationError | ||
|
||
from buildrunner.validation.errors import Errors, get_validation_errors | ||
from buildrunner.validation.step import Step, StepPushCommitDict | ||
|
||
class Errors: | ||
""" Error class for storing validation errors """ | ||
class Error: | ||
""" Error class for storing validation error """ | ||
def __init__(self, field: str, message: str): | ||
self.field: str = field | ||
self.message: Union[str, None] = message | ||
|
||
def __init__(self): | ||
self.errors = [] | ||
|
||
def add(self, field: str, message: str): | ||
""" Add an error """ | ||
self.errors.append(self.Error(field, message)) | ||
|
||
def count(self): | ||
""" Return the number of errors """ | ||
return len(self.errors) | ||
|
||
def __str__(self): | ||
return '\n'.join([f' {error.field}: {error.message}' for error in self.errors]) | ||
|
||
def __repr__(self): | ||
return self.__str__() | ||
|
||
|
||
class StepBuild(BaseModel): | ||
""" Build model within a step """ | ||
path: Optional[str] | ||
dockerfile: Optional[str] | ||
pull: Optional[bool] | ||
platform: Optional[str] | ||
platforms: Optional[List[str]] | ||
|
||
|
||
class StepPushDict(BaseModel): | ||
""" Push model within a step """ | ||
repository: str | ||
tags: Optional[List[str]] | ||
|
||
|
||
class Step(BaseModel): | ||
""" Step model """ | ||
build: Optional[Union[StepBuild, str]] | ||
push: Optional[Union[StepPushDict, List[Union[str, StepPushDict]], str]] | ||
|
||
def is_multi_platform(self): | ||
""" | ||
Check if the step is a multi-platform build step | ||
""" | ||
return isinstance(self.build, StepBuild) and \ | ||
self.build.platforms is not None | ||
class Config(BaseModel, extra='forbid'): | ||
""" Top level config model """ | ||
|
||
# Unclear if this is actively used | ||
class GithubModel(BaseModel, extra='forbid'): | ||
""" Github model """ | ||
endpoint: str | ||
version: str | ||
username: str | ||
app_token: str | ||
|
||
class SSHKey(BaseModel, extra='forbid'): | ||
""" SSH key model """ | ||
file: Optional[str] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these working without assigning "None" to the value? That's surprising to me, I thought newer pydantic versions required it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it appears to be working. But I don't quite understand what you are saying. Can you give me an example of what you mean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've had to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is using an older version of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, yes, I knew that |
||
key: Optional[str] | ||
password: Optional[str] | ||
prompt_password: Optional[bool] = Field(alias='prompt-password') | ||
aliases: Optional[List[str]] | ||
|
||
class Config(BaseModel): | ||
""" Top level config model """ | ||
version: Optional[float] | ||
steps: Optional[Dict[str, Step]] | ||
|
||
github: Optional[Dict[str, GithubModel]] | ||
# Global config attributes | ||
env: Optional[Dict[str, str]] | ||
build_servers: Optional[Dict[str, Union[str, List[str]]]] = Field(alias='build-servers') | ||
# Intentionally has loose restrictions on ssh-keys since documentation isn't clear | ||
ssh_keys: Optional[Union[SSHKey, List[SSHKey]]] = Field(alias='ssh-keys') | ||
local_files: Optional[Dict[str, str]] = Field(alias='local-files') | ||
caches_root: Optional[str] = Field(alias='caches-root') | ||
docker_registry: Optional[str] = Field(alias='docker-registry') | ||
temp_dir: Optional[str] = Field(alias='temp-dir') | ||
|
||
# Note this is pydantic version 1.10 syntax | ||
@validator('steps') | ||
@classmethod | ||
|
@@ -82,7 +59,7 @@ def validate_steps(cls, values) -> None: | |
ValueError | pydantic.ValidationError : If the config file is invalid | ||
""" | ||
|
||
def validate_push(push: Union[StepPushDict, List[Union[str, StepPushDict]], str], | ||
def validate_push(push: Union[StepPushCommitDict, str, List[Union[str, StepPushCommitDict]]], | ||
mp_push_tags: Set[str], | ||
step_name: str, | ||
update_mp_push_tags: bool = True): | ||
|
@@ -107,7 +84,7 @@ def validate_push(push: Union[StepPushDict, List[Union[str, StepPushDict]], str] | |
if ":" not in name: | ||
name = f'{name}:latest' | ||
|
||
if isinstance(push, StepPushDict): | ||
if isinstance(push, StepPushCommitDict): | ||
names = [f"{push.repository}:{tag}" for tag in push.tags] | ||
|
||
if names is not None: | ||
|
@@ -169,14 +146,6 @@ def validate_multi_platform_build(mp_push_tags: Set[str]): | |
return values | ||
|
||
|
||
def _add_validation_errors(exc: ValidationError) -> Errors: | ||
errors = Errors() | ||
for error in exc.errors(): | ||
loc = [str(item) for item in error["loc"]] | ||
errors.add(field='.'.join(loc), message=f'{error["msg"]} ({error["type"]})') | ||
return errors | ||
|
||
|
||
def validate_config(**kwargs) -> Errors: | ||
""" | ||
Check if the config file is valid | ||
|
@@ -188,5 +157,5 @@ def validate_config(**kwargs) -> Errors: | |
try: | ||
Config(**kwargs) | ||
except ValidationError as exc: | ||
errors = _add_validation_errors(exc) | ||
errors = get_validation_errors(exc) | ||
return errors |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
""" | ||
Copyright 2023 Adobe | ||
All Rights Reserved. | ||
|
||
NOTICE: Adobe permits you to use, modify, and distribute this file in accordance | ||
with the terms of the Adobe license agreement accompanying it. | ||
""" | ||
from typing import Union | ||
|
||
from pydantic import ValidationError | ||
|
||
|
||
class Errors: | ||
""" Error class for storing validation errors """ | ||
class Error: | ||
""" Error class for storing validation error """ | ||
def __init__(self, field: str, message: str): | ||
self.field: str = field | ||
self.message: Union[str, None] = message | ||
|
||
def __init__(self): | ||
self.errors = [] | ||
|
||
def add(self, field: str, message: str): | ||
""" Add an error """ | ||
self.errors.append(self.Error(field, message)) | ||
|
||
def count(self): | ||
""" Return the number of errors """ | ||
return len(self.errors) | ||
|
||
def __str__(self): | ||
return '\n'.join([f' {error.field}: {error.message}' for error in self.errors]) | ||
|
||
def __repr__(self): | ||
return self.__str__() | ||
|
||
|
||
def get_validation_errors(exc: ValidationError) -> Errors: | ||
""" Get validation errors to an Errors object """ | ||
errors = Errors() | ||
for error in exc.errors(): | ||
loc = [str(item) for item in error["loc"]] | ||
if error["type"] == "value_error.extra": | ||
errors.add(field='.'.join(loc), message='not a valid field, please check the spelling and documentation') | ||
else: | ||
errors.add(field='.'.join(loc), message=f'{error["msg"]} ({error["type"]})') | ||
return errors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is cool syntax, didn't know that was possible. Should the Config top-level class also forbid extra?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was just afraid that I may have missed something. I'll add it in to the top-level config. Then if it fails we can address where the issue is, either update the validation code, the documentation or the buildrunner config with the issue.