Created
August 3, 2026 19:31
-
-
Save berinle/3a9386fb67d51a12f4eabe5743a2abe8 to your computer and use it in GitHub Desktop.
list all apps and their service bindings
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Report every app and the service instances bound to it. | |
| # Usage: ./cf-app-services.sh [table|csv|json] (default: table) | |
| set -euo pipefail | |
| FORMAT="${1:-table}" | |
| case "$FORMAT" in | |
| -h|--help|help) | |
| cat <<'USAGE' | |
| cf-app-services - report every app and the service instances bound to it | |
| Usage: cf-app-services [table|csv|json] | |
| table aligned summary, one row per app (default) | |
| csv one row per app/service pair; apps with no services get a single | |
| row with empty service columns | |
| json full structure, apps with a nested `services` array | |
| Covers every org and space your login can see, regardless of current target. | |
| Includes apps with no bindings. Counts app bindings only - service keys and | |
| route service bindings (/v3/service_route_bindings) are not included. | |
| USAGE | |
| exit 0 ;; | |
| esac | |
| command -v cf >/dev/null || { echo "cf CLI not found on PATH" >&2; exit 127; } | |
| command -v jq >/dev/null || { echo "jq not found on PATH" >&2; exit 127; } | |
| cf target >/dev/null 2>&1 || { echo "Not logged in. Run 'cf login' first." >&2; exit 1; } | |
| # Follow v3 pagination, merging both `resources` and `included` across pages. | |
| fetch_all() { | |
| local path="$1" sep='?' url body pages='[]' | |
| case "$path" in *\?*) sep='&';; esac | |
| url="${path}${sep}per_page=5000" | |
| while [ -n "$url" ] && [ "$url" != "null" ]; do | |
| body=$(cf curl "$url") | |
| if jq -e '.errors' >/dev/null 2>&1 <<<"$body"; then | |
| echo "API error on ${url}:" >&2 | |
| jq -r '.errors[] | " \(.title): \(.detail)"' >&2 <<<"$body" | |
| exit 1 | |
| fi | |
| pages=$(jq -c --argjson p "$body" '. + [$p]' <<<"$pages") | |
| url=$(jq -r '.pagination.next.href // empty' <<<"$body" | sed -E 's#^https?://[^/]+##') | |
| done | |
| jq -c '{ | |
| resources: (map(.resources) | add // []), | |
| included: (reduce (map(.included // {}) | .[]) as $i ({}; | |
| reduce ($i | keys_unsorted[]) as $k (.; .[$k] = ((.[$k] // []) + $i[$k])))) | |
| }' <<<"$pages" | |
| } | |
| APPS=$(fetch_all "/v3/apps?include=space.organization") | |
| BINDINGS=$(fetch_all "/v3/service_credential_bindings?type=app") | |
| INSTANCES=$(fetch_all "/v3/service_instances") | |
| PLANS=$(fetch_all "/v3/service_plans?include=service_offering") | |
| REPORT=$(jq -n \ | |
| --argjson apps "$APPS" \ | |
| --argjson bindings "$BINDINGS" \ | |
| --argjson instances "$INSTANCES" \ | |
| --argjson plans "$PLANS" ' | |
| def index_by_guid: reduce .[] as $x ({}; .[$x.guid] = $x); | |
| ($apps.included.spaces // [] | index_by_guid) as $spaces | | |
| ($apps.included.organizations // [] | index_by_guid) as $orgs | | |
| ($plans.included.service_offerings // [] | index_by_guid) as $offerings | | |
| ($plans.resources | index_by_guid) as $plansById | | |
| ($instances.resources | index_by_guid) as $siById | | |
| # app guid -> [ bound service objects ] | |
| ($bindings.resources | |
| | map(. as $b | |
| | ($siById[$b.relationships.service_instance.data.guid] // null) as $si | |
| | ($plansById[$si.relationships.service_plan.data.guid // ""] // null) as $plan | |
| | { | |
| app_guid: $b.relationships.app.data.guid, | |
| service: ($si.name // "<inaccessible:\($b.relationships.service_instance.data.guid)>"), | |
| service_guid: $b.relationships.service_instance.data.guid, | |
| type: ($si.type // "unknown"), | |
| offering: ($offerings[$plan.relationships.service_offering.data.guid // ""].name // "-"), | |
| plan: ($plan.name // "-"), | |
| binding_name: $b.name, | |
| binding_state: ($b.last_operation.state // "-") | |
| }) | |
| | group_by(.app_guid) | |
| | map({ key: .[0].app_guid, value: (sort_by(.service) | map(del(.app_guid))) }) | |
| | from_entries) as $boundByApp | | |
| $apps.resources | |
| | map( | |
| ($spaces[.relationships.space.data.guid] // null) as $space | | |
| { | |
| org: ($orgs[$space.relationships.organization.data.guid // ""].name // "-"), | |
| space: ($space.name // "-"), | |
| app: .name, | |
| app_guid: .guid, | |
| state: .state, | |
| services: ($boundByApp[.guid] // []) | |
| }) | |
| | sort_by(.org, .space, .app) | |
| ') | |
| case "$FORMAT" in | |
| json) jq . <<<"$REPORT" ;; | |
| csv) | |
| jq -r ' | |
| ["org","space","app","state","service","service_type","offering","plan","binding_state"], | |
| (.[] | . as $a | | |
| if (.services | length) == 0 | |
| then [$a.org, $a.space, $a.app, $a.state, "", "", "", "", ""] | |
| else .services[] as $s | | |
| [$a.org, $a.space, $a.app, $a.state, $s.service, $s.type, $s.offering, $s.plan, $s.binding_state] | |
| end) | |
| | @csv' <<<"$REPORT" ;; | |
| table) | |
| jq -r ' | |
| def pad($n): . + (" " * ($n - (. | length))); | |
| ([["ORG","SPACE","APP","STATE","SERVICES"]] + | |
| (map([.org, .space, .app, .state, | |
| (if (.services|length) == 0 then "(none)" | |
| else (.services | map("\(.service) [\(if .type == "user-provided" then "user-provided" else "\(.offering)/\(.plan)" end)]") | join(", ")) | |
| end)]))) as $rows | |
| | ($rows | map(map(length)) | transpose | map(max)) as $w | |
| | $rows[] | [range(0; length) as $i | (.[$i] | pad($w[$i]))] | join(" ") | sub(" +$"; "") | |
| ' <<<"$REPORT" | |
| echo | |
| jq -r ' | |
| "Apps: \(length) with services: \([.[] | select((.services|length) > 0)] | length) without: \([.[] | select((.services|length) == 0)] | length) bindings: \([.[].services[]] | length)" | |
| ' <<<"$REPORT" ;; | |
| *) echo "Unknown format: $FORMAT (use table|csv|json)" >&2; exit 2 ;; | |
| esac |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment