Skip to content

Instantly share code, notes, and snippets.

@berinle
Created August 3, 2026 19:31
Show Gist options
  • Select an option

  • Save berinle/3a9386fb67d51a12f4eabe5743a2abe8 to your computer and use it in GitHub Desktop.

Select an option

Save berinle/3a9386fb67d51a12f4eabe5743a2abe8 to your computer and use it in GitHub Desktop.
list all apps and their service bindings
#!/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