-
Notifications
You must be signed in to change notification settings - Fork 9
Implemented spawn config validation #40
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
base: main
Are you sure you want to change the base?
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -77,6 +77,33 @@ cfengine build | |||||
|
|
||||||
| (This is equivalent to running `cfbs build`). | ||||||
|
|
||||||
| ### Spawn and install cfengine from a config | ||||||
|
|
||||||
| **this feature is still in work in progress** | ||||||
|
|
||||||
| Given a yaml config: | ||||||
|
|
||||||
| ```yaml | ||||||
| templates: | ||||||
| ubuntu: | ||||||
| count: 1 | ||||||
|
Member
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. count should be moved to group, not be in template
Contributor
Author
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. Templates is not a real key in the final config, all the names defined in templates get expanded before analyzing the config. Ex: templates:
mycfengine:
version: 3.27.0
groups:
- myhub:
role: hub
cfengine: mycfengineGets turned into: groups:
- myhub:
role: hub
cfengine:
version: 3.27.0Also, I believe it makes sense to have "count" inside "spawn", Ex: groups:
- client1:
role: client
source:
count: 4
mode: spawn
spawn:
provider: vagrant
vagrant:
box: ubuntu/focal64
- client2:
role: client
source:
# count: 4. Here count doesn't make sense, because we have saved hosts
mode: save
hosts: [ 8.8.8.8 ] |
||||||
| mode: spawn | ||||||
| spawn: | ||||||
| provider: vagrant | ||||||
| vagrant: | ||||||
| box: ubuntu/focal64 | ||||||
|
|
||||||
| groups: | ||||||
| myhub: | ||||||
| role: hub | ||||||
| source: ubuntu | ||||||
| ``` | ||||||
|
|
||||||
| It up will spawn the necessary VMs and install cfengine using cf-remote | ||||||
|
Contributor
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.
Suggested change
|
||||||
| ``` | ||||||
| cfengine up config.yaml | ||||||
| ``` | ||||||
|
|
||||||
| ## Supported platforms and versions | ||||||
|
|
||||||
| This tool will only support a limited number of platforms, it is not intended to run everywhere CFEngine runs. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| import os | ||
| import re | ||
| import json | ||
| import yaml | ||
| from cfengine_cli.profile import profile_cfengine, generate_callstack | ||
| from cfengine_cli.dev import dispatch_dev_subcommand | ||
| from cfengine_cli.lint import lint_folder, lint_single_arg | ||
|
|
@@ -14,6 +15,7 @@ | |
| format_policy_fin_fout, | ||
| ) | ||
| from cfengine_cli.utils import UserError | ||
| from cfengine_cli.up import validate_config | ||
| from cfbs.utils import find | ||
| from cfbs.commands import build_command | ||
| from cf_remote.commands import deploy as deploy_command | ||
|
|
@@ -148,3 +150,20 @@ def profile(args) -> int: | |
| generate_callstack(data, args.flamegraph) | ||
|
|
||
| return 0 | ||
|
|
||
|
|
||
| def up(args) -> int: | ||
| content = None | ||
| try: | ||
| with open(args.config, "r") as f: | ||
| content = yaml.safe_load(f) | ||
| except yaml.YAMLError: | ||
| raise UserError("'%s' is not a valid yaml config" % args.config) | ||
|
Contributor
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. f-strings are used everywhere else. Maybe use them here as well? |
||
| except FileNotFoundError: | ||
| raise UserError("'%s' doesn't exist" % args.config) | ||
|
|
||
| validate_config(content) | ||
| if args.validate: | ||
| return 0 | ||
| print("Starting VMs...") | ||
| return 0 | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,225 @@ | ||||||
| from pydantic import BaseModel, model_validator, ValidationError, Field | ||||||
| from typing import Union, Literal, Optional, List, Annotated | ||||||
| from functools import reduce | ||||||
| from cf_remote import log | ||||||
|
|
||||||
| import cfengine_cli.validate as validate | ||||||
| from cfengine_cli.utils import UserError | ||||||
|
|
||||||
|
|
||||||
| # Forces pydantic to throw validation error if config contains unknown keys | ||||||
| class NoExtra(BaseModel, extra="forbid"): | ||||||
| pass | ||||||
|
|
||||||
|
|
||||||
| class Config(NoExtra): | ||||||
| pass | ||||||
|
|
||||||
|
|
||||||
| class AWSConfig(Config): | ||||||
| image: str | ||||||
| size: Literal["micro", "xlarge"] = "micro" | ||||||
|
|
||||||
| @model_validator(mode="after") | ||||||
| def check_aws_config(self): | ||||||
| validate.validate_aws_image(self.image) | ||||||
| return self | ||||||
|
|
||||||
|
|
||||||
| class VagrantConfig(Config): | ||||||
| box: str | ||||||
| memory: int = 512 | ||||||
| cpus: int = 1 | ||||||
| sync_folder: Optional[str] = None | ||||||
| provision: Optional[str] = None | ||||||
|
|
||||||
| @model_validator(mode="after") | ||||||
| def check_vagrant_config(self): | ||||||
| if self.memory < 512: | ||||||
| raise UserError("Cannot allocate less than 512MB to a Vagrant VM") | ||||||
| if self.cpus < 1: | ||||||
| raise UserError("Cannot use less than 1 cpu per Vagrant VM") | ||||||
|
|
||||||
| validate.validate_vagrant_box(self.box) | ||||||
|
|
||||||
| return self | ||||||
|
|
||||||
|
|
||||||
| class GCPConfig(Config): | ||||||
| image: str # There is no list of avalaible GCP platforms to validate against yet | ||||||
|
Contributor
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.
Suggested change
|
||||||
| network: Optional[str] = None | ||||||
| public_ip: bool = True | ||||||
| size: str = "n1-standard-1" | ||||||
|
|
||||||
|
|
||||||
| class AWSProvider(Config): | ||||||
| provider: Literal["aws"] | ||||||
| aws: AWSConfig | ||||||
|
|
||||||
| @model_validator(mode="after") | ||||||
| def check_aws_provider(self): | ||||||
| validate.validate_aws_credentials() | ||||||
| return self | ||||||
|
|
||||||
|
|
||||||
| class GCPProvider(Config): | ||||||
| provider: Literal["gcp"] | ||||||
| gcp: GCPConfig | ||||||
|
|
||||||
| @model_validator(mode="after") | ||||||
| def check_gcp_provider(self): | ||||||
| validate.validate_gcp_credentials() | ||||||
| return self | ||||||
|
|
||||||
|
|
||||||
| class VagrantProvider(Config): | ||||||
| provider: Literal["vagrant"] | ||||||
| vagrant: VagrantConfig | ||||||
|
|
||||||
|
|
||||||
| class SaveMode(Config): | ||||||
| mode: Literal["save"] | ||||||
| hosts: List[str] | ||||||
|
|
||||||
|
|
||||||
| class SpawnMode(Config): | ||||||
| mode: Literal["spawn"] | ||||||
| # "Field" forces pydantic to report errors on the branch defined by the field "provider" | ||||||
| spawn: Annotated[ | ||||||
| Union[VagrantProvider, AWSProvider, GCPProvider], | ||||||
| Field(discriminator="provider"), | ||||||
| ] | ||||||
| count: int | ||||||
|
|
||||||
| @model_validator(mode="after") | ||||||
| def check_spawn_config(self): | ||||||
| if self.count < 1: | ||||||
| raise UserError("Cannot spawn less than 1 instance") | ||||||
| return self | ||||||
|
|
||||||
|
|
||||||
| class CFEngineConfig(Config): | ||||||
| version: Optional[str] = None | ||||||
| bootstrap: Optional[str] = None | ||||||
| edition: Literal["community", "enterprise"] = "enterprise" | ||||||
| remote_download: bool = False | ||||||
| hub_package: Optional[str] = None | ||||||
| client_package: Optional[str] = None | ||||||
| package: Optional[str] = None | ||||||
| demo: bool = False | ||||||
|
|
||||||
| @model_validator(mode="after") | ||||||
| def check_cfengine_config(self): | ||||||
| packages = [self.package, self.hub_package, self.client_package] | ||||||
| for p in packages: | ||||||
| validate.validate_package(p, self.remote_download) | ||||||
|
|
||||||
| if self.version and any(packages): | ||||||
| log.warning("Specifying package overrides cfengine version") | ||||||
|
|
||||||
| validate.validate_version(self.version, self.edition) | ||||||
| validate.validate_state_bootstrap(self.bootstrap) | ||||||
|
|
||||||
| return self | ||||||
|
|
||||||
|
|
||||||
| class GroupConfig(Config): | ||||||
| role: Literal["client", "hub"] | ||||||
| # "Field" forces pydantic to report errors on the branch defined by the field "provider" | ||||||
|
Contributor
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.
Suggested change
|
||||||
| source: Annotated[Union[SaveMode, SpawnMode], Field(discriminator="mode")] | ||||||
| cfengine: Optional[CFEngineConfig] = None | ||||||
| scripts: Optional[List[str]] = None | ||||||
|
|
||||||
| @model_validator(mode="after") | ||||||
| def check_group_config(self): | ||||||
| if ( | ||||||
| self.role == "hub" | ||||||
| and self.source.mode == "spawn" | ||||||
| and self.source.count != 1 | ||||||
| ): | ||||||
| raise UserError("A hub can only have one host") | ||||||
|
|
||||||
| return self | ||||||
|
|
||||||
|
|
||||||
| def rgetattr(obj, attr, *args): | ||||||
|
Contributor
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. Unused function? |
||||||
| def _getattr(obj, attr): | ||||||
| return getattr(obj, attr, *args) | ||||||
|
|
||||||
| return reduce(_getattr, [obj] + attr.split(".")) | ||||||
|
|
||||||
|
|
||||||
| class Group: | ||||||
| """ | ||||||
| All group-specific data: | ||||||
| - Vagrantfile | ||||||
| Config that declares it: | ||||||
| - provider, count, cfengine version, role, ... | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, config: GroupConfig): | ||||||
| self.config = config | ||||||
| self.hosts = [] | ||||||
|
|
||||||
|
|
||||||
| class Host: | ||||||
|
Contributor
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. Unused? |
||||||
| """ | ||||||
| All host-specific data: | ||||||
| - user, ip, ssh config, OS, uuid, ... | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self): | ||||||
| pass | ||||||
|
|
||||||
|
|
||||||
| def _resolve_templates(parent, templates): | ||||||
| if not parent: | ||||||
| return | ||||||
| if isinstance(parent, dict): | ||||||
| for key, value in parent.items(): | ||||||
| if isinstance(value, str) and value in templates: | ||||||
| parent[key] = templates[value] | ||||||
| else: | ||||||
| _resolve_templates(value, templates) | ||||||
| if isinstance(parent, list): | ||||||
| for value in parent: | ||||||
| _resolve_templates(value, templates) | ||||||
|
|
||||||
|
|
||||||
| def validate_config(content): | ||||||
| if not content: | ||||||
| raise UserError("Empty spawn config") | ||||||
|
|
||||||
| if "groups" not in content: | ||||||
| raise UserError("Missing 'groups' key in spawn config") | ||||||
|
|
||||||
| groups = content["groups"] | ||||||
| templates = content.get("templates") | ||||||
| if templates: | ||||||
| _resolve_templates(groups, templates) | ||||||
|
|
||||||
| if not isinstance(groups, list): | ||||||
| groups = [groups] | ||||||
|
|
||||||
| state = {} | ||||||
| try: | ||||||
| for g in groups: | ||||||
| if len(g) != 1: | ||||||
| raise UserError( | ||||||
| "Too many keys in group definition: {}".format( | ||||||
| ", ".join(list(g.keys())) | ||||||
| ) | ||||||
| ) | ||||||
|
|
||||||
| for k, v in g.items(): | ||||||
| state[k] = Group(GroupConfig(**v)) | ||||||
|
|
||||||
| except ValidationError as v: | ||||||
| msgs = [] | ||||||
| for err in v.errors(): | ||||||
| msgs.append( | ||||||
| "{}. Input '{}' at location '{}'".format( | ||||||
| err["msg"], err["input"], err["loc"] | ||||||
| ) | ||||||
| ) | ||||||
| raise UserError("\n".join(msgs)) | ||||||
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.