PLAN-001: Backstage Metadata Enrichment and Catalog Generator
IMPLEMENTATION RULES: Before implementing this plan, read and follow:
- WORKFLOW.md - The implementation process
- PLANS.md - Plan structure and best practices
Status: Complete
Goal: Add SCRIPT_KIND, SCRIPT_TYPE, SCRIPT_OWNER metadata fields to all service definitions and build a catalog generator that produces Backstage-compatible YAML from them
Last Updated: 2026-03-12
Investigation: INVESTIGATE-backstage.md
Blocks: PLAN-002-backstage-deployment cannot load a catalog without this
Priority: Medium — no cluster needed, no risk, pure code
Overview
UIS service definitions (provision-host/uis/services/*/service-*.sh) are the single source of truth for service metadata. The docs website is already generated from them via uis-docs.sh. This plan extends that pattern to Backstage:
- Add three new metadata fields to all 29 service definitions
- Update documentation and schema for the new fields
- Build a generator script that produces Backstage catalog YAML
- Validate output against the draft catalog in the investigation
No cluster is needed. All work is local code and can be tested without deploying anything.
Reference patterns
uis-docs.sh+service-scanner.sh— existing generator pattern to follow- Draft catalog at
plans/backlog/catalog/— validation reference for generator output
Phase 1: Add Metadata Fields to Service Definitions — ✅ DONE
Add SCRIPT_KIND, SCRIPT_TYPE, and SCRIPT_OWNER to all 29 service definitions.
Tasks
-
1.1 Add fields to all DATABASES category services (6 scripts — these are
Resourcekind): ✓Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-postgresql.shResourcedatabaseplatform-teamservice-mysql.shResourcedatabaseplatform-teamservice-mongodb.shResourcedatabaseplatform-teamservice-elasticsearch.shResourcedatabaseplatform-teamservice-redis.shResourcecacheplatform-teamservice-qdrant.shResourcedatabaseplatform-team -
1.2 Add fields to INTEGRATION category services (3 scripts):
Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-rabbitmq.shResourcemessage-brokerplatform-teamservice-gravitee.shComponentserviceapp-teamservice-enonic.shComponentserviceapp-team -
1.3 Add fields to OBSERVABILITY category services (5 scripts):
Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-grafana.shComponentserviceplatform-teamservice-prometheus.shComponentserviceplatform-teamservice-loki.shComponentserviceplatform-teamservice-tempo.shComponentserviceplatform-teamservice-otel-collector.shComponentserviceplatform-team -
1.4 Add fields to AI category services (2 scripts):
Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-openwebui.shComponentserviceapp-teamservice-litellm.shComponentserviceapp-team -
1.5 Add fields to ANALYTICS category services (4 scripts):
Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-openmetadata.shComponentserviceapp-teamservice-unity-catalog.shComponentserviceapp-teamservice-jupyterhub.shComponentserviceapp-teamservice-spark.shComponentserviceapp-team -
1.6 Add fields to IDENTITY category services (1 script):
Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-authentik.shComponentserviceplatform-team -
1.7 Add fields to MANAGEMENT category services (5 scripts):
Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-argocd.shComponenttoolplatform-teamservice-pgadmin.shComponenttoolplatform-teamservice-redisinsight.shComponenttoolplatform-teamservice-whoami.shComponenttoolplatform-teamservice-nginx.shComponentserviceplatform-team -
1.8 Add fields to NETWORKING category services (2 scripts):
Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-cloudflare-tunnel.shComponentserviceplatform-teamservice-tailscale-tunnel.shComponentserviceplatform-team -
1.9 Add fields to APPLICATIONS category services (1 script):
Service SCRIPT_KINDSCRIPT_TYPESCRIPT_OWNERservice-nextcloud.shComponentserviceapp-teamNote:
service-nextcloud.shlives inservices/management/but hasSCRIPT_CATEGORY="APPLICATIONS". The category field is what matters for the catalog, not the directory.
Implementation Details
Add a new # === Extended Metadata (Optional) === section after # === Deployment Details (Optional) === in each script:
# === Extended Metadata (Optional) ===
SCRIPT_KIND="Component" # Component | Resource
SCRIPT_TYPE="service" # service | tool | library | database | cache | message-broker
SCRIPT_OWNER="platform-team" # platform-team | app-team
Validation
- All 29 service scripts have the three new fields ✓
-
./uis liststill works — new optional fields don't affect the scanner (verified: scanner only reads known fields line-by-line) ✓ -
./uis docs generatestill works — new fields are ignored by the JSON generator (it only reads its own set of SCRIPT_* variables) ✓
Phase 2: Update Documentation and Schema — ✅ DONE
Update the docs and JSON schema to reflect the new fields.
Tasks
- 2.1 Update
provision-host/uis/schemas/service.schema.json— addkind,type,ownerproperties ✓ - 2.2 Update
website/docs/contributors/guides/adding-a-service.md— add new fields to the service definition example (Step 2) and field reference table ✓ - 2.3 Update
website/docs/contributors/rules/kubernetes-deployment.md— add new fields to the service metadata reference section ✓ - 2.4 Update
website/docs/contributors/rules/naming-conventions.md— add naming conventions for allowed values (Component/Resource,service/tool/library/database/cache/message-broker,platform-team/app-team) ✓
Implementation Details
2.1 Schema update — add to service.schema.json:
"kind": {
"type": "string",
"description": "Whether this is a software component or infrastructure resource",
"enum": ["Component", "Resource"],
"default": "Component"
},
"type": {
"type": "string",
"description": "What kind of component or resource",
"enum": ["service", "tool", "library", "database", "cache", "message-broker"],
"default": "service"
},
"owner": {
"type": "string",
"description": "Which team owns this service",
"enum": ["platform-team", "app-team"],
"default": "platform-team"
}
These are NOT added to the required array — they are optional with sensible defaults.
Validation
- Schema includes
kind,type,owneras optional properties with enums and defaults ✓ - Adding-a-service guide shows Extended Metadata in example and field reference ✓
- Kubernetes deployment rules show Extended Metadata in groups table and example ✓
- Naming conventions document allowed values for all three fields ✓
Phase 3: Build the Catalog Generator — ✅ DONE
Create the script that generates Backstage catalog YAML from service definitions.
Tasks
- 3.1 Create
provision-host/uis/manage/uis-backstage-catalog.sh— the generator script ✓ - 3.2 Implement service scanning using single-pass
extract_all_metadata()(optimized — reads all fields in one pass per file) ✓ - 3.3 Implement component/resource YAML generation — one file per service in
components/orresources/✓ - 3.4 Implement static entity generation — Domain (
uis-infrastructure), Systems (one per category that has services — skip STORAGE which has none), Groups (platform-team,app-team,business-owners), Users ✓ - 3.5 Implement
all.yamlLocation entity generation — references all generated files ✓ - 3.6 Add Tika and OnlyOffice as hardcoded static component entries (bundled services without their own service definitions) ✓
- 3.7 Implement
dependsOnmapping — convertSCRIPT_REQUIRESto Backstage references (resource:postgresql,component:litellm, etc.) usingSCRIPT_KINDto determine prefix ✓ - 3.8 Add
--output-dirflag (default:generated/backstage/catalog/) ✓ - 3.9 Add
--dry-runflag to show what would be generated without writing files ✓ - 3.10 Wire into CLI: add
catalog generatesubcommand registered inuis-cli.sh✓
Implementation Details
Generator structure — follows the uis-docs.sh pattern:
#!/bin/bash
# uis-backstage-catalog.sh - Generate Backstage catalog YAML from service definitions
source "$LIB_DIR/logging.sh"
source "$LIB_DIR/categories.sh"
source "$LIB_DIR/service-scanner.sh"
Generated file structure:
generated/backstage/catalog/
├── all.yaml ← Location entity referencing everything
├── domains/
│ └── uis-infrastructure.yaml
├── systems/ ← 9 systems (one per category with services)
│ ├── ai.yaml
│ ├── analytics.yaml
│ ├── applications.yaml ← Nextcloud (SCRIPT_CATEGORY="APPLICATIONS")
│ ├── databases.yaml
│ ├── identity.yaml
│ ├── integration.yaml
│ ├── management.yaml
│ ├── networking.yaml
│ └── observability.yaml
├── components/
│ ├── openwebui.yaml
│ ├── grafana.yaml
│ └── ...
├── resources/
│ ├── postgresql.yaml
│ ├── redis.yaml
│ └── ...
└── groups/
├── platform-team.yaml
├── app-team.yaml
└── business-owners.yaml
Per-component YAML template (generated from service definition fields):
apiVersion: backstage.io/v1alpha1
kind: ${SCRIPT_KIND}
metadata:
name: ${SCRIPT_ID}
description: "${SCRIPT_DESCRIPTION}"
annotations:
backstage.io/kubernetes-id: ${SCRIPT_ID}
backstage.io/kubernetes-namespace: ${SCRIPT_NAMESPACE}
uis.sovereignsky.no/docs-url: "https://uis.sovereignsky.no${SCRIPT_DOCS}"
uis.sovereignsky.no/business-owner: "business-owners"
links:
- url: https://uis.sovereignsky.no${SCRIPT_DOCS}
title: "${SCRIPT_ID} Docs"
icon: docs
spec:
type: ${SCRIPT_TYPE}
lifecycle: production
owner: ${SCRIPT_OWNER}
system: ${SCRIPT_CATEGORY_LOWERCASE}
dependsOn:
# Generated from SCRIPT_REQUIRES
Default values when fields are missing:
SCRIPT_KIND:Component(exceptDATABASEScategory →Resource)SCRIPT_TYPE:serviceSCRIPT_OWNER:platform-teambackstage.io/kubernetes-id: defaults toSCRIPT_ID
Grafana annotations — since the Grafana plugin is required (PLAN-002), include Grafana dashboard annotations in generated entities where applicable. The generator can add a default grafana/dashboard-selector annotation based on SCRIPT_ID (e.g., "tag:postgresql"). Services without Grafana dashboards get the annotation but it simply shows no dashboards — no harm.
dependsOn mapping — for each ID in SCRIPT_REQUIRES, look up the required service's SCRIPT_KIND to determine prefix:
SCRIPT_KIND="Resource"→resource:postgresqlSCRIPT_KIND="Component"→component:litellm
Categories and Systems — there are 10 categories defined in categories.sh, but STORAGE has no services. The generator should only create System entities for categories that have at least one service (currently 9: OBSERVABILITY, AI, ANALYTICS, IDENTITY, DATABASES, MANAGEMENT, APPLICATIONS, NETWORKING, INTEGRATION).
Performance note — the existing get_service_value() function reads the file once per field. For the generator, which needs ~10 fields per service, consider reading all fields in a single pass (similar to how extract_script_metadata works, but extracting more fields). This is an implementation detail — get_service_value works correctly, just slower.
Validation
- Generator produces YAML for all 29 services + 2 static (Tika, OnlyOffice) — 31 total ✓
- Generated output matches the structure of the draft catalog in
plans/backlog/catalog/✓ -
all.yamlreferences all generated entity files ✓ - Domain, Systems, and Groups are generated correctly ✓
-
dependsOnreferences use correctresource:orcomponent:prefixes ✓ -
--dry-runshows output without writing files ✓ - Generated YAML is valid (no syntax errors) ✓
- Script is bash 3.2 compatible (macOS default bash — no associative arrays or
${var,,}) ✓
Phase 4: Validate and Clean Up — ✅ DONE
Compare generator output with draft catalog and finalize.
Tasks
- 4.1 Run generator and diff output against
plans/backlog/catalog/✓ - 4.2 Discrepancies identified and explained (see below) ✓
- 4.3 Added
generated/to.gitignore(generated files should not be version-controlled) ✓ - 4.4 Update
INVESTIGATE-backstage.md— note that PLAN-001 is complete ✓
Diff Results
Structural differences from draft catalog (all explained):
spark.yamlvsapache-spark.yaml— generator usesSCRIPT_ID="spark"(correct)enonic.yamlvsenonic-xp.yaml— generator usesSCRIPT_ID="enonic"(correct)otel-collector.yamlvsotlp-collector.yaml— generator usesSCRIPT_ID="otel-collector"(correct)- Draft has
sovdev-logger.yaml— no service definition exists (it's a library, not a deployed service) - Generator adds
grafana/dashboard-selectorannotation — enhancement from this plan - Minor description text differences — generator uses
SCRIPT_DESCRIPTION(source of truth)
Validation
- Generator output matches the draft catalog structure (differences are explained above) ✓
- User confirms the generated catalog is ready for use by PLAN-002 ✓
Acceptance Criteria
- All 29 service definitions have
SCRIPT_KIND,SCRIPT_TYPE,SCRIPT_OWNERfields ✓ - JSON schema (
service.schema.json) includes the new fields ✓ - Documentation updated (adding-a-service.md, kubernetes-deployment.md, naming-conventions.md) ✓
- Generator script exists and runs without errors ✓
- Generator produces valid Backstage catalog YAML for all services (31 entities: 29 + 2 static) ✓
- Static entities (Domain, Systems, Groups, Users, Tika, OnlyOffice) are generated ✓
-
dependsOnreferences use correctresource:/component:prefixes ✓ -
all.yamlLocation entity references all files ✓ - Existing CLI commands (
./uis list,./uis docs generate) still work — new optional fields are ignored by existing parsers ✓ - Generator can be invoked via
./uis catalog generate✓
Files to Create
| File | Purpose |
|---|---|
provision-host/uis/manage/uis-backstage-catalog.sh | Backstage catalog generator script |
generated/backstage/catalog/ | Output directory (generated files) |
Files to Modify
| File | Change |
|---|---|
29 service scripts in provision-host/uis/services/*/ | Add SCRIPT_KIND, SCRIPT_TYPE, SCRIPT_OWNER |
provision-host/uis/schemas/service.schema.json | Add kind, type, owner properties |
website/docs/contributors/guides/adding-a-service.md | Add new fields to Step 2 example and reference table |
website/docs/contributors/rules/kubernetes-deployment.md | Add new fields to metadata reference |
website/docs/contributors/rules/naming-conventions.md | Add allowed values for new fields |
provision-host/uis/manage/uis-docs.sh or uis-cli.sh | Wire in the new generator command |