diff --git a/scripts/README.md b/scripts/README.md index 8e65b97..7dd3686 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -295,12 +295,20 @@ The notes printed with the delete plan follow the STACKIT documentation as opene | `opensearch`, `rabbitmq`, `logme` | Deletion "includes deletion of all settings and configurations made for this instance, all data stored in the service (if any) and all corresponding backups in the STACKIT cloud". | [OpenSearch](https://docs.stackit.cloud/products/databases/opensearch/how-tos/delete-an-opensearch-service/), [RabbitMQ](https://docs.stackit.cloud/products/messaging/rabbitmq/how-tos/create-and-manage-services/), [LogMe](https://docs.stackit.cloud/products/logging-and-monitoring/logme/how-tos/create-and-manage-logme-services/) | | `mariadb`, `redis` | Deletion "cannot be undone"; backups "are stored for 14 days". The documentation does not say whether backups outlive the instance. | [MariaDB instances](https://docs.stackit.cloud/products/databases/mariadb/how-tos/create-and-manage-instances/), [MariaDB architecture](https://docs.stackit.cloud/products/databases/mariadb/basics/architecture/), [Redis instances](https://docs.stackit.cloud/products/databases/redis/how-tos/create-and-manage-instances-for-redis/), [Redis architecture](https://docs.stackit.cloud/products/databases/redis/basics/architecture-of-redis/) | +### Which projects a run covers + +The project belongs to the identity, so `--key` changes it. Without `--key` the script takes the project from the CLI configuration of the active profile. With `--key` it does not: the key is another subject, often in another organization, and it would get 403 for that project. Such a run needs `--project-id` or `--all-projects` and says so when it gets neither. The region is inherited in both cases, because every subject uses the same regions. + +`--all-projects` asks twice. `stackit project list` returns the projects the subject is a member of. A role on an organization creates no membership, so a service account that holds one gets an empty list there; its projects appear under `stackit project list --parent-id `, which the script calls for every organization of `stackit organization list`. The union of both, each project once, is what the run covers. + +One gap remains: the API returns the projects that are children of the container it is asked for ([`GET /v2/projects`](https://github.com/stackitcloud/stackit-api-specifications/blob/main/services/resource-manager/v0/resource-manager.json)), and the CLI 0.72.0 has no command that lists folders. A project inside a folder is therefore covered through the membership list, not through its organization. + ### Flags - `--key NAME|PATH|EMAIL` — service account key from `$STACKIT_KEY_DIR` (default `~/.stackit/keys`), activated in its own CLI profile; `--key ?` opens a menu. Without it the logged-in session is used. - `--profile NAME` — CLI profile for the key (default: `resource-graph`). -- `--project-id ID` — project to inspect, may be repeated. Default: the project from the CLI configuration. -- `--all-projects` — every project the identity is a member of. +- `--project-id ID` — project to inspect, may be repeated. Default: the project from the CLI configuration, which is read only when no `--key` is given. +- `--all-projects` — every project the identity reaches: the projects it is a member of plus the projects under every organization it can read. - `--region R` — default: the region from the CLI configuration. - `--services a,b,c` — query only these services; `--list-services` prints all of them. - `--delete KIND/NAME` — delete an object, may be repeated; `KIND/ID` works too. Needs exactly one project and a terminal. Deleting a `service-account` needs `python3` to read the script's own identity from the access token, or a `--key` file with an issuer email. diff --git a/scripts/project-resource-graph.sh b/scripts/project-resource-graph.sh index 35d63bf..1496e69 100755 --- a/scripts/project-resource-graph.sh +++ b/scripts/project-resource-graph.sh @@ -122,8 +122,11 @@ Options: --profile NAME CLI profile for the key. Default: $STACKIT_RESOURCE_GRAPH_PROFILE or resource-graph. --project-id ID Project, may be repeated. Without it the project - from the CLI configuration. - --all-projects All projects the identity is a member of. + from the CLI configuration, which is read only + when no --key is given. + --all-projects Every project the identity reaches: its own + memberships plus the projects under every + organization it can read. --region R Default: region from the CLI configuration. --services a,b,c Query only these services. --list-services Show queryable services and exit. @@ -235,13 +238,20 @@ require_tools() { # The region must be read from the active profile before switching to another # profile; a profile created with --empty has none. Both values are optional: a # failure here must not end the script as long as --region and --project-id are set. +# +# The project is treated differently than the region: every subject uses the same +# regions, but a project belongs to one subject. The project of the logged-in +# session is therefore not read when --key selects another subject, which often +# sits in another organization and gets 403 for that project. load_cli_defaults() { local cli_config cli_config="$(stackit config list -o json 2>/dev/null || true)" if [[ -z "$REGION" ]]; then REGION="$(printf '%s' "$cli_config" | jq -r '.region // empty' 2>/dev/null || true)" fi - DEFAULT_PROJECT="$(printf '%s' "$cli_config" | jq -r '.project_id // empty' 2>/dev/null || true)" + if [[ -z "$KEY_SELECTOR" ]]; then + DEFAULT_PROJECT="$(printf '%s' "$cli_config" | jq -r '.project_id // empty' 2>/dev/null || true)" + fi } create_workspace() { @@ -356,21 +366,63 @@ resolve_identity() { } # --- Projects ----------------------------------------------------------------- +# Prints "\x1f" for one project list call. The CLI answers with +# a bare array in some versions and with a paged object in others. +project_rows() { # + stackit_cli "$@" -o json 2>/dev/null \ + | jq -r '(if type=="array" then . else (.items // []) end)[] + | [(.projectId // ""), (.name // "")] | join("\u001f")' +} + +# Prints the organization IDs the subject can read. +organization_ids() { + stackit_cli organization list -o json 2>/dev/null \ + | jq -r '(if type=="array" then . else (.items // []) end)[] + | .organizationId // empty' +} + +# Prints "\x1f" for every project the subject reaches, each ID +# once. Two sources are needed. "project list" without a filter returns the +# projects the subject is a member of; a role on an organization creates no such +# membership, so a service account with an organization role gets an empty list +# there and its projects only appear under --parent-id. Projects inside a folder +# are missing from the second source: the API returns the children of the +# container that is asked for, and the CLI 0.72.0 has no command that lists +# folders. +# || true: one source that fails must not swallow the other. +readable_projects() { + local org + { + project_rows project list || true + while read -r org; do + [[ -n "$org" ]] || continue + project_rows project list --parent-id "$org" || true + done < <(organization_ids) + } | awk -F $'\037' '$1 != "" && !seen[$1]++' +} + +# Prints why no project was found. Without --key the CLI configuration is the +# missing piece; with --key it is deliberately not read. +no_project_message() { + if [[ -n "$KEY_SELECTOR" ]]; then + printf '%s' "no project. The project of the CLI configuration belongs to the logged-in session and is not used with --key. Set --project-id or use --all-projects." + else + printf '%s' "no project. Set --project-id, use --all-projects or run 'stackit config set project-id ...'." + fi +} + resolve_projects() { local id name : > "$WORK/project-names" if [[ $ALL_PROJECTS -eq 1 ]]; then while IFS=$'\037' read -r id name; do - [[ -n "${id:-}" ]] || continue PROJECTS+=("$id") printf '%s\037%s\n' "$id" "$name" >> "$WORK/project-names" - done < <(stackit_cli project list -o json 2>/dev/null \ - | jq -r '(if type=="array" then . else (.items // []) end)[] - | [(.projectId // ""), (.name // "")] | join("\u001f")') + done < <(readable_projects) [[ ${#PROJECTS[@]} -gt 0 ]] || die "no readable projects" + info "${#PROJECTS[@]} projects: memberships and the projects under every readable organization" elif [[ ${#PROJECTS[@]} -eq 0 ]]; then - [[ -n "$DEFAULT_PROJECT" ]] \ - || die "no project. Set --project-id, use --all-projects or run 'stackit config set project-id ...'." + [[ -n "$DEFAULT_PROJECT" ]] || die "$(no_project_message)" PROJECTS+=("$DEFAULT_PROJECT") fi }