Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <organization>`, 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.
Expand Down
70 changes: 61 additions & 9 deletions scripts/project-resource-graph.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -356,21 +366,63 @@ resolve_identity() {
}

# --- Projects -----------------------------------------------------------------
# Prints "<project-id>\x1f<name>" for one project list call. The CLI answers with
# a bare array in some versions and with a paged object in others.
project_rows() { # <cli words...>
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 "<project-id>\x1f<name>" 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
}
Expand Down
Loading