|
8 | 8 |
|
9 | 9 | from ..context import StackletContext |
10 | 10 | from ..exceptions import InvalidInputException |
11 | | -from ..graphql.cli import GraphQLCommand, register_graphql_commands |
| 11 | +from ..graphql.cli import GraphQLCommand, register_graphql_commands, run_graphql |
12 | 12 | from ..graphql.snippets import ( |
| 13 | + AddPolicyCollection, |
13 | 14 | AddRepository, |
14 | 15 | ListRepository, |
15 | 16 | ProcessRepository, |
16 | 17 | RemoveRepository, |
17 | 18 | ScanRepository, |
18 | 19 | ShowRepository, |
19 | 20 | ) |
| 21 | +from ..graphql.snippets.policy_collection import VIEW_OPTIONS |
| 22 | +from ..utils import wrap_command |
20 | 23 |
|
21 | 24 |
|
22 | 25 | @click.group(short_help="Run repository queries/mutations") |
23 | 26 | def repository(*args, **kwargs): |
24 | 27 | """Query against and Run mutations against Repository objects in Stacklet""" |
25 | 28 |
|
26 | 29 |
|
27 | | -def _add_pre_check(context: StackletContext, cli_args: dict[str, Any]) -> dict[str, Any]: |
| 30 | +def _read_ssh_private_key(cli_args: dict[str, Any]) -> None: |
| 31 | + """Replace a --ssh-private-key path with the key itself, in place.""" |
28 | 32 | private_key = cli_args.get("ssh_private_key") |
29 | | - auth_user = cli_args.get("auth_user") |
30 | | - if private_key: |
31 | | - if auth_user is None: |
32 | | - raise InvalidInputException("Both --auth-user and --ssh-private-key are required") |
33 | | - with open(os.path.expanduser(private_key), "r") as f: |
34 | | - cli_args["ssh_private_key"] = f.read().strip("\n") |
| 33 | + if not private_key: |
| 34 | + return |
| 35 | + if cli_args.get("auth_user") is None: |
| 36 | + raise InvalidInputException("Both --auth-user and --ssh-private-key are required") |
| 37 | + with open(os.path.expanduser(private_key), "r") as f: |
| 38 | + cli_args["ssh_private_key"] = f.read().strip("\n") |
| 39 | + |
35 | 40 |
|
| 41 | +def _add_pre_check(context: StackletContext, cli_args: dict[str, Any]) -> dict[str, Any]: |
| 42 | + _read_ssh_private_key(cli_args) |
36 | 43 | return cli_args |
37 | 44 |
|
38 | 45 |
|
| 46 | +# `register`'s arguments that belong to the repository config half. |
| 47 | +REGISTER_REPOSITORY_ARGS = ("url", "name", *AddRepository.optional) |
| 48 | + |
| 49 | +REGISTER_REQUIRED = { |
| 50 | + "url": AddRepository.required["url"], |
| 51 | + "name": AddRepository.required["name"], |
| 52 | + "provider": "Cloud provider for the policy collection", |
| 53 | +} |
| 54 | + |
| 55 | +REGISTER_OPTIONAL = { |
| 56 | + **AddRepository.optional, |
| 57 | + "collection_name": "Name for the policy collection. Defaults to the repository name", |
| 58 | + "collection_description": "Policy Collection Description", |
| 59 | + **VIEW_OPTIONS, |
| 60 | + "start_rev_spec": ( |
| 61 | + "Revision the first scan starts from. TAIL scans the whole history, recording a " |
| 62 | + "policy version per change -- a deep import. Only settable at creation time, so " |
| 63 | + "it has to be decided here" |
| 64 | + ), |
| 65 | + "deep_import": { |
| 66 | + "help": "Scan the repository's whole history. Shorthand for --start-rev-spec=TAIL", |
| 67 | + "is_flag": True, |
| 68 | + }, |
| 69 | +} |
| 70 | + |
| 71 | + |
| 72 | +def register(context: StackletContext, **cli_args): |
| 73 | + """ |
| 74 | + Register a policy repository and the dynamic policy collection that scans it. |
| 75 | +
|
| 76 | + Registering a repository takes two mutations. `addRepositoryConfig` records the |
| 77 | + repository's details but gives it no view, and policies are scanned per view -- so a |
| 78 | + bare repository config yields no policies at all. Creating a dynamic policy |
| 79 | + collection is what creates the view and triggers the first scan. This runs both, the |
| 80 | + way the console onboards a repository. |
| 81 | +
|
| 82 | + Use `repository add` and `policy-collection add` to drive either half on its own. |
| 83 | + """ |
| 84 | + if cli_args.pop("deep_import"): |
| 85 | + if cli_args["start_rev_spec"]: |
| 86 | + raise InvalidInputException("--deep-import and --start-rev-spec are exclusive") |
| 87 | + cli_args["start_rev_spec"] = "TAIL" |
| 88 | + |
| 89 | + _read_ssh_private_key(cli_args) |
| 90 | + |
| 91 | + repo_args = {name: cli_args.pop(name) for name in REGISTER_REPOSITORY_ARGS} |
| 92 | + repo_result = run_graphql(context, snippet_class=AddRepository, variables=repo_args, raw=True) |
| 93 | + |
| 94 | + added = (repo_result.get("data") or {}).get("addRepositoryConfig") or {} |
| 95 | + if problems := added.get("problems"): |
| 96 | + raise InvalidInputException( |
| 97 | + "could not add repository: %s" % "; ".join(p["message"] for p in problems) |
| 98 | + ) |
| 99 | + repo_config = added.get("repositoryConfig") |
| 100 | + if not repo_config: |
| 101 | + # Errors rather than problems: show the response instead of guessing at it. |
| 102 | + raise InvalidInputException("could not add repository: %s" % repo_result) |
| 103 | + |
| 104 | + repo_uuid = repo_config["uuid"] |
| 105 | + collection_args = dict(cli_args) |
| 106 | + collection_args["repository_uuid"] = repo_uuid |
| 107 | + collection_args["name"] = collection_args.pop("collection_name") or repo_args["name"] |
| 108 | + collection_args["description"] = collection_args.pop("collection_description") |
| 109 | + # A dynamic collection is always auto-updating; the platform rejects anything else. |
| 110 | + collection_args["auto_update"] = "true" |
| 111 | + |
| 112 | + collection_result = run_graphql( |
| 113 | + context, snippet_class=AddPolicyCollection, variables=collection_args, raw=True |
| 114 | + ) |
| 115 | + collection = ((collection_result.get("data") or {}).get("addPolicyCollection") or {}).get( |
| 116 | + "collection" |
| 117 | + ) |
| 118 | + if not collection: |
| 119 | + # The repository config exists now, and has no view until a dynamic collection |
| 120 | + # gives it one, so spell out the state rather than leaving it to be rediscovered. |
| 121 | + raise InvalidInputException( |
| 122 | + f"repository {repo_uuid} was added, but creating its policy collection " |
| 123 | + "failed, so nothing will be scanned. Either retry with `policy-collection " |
| 124 | + f"add --repository-uuid={repo_uuid}`, or remove the repository with " |
| 125 | + f"`repository remove --uuid={repo_uuid}`. Response: {collection_result}" |
| 126 | + ) |
| 127 | + |
| 128 | + fmt = context.formatter() |
| 129 | + click.echo(fmt({"repositoryConfig": repo_config, "policyCollection": collection})) |
| 130 | + |
| 131 | + |
| 132 | +register = wrap_command(register, REGISTER_OPTIONAL) |
| 133 | +register = wrap_command(register, REGISTER_REQUIRED, required=True) |
| 134 | +repository.add_command( |
| 135 | + click.command( |
| 136 | + "register", |
| 137 | + short_help="Register a repository and a dynamic policy collection to scan it", |
| 138 | + )(click.pass_obj(register)) |
| 139 | +) |
| 140 | + |
| 141 | + |
39 | 142 | register_graphql_commands( |
40 | 143 | repository, |
41 | 144 | [ |
|
0 commit comments