diff --git a/.github/workflows/add_validate_build_all.sh b/.github/workflows/add_validate_build_all.sh index 3e996773..3f90808e 100644 --- a/.github/workflows/add_validate_build_all.sh +++ b/.github/workflows/add_validate_build_all.sh @@ -11,7 +11,7 @@ rm -rf ./tmp/ mkdir -p ./tmp/ # This script is written to also work in for example cfengine/cfbs -# where we'd need to downooad cfbs.json. +# where we'd need to download cfbs.json. if [ ! -f ./cfbs.json ] ; then echo "No cfbs.json found in current working directory, downloading from GitHub" curl -L https://raw.githubusercontent.com/cfengine/build-index/refs/heads/master/cfbs.json -o ./tmp/cfbs.json diff --git a/.github/workflows/add_validate_build_all.yaml b/.github/workflows/add_validate_build_all.yml similarity index 96% rename from .github/workflows/add_validate_build_all.yaml rename to .github/workflows/add_validate_build_all.yml index d0773d2e..b5808d65 100644 --- a/.github/workflows/add_validate_build_all.yaml +++ b/.github/workflows/add_validate_build_all.yml @@ -22,7 +22,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install cfbs with pip run: pip3 install cfbs diff --git a/JSON.md b/JSON.md index d4eaed2b..be2e599a 100644 --- a/JSON.md +++ b/JSON.md @@ -368,7 +368,7 @@ Which results in: You can start a project with an alternate index: ``` -cfbs init --index blah +cfbs init --index ./some-index.json ``` ```json @@ -376,12 +376,12 @@ cfbs init --index blah "name": "Example", "description": "Example description", "type": "policy-set", - "index": "blah", + "index": "./some-index.json", "build": [] } ``` -`blah` can be a URL or a relative file path (inside project). +The `--index` argument can either be a relative path to a JSON, as seen above, or a HTTPS URL. ## Index file diff --git a/cfbs/args.py b/cfbs/args.py index 7f85db4d..edd4bd87 100644 --- a/cfbs/args.py +++ b/cfbs/args.py @@ -94,7 +94,11 @@ def get_arg_parser(): help="Don't prompt, use defaults (only for testing)", action="store_true", ) - parser.add_argument("--index", help="Specify alternate index", type=str) + parser.add_argument( + "--index", + help="Specify alternate index (HTTPS URL or relative path to JSON file)", + type=str, + ) parser.add_argument( "--check", help="Check if file(s) would be reformatted", action="store_true" ) diff --git a/cfbs/main.py b/cfbs/main.py index 2efbae59..4506a37d 100644 --- a/cfbs/main.py +++ b/cfbs/main.py @@ -9,6 +9,7 @@ from typing import Union from cfbs.result import Result +from cfbs.validate import validate_index_string from cfbs.version import string as version from cfbs.utils import ( CFBSValidationError, @@ -75,6 +76,13 @@ def _main() -> Union[int, Result]: print_help() raise CFBSUserError("Command '%s' not found" % args.command) + if args.index is not None: + if args.command not in ["init", "add", "search", "validate"]: + raise CFBSUserError( + "'--index' option can not be used with the '%s' command" % args.command + ) + validate_index_string(args.index) + if args.masterfiles and args.command != "init": raise CFBSUserError( "The option --masterfiles is only for 'cfbs init', not 'cfbs %s'" diff --git a/cfbs/pretty.py b/cfbs/pretty.py index 4633ed5b..588afeb6 100644 --- a/cfbs/pretty.py +++ b/cfbs/pretty.py @@ -15,6 +15,7 @@ "repo", "url", "by", + "index", "version", "commit", "subdirectory", diff --git a/cfbs/validate.py b/cfbs/validate.py index eb11be86..5a3eae82 100644 --- a/cfbs/validate.py +++ b/cfbs/validate.py @@ -19,9 +19,7 @@ not in build.py. """ -import argparse import logging as log -import sys import re from collections import OrderedDict from typing import List, Tuple @@ -35,7 +33,6 @@ CFBSValidationError, ) from cfbs.pretty import TOP_LEVEL_KEYS, MODULE_KEYS -from cfbs.cfbs_config import CFBSConfig AVAILABLE_BUILD_STEPS = { "copy": 2, @@ -58,6 +55,25 @@ MAX_BUILD_STEP_LENGTH = 256 +def validate_index_string(index): + assert type(index) is str + if index.strip() == "": + raise CFBSValidationError( + 'The "index" string must be a URL / path (string), not "%s" (whitespace)' + % index + ) + if not index.endswith(".json"): + raise CFBSValidationError( + 'The "index" string must refer to a JSON file / URL (ending in .json)' + ) + if not index.startswith(("https://", "./")): + raise CFBSValidationError( + 'The "index" string must be a URL (starting with https://) or relative path (starting with ./)' + ) + if index.startswith("https://") and " " in index: + raise CFBSValidationError('The "index" URL must not contain spaces') + + def split_build_step(command) -> Tuple[str, List[str]]: terms = command.split(" ") operation, args = terms[0], terms[1:] @@ -131,20 +147,8 @@ def _validate_top_level_keys(config): raise CFBSValidationError( 'The "index" field must either be a URL / path (string) or an inline index (object / dictionary)' ) - if type(index) is str and index.strip() == "": - raise CFBSValidationError( - 'The "index" string must be a URL / path (string), not "%s"' % index - ) - if type(index) is str and not index.endswith(".json"): - raise CFBSValidationError( - 'The "index" string must refer to a JSON file / URL (ending in .json)' - ) - if type(index) is str and not index.startswith(("https://", "./")): - raise CFBSValidationError( - 'The "index" string must be a URL (starting with https://) or relative path (starting with ./)' - ) - if type(index) is str and index.startswith("https://") and " " in index: - raise CFBSValidationError('The "index" URL must not contain spaces') + if type(index) is str: + validate_index_string(index) if "provides" in config: if type(config["provides"]) not in (dict, OrderedDict): @@ -343,280 +347,298 @@ def validate_build_step(name, module, i, operation, args, strict=False): pass -def _validate_module_object(context, name, module, config): - def validate_alias(name, module, context): - if context == "index": - search_in = ("index",) - elif context == "provides": - search_in = "provides" - else: - raise CFBSValidationError( - name, '"alias" is only allowed inside "index" or "provides"' - ) - assert "alias" in module - if len(module) != 1: +def _validate_module_alias(name, module, context, config): + if context == "index": + search_in = ("index",) + elif context == "provides": + search_in = "provides" + else: + raise CFBSValidationError( + name, '"alias" is only allowed inside "index" or "provides"' + ) + assert "alias" in module + if len(module) != 1: + raise CFBSValidationError(name, '"alias" cannot be used with other attributes') + if type(module["alias"]) is not str: + raise CFBSValidationError(name, '"alias" must be of type string') + if not module["alias"]: + raise CFBSValidationError(name, '"alias" must be non-empty') + validate_module_name_content(name) + if not config.can_reach_dependency(module["alias"], search_in): + raise CFBSValidationError( + name, '"alias" must reference another module in the index' + ) + if "alias" in config.find_module(module["alias"], search_in): + raise CFBSValidationError(name, '"alias" cannot reference another alias') + + +def _validate_module_name(name, module): + assert "name" in module + assert name == module["name"] + if type(module["name"]) is not str: + raise CFBSValidationError(name, '"name" must be of type string') + if not module["name"]: + raise CFBSValidationError(name, '"name" must be non-empty') + + validate_module_name_content(name) + + +def _validate_module_description(name, module): + assert "description" in module + if type(module["description"]) is not str: + raise CFBSValidationError(name, '"description" must be of type string') + if not module["description"]: + raise CFBSValidationError(name, '"description" must be non-empty') + + +def _validate_module_tags(name, module): + assert "tags" in module + if type(module["tags"]) is not list: + raise CFBSValidationError(name, '"tags" must be of type list') + for tag in module["tags"]: + if type(tag) is not str: + raise CFBSValidationError(name, '"tags" must be a list of strings') + + +def _validate_module_repo(name, module): + assert "repo" in module + if type(module["repo"]) is not str: + raise CFBSValidationError(name, '"repo" must be of type string') + if not module["repo"]: + raise CFBSValidationError(name, '"repo" must be non-empty') + + +def _validate_module_by(name, module): + assert "by" in module + if type(module["by"]) is not str: + raise CFBSValidationError(name, '"by" must be of type string') + if not module["by"]: + raise CFBSValidationError(name, '"by" must be non-empty') + + +def _validate_module_dependencies(name, module, config, context): + if context == "build": + search_in = ("build",) + elif context == "provides": + search_in = ("index", "provides") + else: + assert context == "index" + search_in = ("index",) + assert "dependencies" in module + if type(module["dependencies"]) is not list: + raise CFBSValidationError( + name, 'Value of attribute "dependencies" must be of type list' + ) + for dependency in module["dependencies"]: + if type(dependency) is not str: + raise CFBSValidationError(name, '"dependencies" must be a list of strings') + if not config.can_reach_dependency(dependency, search_in): raise CFBSValidationError( - name, '"alias" cannot be used with other attributes' + name, + '"dependencies" references a module which could not be found: "%s"' + % dependency, ) - if type(module["alias"]) is not str: - raise CFBSValidationError(name, '"alias" must be of type string') - if not module["alias"]: - raise CFBSValidationError(name, '"alias" must be non-empty') - validate_module_name_content(name) - if not config.can_reach_dependency(module["alias"], search_in): + if "alias" in config.find_module(dependency): + raise CFBSValidationError(name, '"dependencies" cannot reference an alias') + + +def _validate_module_index(name, module): + assert "index" in module + if type(module["index"]) is not str: + raise CFBSValidationError(name, '"index" in "%s" must be a string' % name) + try: + validate_index_string(module["index"]) + except CFBSValidationError as e: + msg = str(e) + " (in module '%s')" % name + raise CFBSValidationError(msg) + + +def _validate_module_version(name, module): + assert "version" in module + if type(module["version"]) is not str: + raise CFBSValidationError(name, '"version" must be of type string') + regex = r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-([0-9]+))?" + if re.fullmatch(regex, module["version"]) is None: + raise CFBSValidationError(name, '"version" must match regex %s' % regex) + + +def _validate_module_commit(name, module): + assert "commit" in module + commit = module["commit"] + if type(commit) is not str: + raise CFBSValidationError(name, '"commit" must be of type string') + if not is_a_commit_hash(commit): + raise CFBSValidationError(name, '"commit" must be a commit reference') + + +def _validate_module_subdirectory(name, module): + assert "subdirectory" in module + if type(module["subdirectory"]) is not str: + raise CFBSValidationError(name, '"subdirectory" must be of type string') + if not module["subdirectory"]: + raise CFBSValidationError(name, '"subdirectory" must be non-empty') + if module["subdirectory"].startswith("./"): + raise CFBSValidationError(name, '"subdirectory" must not start with ./') + if module["subdirectory"].startswith("/"): + raise CFBSValidationError( + name, '"subdirectory" must be a relative path, not starting with /' + ) + if " " in module["subdirectory"]: + raise CFBSValidationError(name, '"subdirectory" cannot contain spaces') + if module["subdirectory"].endswith(("/", "/.")): + raise CFBSValidationError(name, '"subdirectory" must not end with / or /.') + + +def _validate_module_steps(name, module): + assert "steps" in module + if type(module["steps"]) is not list: + raise CFBSValidationError(name, '"steps" must be of type list') + if not module["steps"]: + raise CFBSValidationError(name, '"steps" must be non-empty') + for i, step in enumerate(module["steps"]): + if type(step) is not str: + raise CFBSValidationError(name, '"steps" must be a list of strings') + if not step or step.strip() == "": raise CFBSValidationError( - name, '"alias" must reference another module in the index' + name, '"steps" must be a list of non-empty / non-whitespace strings' ) - if "alias" in config.find_module(module["alias"], search_in): - raise CFBSValidationError(name, '"alias" cannot reference another alias') + operation, args = split_build_step(step) + validate_build_step(name, module, i, operation, args) - def validate_name(name, module): - assert "name" in module - assert name == module["name"] - if type(module["name"]) is not str: - raise CFBSValidationError(name, '"name" must be of type string') - if not module["name"]: - raise CFBSValidationError(name, '"name" must be non-empty') - validate_module_name_content(name) +def _validate_module_url_field(name, module, field): + assert field in module + url = module.get(field) + if url and not url.startswith("https://"): + raise CFBSValidationError(name, '"%s" must be an HTTPS URL' % field) - def validate_description(name, module): - assert "description" in module - if type(module["description"]) is not str: - raise CFBSValidationError(name, '"description" must be of type string') - if not module["description"]: - raise CFBSValidationError(name, '"description" must be non-empty') - - def validate_tags(name, module): - assert "tags" in module - if type(module["tags"]) is not list: - raise CFBSValidationError(name, '"tags" must be of type list') - for tag in module["tags"]: - if type(tag) is not str: - raise CFBSValidationError(name, '"tags" must be a list of strings') - - def validate_repo(name, module): - assert "repo" in module - if type(module["repo"]) is not str: - raise CFBSValidationError(name, '"repo" must be of type string') - if not module["repo"]: - raise CFBSValidationError(name, '"repo" must be non-empty') - - def validate_by(name, module): - assert "by" in module - if type(module["by"]) is not str: - raise CFBSValidationError(name, '"by" must be of type string') - if not module["by"]: - raise CFBSValidationError(name, '"by" must be non-empty') - - def validate_dependencies(name, module, config, context): - if context == "build": - search_in = ("build",) - elif context == "provides": - search_in = ("index", "provides") - else: - assert context == "index" - search_in = ("index",) - assert "dependencies" in module - if type(module["dependencies"]) is not list: + +def _validate_module_input(name, module): + assert "input" in module + if type(module["input"]) is not list or not module["input"]: + raise CFBSValidationError( + name, 'The module\'s "input" must be a non-empty array' + ) + + required_string_fields = ["type", "variable", "namespace", "bundle", "label"] + + required_string_fields_subtype = ["type", "label", "question"] + + for input_element in module["input"]: + if type(input_element) not in (dict, OrderedDict) or not input_element: raise CFBSValidationError( - name, 'Value of attribute "dependencies" must be of type list' + name, + 'The module\'s "input" array must consist of non-empty objects (dictionaries)', ) - for dependency in module["dependencies"]: - if type(dependency) is not str: - raise CFBSValidationError( - name, '"dependencies" must be a list of strings' - ) - if not config.can_reach_dependency(dependency, search_in): + for field in required_string_fields: + if field not in input_element: raise CFBSValidationError( name, - '"dependencies" references a module which could not be found: "%s"' - % dependency, + 'The "%s" field is required in module input elements' % field, ) - if "alias" in config.find_module(dependency): + if ( + type(input_element[field]) is not str + or input_element[field].strip() == "" + ): raise CFBSValidationError( - name, '"dependencies" cannot reference an alias' + name, + 'The "%s" field in input elements must be a non-empty / non-whitespace string' + % field, ) - def validate_version(name, module): - assert "version" in module - if type(module["version"]) is not str: - raise CFBSValidationError(name, '"version" must be of type string') - regex = r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-([0-9]+))?" - if re.fullmatch(regex, module["version"]) is None: - raise CFBSValidationError(name, '"version" must match regex %s' % regex) - - def validate_commit(name, module): - assert "commit" in module - commit = module["commit"] - if type(commit) is not str: - raise CFBSValidationError(name, '"commit" must be of type string') - if not is_a_commit_hash(commit): - raise CFBSValidationError(name, '"commit" must be a commit reference') - - def validate_subdirectory(name, module): - assert "subdirectory" in module - if type(module["subdirectory"]) is not str: - raise CFBSValidationError(name, '"subdirectory" must be of type string') - if not module["subdirectory"]: - raise CFBSValidationError(name, '"subdirectory" must be non-empty') - if module["subdirectory"].startswith("./"): - raise CFBSValidationError(name, '"subdirectory" must not start with ./') - if module["subdirectory"].startswith("/"): + if input_element["type"] not in ("string", "list"): raise CFBSValidationError( - name, '"subdirectory" must be a relative path, not starting with /' + name, + 'The input "type" must be "string" or "list", not "%s"' + % input_element["type"], ) - if " " in module["subdirectory"]: - raise CFBSValidationError(name, '"subdirectory" cannot contain spaces') - if module["subdirectory"].endswith(("/", "/.")): - raise CFBSValidationError(name, '"subdirectory" must not end with / or /.') - - def validate_steps(name, module): - assert "steps" in module - if type(module["steps"]) is not list: - raise CFBSValidationError(name, '"steps" must be of type list') - if not module["steps"]: - raise CFBSValidationError(name, '"steps" must be non-empty') - for i, step in enumerate(module["steps"]): - if type(step) is not str: - raise CFBSValidationError(name, '"steps" must be a list of strings') - if not step or step.strip() == "": - raise CFBSValidationError( - name, '"steps" must be a list of non-empty / non-whitespace strings' - ) - operation, args = split_build_step(step) - validate_build_step(name, module, i, operation, args) - - def validate_url_field(name, module, field): - assert field in module - url = module.get(field) - if url and not url.startswith("https://"): - raise CFBSValidationError(name, '"%s" must be an HTTPS URL' % field) - - def validate_module_input(name, module): - assert "input" in module - if type(module["input"]) is not list or not module["input"]: + if not re.fullmatch(r"[a-z_]+", input_element["variable"]): raise CFBSValidationError( - name, 'The module\'s "input" must be a non-empty array' + name, + '"%s" is not an acceptable variable name, must match regex "[a-z_]+"' + % input_element["variable"], + ) + if not re.fullmatch(r"[a-z_][a-z0-9_]+", input_element["namespace"]): + raise CFBSValidationError( + name, + '"%s" is not an acceptable namespace, must match regex "[a-z_][a-z0-9_]+"' + % input_element["namespace"], + ) + if not re.fullmatch(r"[a-z_]+", input_element["bundle"]): + raise CFBSValidationError( + name, + '"%s" is not an acceptable bundle name, must match regex "[a-z_]+"' + % input_element["bundle"], ) - required_string_fields = ["type", "variable", "namespace", "bundle", "label"] - - required_string_fields_subtype = ["type", "label", "question"] - - for input_element in module["input"]: - if type(input_element) not in (dict, OrderedDict) or not input_element: - raise CFBSValidationError( - name, - 'The module\'s "input" array must consist of non-empty objects (dictionaries)', - ) - for field in required_string_fields: - if field not in input_element: - raise CFBSValidationError( - name, - 'The "%s" field is required in module input elements' % field, - ) - if ( - type(input_element[field]) is not str - or input_element[field].strip() == "" - ): - raise CFBSValidationError( - name, - 'The "%s" field in input elements must be a non-empty / non-whitespace string' - % field, - ) - - if input_element["type"] not in ("string", "list"): + if input_element["type"] == "list": + if "while" not in input_element: raise CFBSValidationError( - name, - 'The input "type" must be "string" or "list", not "%s"' - % input_element["type"], + name, 'For a "list" input element, a "while" prompt is required' ) - if not re.fullmatch(r"[a-z_]+", input_element["variable"]): + if ( + type(input_element["while"]) is not str + or not input_element["while"].strip() + ): raise CFBSValidationError( name, - '"%s" is not an acceptable variable name, must match regex "[a-z_]+"' - % input_element["variable"], + 'The "while" prompt in an input "list" element must be a non-empty / non-whitespace string', ) - if not re.fullmatch(r"[a-z_][a-z0-9_]+", input_element["namespace"]): + if "subtype" not in input_element: raise CFBSValidationError( - name, - '"%s" is not an acceptable namespace, must match regex "[a-z_][a-z0-9_]+"' - % input_element["namespace"], + name, 'For a "list" input element, a "subtype" is required' ) - if not re.fullmatch(r"[a-z_]+", input_element["bundle"]): + if type(input_element["subtype"]) not in (list, dict, OrderedDict): raise CFBSValidationError( name, - '"%s" is not an acceptable bundle name, must match regex "[a-z_]+"' - % input_element["bundle"], + 'The list element "subtype" must be an object or an array of objects (dictionaries)', ) - - if input_element["type"] == "list": - if "while" not in input_element: - raise CFBSValidationError( - name, 'For a "list" input element, a "while" prompt is required' - ) - if ( - type(input_element["while"]) is not str - or not input_element["while"].strip() - ): - raise CFBSValidationError( - name, - 'The "while" prompt in an input "list" element must be a non-empty / non-whitespace string', - ) - if "subtype" not in input_element: - raise CFBSValidationError( - name, 'For a "list" input element, a "subtype" is required' - ) - if type(input_element["subtype"]) not in (list, dict, OrderedDict): - raise CFBSValidationError( - name, - 'The list element "subtype" must be an object or an array of objects (dictionaries)', - ) - subtype = input_element["subtype"] - if type(subtype) is not list: - subtype = [subtype] - for part in subtype: - for field in required_string_fields_subtype: - if field not in part: - raise CFBSValidationError( - name, - 'The "%s" field is required in module input "subtype" objects' - % field, - ) - if type(part[field]) is not str or part[field].strip() == "": - raise CFBSValidationError( - name, - 'The "%s" field in module input "subtype" objects must be a non-empty / non-whitespace string' - % field, - ) - if len(subtype) > 1: - # The "key" field is used to create the JSON objects for each - # input in a list of "things" which are not just strings, - # i.e. consist of multiple values - if ( - "key" not in part - or type(part["key"]) is not str - or part["key"].strip() == "" - ): - raise CFBSValidationError( - name, - 'When using module input with type list, and subtype includes multiple values, "key" is required to distinguish them', - ) - if part["type"] != "string": + subtype = input_element["subtype"] + if type(subtype) is not list: + subtype = [subtype] + for part in subtype: + for field in required_string_fields_subtype: + if field not in part: + raise CFBSValidationError( + name, + 'The "%s" field is required in module input "subtype" objects' + % field, + ) + if type(part[field]) is not str or part[field].strip() == "": raise CFBSValidationError( name, - 'Only "string" supported for the "type" of module input list elements, not "%s"' - % part["type"], + 'The "%s" field in module input "subtype" objects must be a non-empty / non-whitespace string' + % field, ) + if len(subtype) > 1: + # The "key" field is used to create the JSON objects for each + # input in a list of "things" which are not just strings, + # i.e. consist of multiple values + if ( + "key" not in part + or type(part["key"]) is not str + or part["key"].strip() == "" + ): + raise CFBSValidationError( + name, + 'When using module input with type list, and subtype includes multiple values, "key" is required to distinguish them', + ) + if part["type"] != "string": + raise CFBSValidationError( + name, + 'Only "string" supported for the "type" of module input list elements, not "%s"' + % part["type"], + ) + +def _validate_module_object(context, name, module, config): assert context in ("index", "provides", "build") # Step 1 - Handle special cases (alias): if "alias" in module: # Needs to be validated first because it's missing the other fields: - validate_alias(name, module, context) + _validate_module_alias(name, module, context, config) return # alias entries would fail the other validation below # Step 2 - Check for required fields: @@ -650,31 +672,33 @@ def validate_module_input(name, module): # Step 3 - Validate fields: if "name" in module: - validate_name(name, module) + _validate_module_name(name, module) if "description" in module: - validate_description(name, module) + _validate_module_description(name, module) if "tags" in module: - validate_tags(name, module) + _validate_module_tags(name, module) if "repo" in module: - validate_repo(name, module) + _validate_module_repo(name, module) if "by" in module: - validate_by(name, module) + _validate_module_by(name, module) if "dependencies" in module: - validate_dependencies(name, module, config, context) + _validate_module_dependencies(name, module, config, context) + if "index" in module: + _validate_module_index(name, module) if "version" in module: - validate_version(name, module) + _validate_module_version(name, module) if "commit" in module: - validate_commit(name, module) + _validate_module_commit(name, module) if "subdirectory" in module: - validate_subdirectory(name, module) + _validate_module_subdirectory(name, module) if "steps" in module: - validate_steps(name, module) + _validate_module_steps(name, module) if "website" in module: - validate_url_field(name, module, "website") + _validate_module_url_field(name, module, "website") if "documentation" in module: - validate_url_field(name, module, "documentation") + _validate_module_url_field(name, module, "documentation") if "input" in module: - validate_module_input(name, module) + _validate_module_input(name, module) # Step 4 - Additional validation checks: @@ -703,18 +727,3 @@ def _validate_config_for_build_field(config, empty_build_list_ok=False): raise CFBSExitError( "The \"build\" field in ./cfbs.json is empty - add modules with 'cfbs add'" ) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("file", nargs="?", default="./cfbs.json") - args = parser.parse_args() - - config = CFBSConfig.get_instance(filename=args.file, non_interactive=True) - r = validate_config(config) - - sys.exit(r) - - -if __name__ == "__main__": - main()