ЁЯПл The SchoolтА║ЁЯЫдя╕П Platform EngineeringтА║ЁЯУЗ рдзрдбрд╛ 04 тАФ Service catalog: рдкреНрд░рддреНрдпреЗрдХ рд╡рд░реНрдЧрдЦреЛрд▓реАрдЪреА рдиреЛрдВрджрд╡рд╣реА
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУЗ рдзрдбрд╛ 04 тАФ Service catalog: рдкреНрд░рддреНрдпреЗрдХ рд╡рд░реНрдЧрдЦреЛрд▓реАрдЪреА рдиреЛрдВрджрд╡рд╣реА

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 04 ┬╖ рдорд╛рдЧреЗ: lesson-03-golden-paths ┬╖ рдкреБрдвреЗ: lesson-05-self-service-infra


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ03, рдЖрдгрд┐ service catalog: рдкреНрд░рддреНрдпреЗрдХ service рдЖрдгрд┐ resource рдЪреА рдПрдХ рдиреЛрдВрджрд╡рд╣реА, рддрд┐рдЪрд╛ рдорд╛рд▓рдХ рдХреЛрдг, рдЖрдгрд┐ рддреА рдХрд╢рд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЖрд╣реЗ. рддреНрдпрд╛рд╡рд░реВрди рдкрд╣рд╛рдЯреЗ 3 рд╡рд╛рдЬрддрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреНрдпрд╛ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рдВрдЪреА рдЙрддреНрддрд░реЗ рдорд┐рд│рддрд╛рдд: рдореА рдХреЛрдгрд╛рд▓рд╛ рдлреЛрди рдХрд░реВ, рд╣реЗ рдмрдВрдж рдкрдбрд▓реЗ рддрд░ рдЖрдгрдЦреА рдХрд╛рдп рдмрдВрдж рдкрдбрддреЗ, рдЖрдгрд┐ рдХреЛрдгрддреНрдпрд╛ services рдЪрд╛ рдЖрддрд╛ рдХреЛрдгреАрдЪ рдорд╛рд▓рдХ рдирд╛рд╣реА. idp/office.py рдордзрд▓реЗ Catalog, idp/demo.py рдордзрд▓реЗ catalog() рдЖрдгрд┐ school_catalog().

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рд╢рд╛рд│реЗрдЪреЗ рдХрд╛рд░реНрдпрд╛рд▓рдп рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реАрдЪреА рдПрдХ рдиреЛрдВрджрд╡рд╣реА ЁЯУЗ рдареЗрд╡рддреЗ: "рдЦреЛрд▓реА 12 тАФ рд╡рд┐рдЬреНрдЮрд╛рди рдкреНрд░рдпреЛрдЧрд╢рд╛рд│рд╛ тАФ рдорд╛рд▓рдХ рд╡рд┐рдЬреНрдЮрд╛рди рд╡рд┐рднрд╛рдЧ тАФ рдЧреЕрд╕рдЪреА рд▓рд╛рдЗрди рдЖрдгрд┐ рдЖрдкрддреНрдХрд╛рд▓реАрди рджрд╛рд░ рд▓рд╛рдЧрддреЗ."

рдПрдХрд╛ рд╕рдХрд╛рд│реА рдЧреЕрд╕рдЪреА рд▓рд╛рдЗрди рдмрд┐рдШрдбрддреЗ. рдХреЛрдгрд╛рд╡рд░ рдкрд░рд┐рдгрд╛рдо рд╣реЛрддреЛ? рджреАрдкрд┐рдХрд╛ рдиреЛрдВрджрд╡рд╣реА рдЙрдШрдбрддреЗ рдЖрдгрд┐ "рд▓рд╛рдЧрддреЗ" рдЪреЗ рдмрд╛рдг рдЙрд▓рдЯреЗ рдлрд┐рд░рддреЗ: рд╡рд┐рдЬреНрдЮрд╛рди рдкреНрд░рдпреЛрдЧрд╢рд╛рд│реЗрд▓рд╛ рдЧреЕрд╕рдЪреА рд▓рд╛рдЗрди рд▓рд╛рдЧрддреЗ, рдЖрдгрд┐ рдкрд╛рд▓рдХрд╛рдВрдЪреНрдпрд╛ open day рд▓рд╛ рд╡рд┐рдЬреНрдЮрд╛рди рдкреНрд░рдпреЛрдЧрд╢рд╛рд│рд╛ рд▓рд╛рдЧрддреЗ. рдореНрд╣рдгрдЬреЗ рдПрдХ рдирд╛рд╣реА, рддрд░ рджреЛрди рдЧреЛрд╖реНрдЯреА рдЕрдбрдЪрдгреАрдд рдЖрд╣реЗрдд.

рдордЧ рддрд┐рд▓рд╛ рджреЛрди рд╡рд┐рдЪрд┐рддреНрд░ рдиреЛрдВрджреА рд╕рд╛рдкрдбрддрд╛рдд. canteen orders рдЦреЛрд▓реАрдЪрд╛ рдорд╛рд▓рдХ "2019 hackathon team" рдЕрд╕реЗ рд▓рд┐рд╣рд┐рд▓реЗ рдЖрд╣реЗ. рддреА team рд╡рд░реНрд╖рд╛рдВрдкреВрд░реНрд╡реАрдЪ рд╡рд┐рдЦреБрд░рд▓реА. fees рдЦреЛрд▓реАрд▓рд╛ рдорд╛рд▓рдХрдЪ рдирд╛рд╣реА. рдпрд╛ рдЕрдирд╛рде рдЦреЛрд▓реНрдпрд╛ рдЖрд╣реЗрдд: рдЕрдЬреВрдирд╣реА рд╡рд╛рдкрд░рд╛рдд рдЕрд╕рд▓реЗрд▓реНрдпрд╛ рдкрдг рдХреЛрдгреАрдЪ рди рд╕рд╛рдВрднрд╛рд│рдгрд╛рд▒реНрдпрд╛. рддрд┐рдереЗ рдХрд╛рд╣реА рдмрд┐рдШрдбрд▓реЗ рддрд░ рдХреЛрдгрд╛рд▓рд╛рдЪ рдлреЛрди рдЬрд╛рдд рдирд╛рд╣реА.

рдЖрдгрд┐ fees рдЦреЛрд▓реА рдореНрд╣рдгрддреЗ рдХреА рддрд┐рд▓рд╛ "payments office" рд▓рд╛рдЧрддреЗ тАФ рдкрдг рдиреЛрдВрджрд╡рд╣реАрдд payments office рдирд╛рд╡рд╛рдЪреЗ рдХрд╛рд╣реАрдЪ рдирд╛рд╣реА. рд╣рд╛ рдПрдХ рддреБрдЯрд▓реЗрд▓рд╛ рд╕рдВрджрд░реНрдн рдЖрд╣реЗ: рдПрдХрддрд░ рддреЗ рд╣рд░рд╡рд▓реЗ рдЖрд╣реЗ, рдХрд┐рдВрд╡рд╛ рдЯреАрдк рдЪреБрдХреАрдЪреА рдЖрд╣реЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    pp["parent-portal<br/>maths"] --> er["exam-results<br/>science"]
    pp --> tt["timetable<br/>maths"]
    er --> db[("results-db<br/>science")]
    er --> auth["auth<br/>facilities-office"]
    tt --> auth
    co["canteen-orders<br/>тЪая╕П team-2019-hackathon"] --> auth
    fe["fees<br/>тЪая╕П no owner"] --> auth
    fe -.-> pay["тЭУ payments-api<br/>not in the catalog"]
    ls["library-search<br/>library"] -.-> si["тЭУ search-index<br/>not in the catalog"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/platform-engineering/lesson-diagrams.html#l04

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдореЛрдареНрдпрд╛ рд╕рдВрд╕реНрдереЗрдд incident рдЪреНрдпрд╛ рд╡реЗрд│реА рд╕рд░реНрд╡рд╛рдд рдорд╣рд╛рдЧрдбрд╛ рдкреНрд░рд╢реНрди рдЕрд╕рддреЛ "рд╣реЗ рдХреЛрдгрд╛рдЪреЗ рдЖрд╣реЗ?". Catalog рдирд╕реЗрд▓ рддрд░ рдЙрддреНрддрд░ chat рдЪреНрдпрд╛ рдЗрддрд┐рд╣рд╛рд╕рд╛рдд рдЖрдгрд┐ рд▓реЛрдХрд╛рдВрдЪреНрдпрд╛ рдбреЛрдХреНрдпрд╛рдд рд░рд╛рд╣рддреЗ. Catalog рдЕрд╕реЗрд▓ рддрд░ рддреЗ рдлрдХреНрдд рд╢реЛрдзрдгреЗ рдЕрд╕рддреЗ. рдпрд╛ рдХреЛрд░реНрд╕рдордзрд▓реНрдпрд╛ рдкреБрдврдЪреНрдпрд╛ рдЬрд╡рд│рдЬрд╡рд│ рд╕рдЧрд│реНрдпрд╛ рдЧреЛрд╖реНрдЯреАрдВрдЪрд╛ рдкрд╛рдпрд╛ рд╕реБрджреНрдзрд╛ catalog рдЖрд╣реЗ: scorecards (рдзрдбрд╛ 10) catalog рдиреЛрдВрджреАрдВрдирд╛ рдЧреБрдг рджреЗрддрд╛рдд, portal (рдзрдбрд╛ 12) рддреНрдпрд╛рд╡рд░ рдмрд╛рдВрдзрд▓реЗ рдЬрд╛рддреЗ, рдЖрдгрд┐ policies (рдзрдбрд╛ 09) рдкреНрд░рддреНрдпреЗрдХ service рд▓рд╛ owner рдЕрд╕рдгреЗ рдмрдВрдзрдирдХрд╛рд░рдХ рдХрд░реВ рд╢рдХрддрд╛рдд.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

idp/office.py рдордзрд▓реЗ Catalog(groups) catalog-info.yaml рд╕рд╛рд░рдЦреНрдпрд╛ рдЖрдХрд╛рд░рд╛рдЪреНрдпрд╛ рдиреЛрдВрджреА рдареЗрд╡рддреЗ: kind, name, owner, lifecycle, depends_on. owned_by(group) рдПрдХрд╛ group рдЪреНрдпрд╛ рдиреЛрдВрджреАрдВрдЪреА рдпрд╛рджреА рджреЗрддреЗ; orphans() рдЬреНрдпрд╛рдВрдЪрд╛ owner рдирд╛рд╣реА рдХрд┐рдВрд╡рд╛ рдорд╛рд╣реАрдд рдЕрд╕рд▓реЗрд▓рд╛ group рдирд╛рд╣реА рдЕрд╢рд╛ рдиреЛрдВрджреА рд╢реЛрдзрддреЗ; broken_refs() catalog рдордзреНрдпреЗ рдирд╕рд▓реЗрд▓реА depends_on рдирд╛рд╡реЗ рд╢реЛрдзрддреЗ; рдЖрдгрд┐ dependents(name) dependency рдЪреЗ рдмрд╛рдг рдЙрд▓рдЯреЗ, рд╕рд╛рдЦрд│реНрдпрд╛ рдкрдХрдбрдд рдлрд┐рд░рддреЗ рдЖрдгрд┐ blast radius рджреЗрддреЗ. idp/demo.py рдордзрд▓реЗ school_catalog() рд╢рд╛рд│реЗрдЪреНрдпрд╛ 8 рдиреЛрдВрджреА рдмрдирд╡рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 idp/demo.py catalog
python3 - <<'EOF'
import sys; sys.path.insert(0, "idp"); from demo import school_catalog
c = school_catalog()
print("if timetable breaks:", c.dependents("timetable"))
print("if exam-results breaks:", c.dependents("exam-results"))
c.register("Component", "canteen-orders", "facilities-office", lifecycle="experimental", depends_on=["auth"])   # adopted
c.register("Resource", "search-index", "library")                                                                # declared
print("orphans now:", c.orphans(), "┬╖ broken references now:", c.broken_refs())
print("library owns:", c.owned_by("library"))
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

catalog рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА the catalog holds 8 entries owned by groups ['science', 'maths', 'library', 'facilities-office']
   science            owns ['exam-results', 'results-db']
   orphans (no owner, or the owner group is gone): ['canteen-orders', 'fees']
   broken references: [('fees', 'payments-api'), ('library-search', 'search-index')]
тФАтФА blast radius тАФ if auth breaks, these depend on it: ['canteen-orders', 'exam-results', 'fees', 'parent-portal', 'timetable']
   if results-db breaks: ['exam-results', 'parent-portal']

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

if timetable breaks: ['parent-portal']
if exam-results breaks: ['parent-portal']
orphans now: ['fees'] ┬╖ broken references now: [('fees', 'payments-api')]
library owns: ['library-search', 'search-index']

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

results-db рдЪрд╛ рдереЗрдЯ user рдПрдХрдЪ рдЖрд╣реЗ, exam-results, рдкрдг рддрд┐рдЪрд╛ blast radius рджреЛрди рдЖрд╣реЗ: parent-portal рд╣реЗ exam-results рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЖрд╣реЗ, рдореНрд╣рдгреВрди рддреНрдпрд╛рд▓рд╛рд╣реА рдлрдЯрдХрд╛ рдмрд╕рддреЛ. рд╕рд╛рдЦрд│реА рдлрд┐рд░рдгреЗ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ. auth рд╣рд╛ рд╢рд╛рд│реЗрддрд▓рд╛ рд╕рд░реНрд╡рд╛рдд рдЬрд╛рд╕реНрдд рд╡рд╛рдЯреВрди рдШреЗрддрд▓реЗрд▓рд╛ рднрд╛рдЧ рдЖрд╣реЗ тАФ 5 рдиреЛрдВрджреА рддреНрдпрд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдЖрд╣реЗрдд тАФ рдЖрдгрд┐ рдореНрд╣рдгреВрдирдЪ рддреНрдпрд╛рдЪрд╛ рдорд╛рд▓рдХ рд╕реБрд╡рд┐рдзрд╛ рдХрд╛рд░реНрдпрд╛рд▓рдп рдЖрд╣реЗ. рдЖрдгрд┐ рдиреЛрдВрджрд╡рд╣реА рдЬрд╛рд╣реАрд░ рдХрд░реВрди рдирд╛рд╣реА, рддрд░ рдХрд░реВрди рд╕реБрдзрд╛рд░рддреЗ: рдПрдХрд╛ adoption рдиреЗ canteen рдЪреЗ рдЕрдирд╛рдердкрдг рджреВрд░ рдХреЗрд▓реЗ, рдПрдХрд╛ рдЬрд╛рд╣реАрд░ рдХреЗрд▓реЗрд▓реНрдпрд╛ resource рдиреЗ рдПрдХ рддреБрдЯрд▓реЗрд▓рд╛ рд╕рдВрджрд░реНрдн рджреБрд░реБрд╕реНрдд рдХреЗрд▓рд╛. fees рдЕрдЬреВрдирд╣реА рддреБрдЯрд▓реЗрд▓реНрдпрд╛ рд╕рдВрджрд░реНрднрд╛рд╕рд╣ рдЕрдирд╛рде рдЖрд╣реЗ тАФ рд╣реЗрдЪ рдкреБрдврдЪреЗ рдХрд╛рдо.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

exam-results рд╕рд╛рдареА рдЦрд░реА Backstage catalog-info.yaml, рддрд┐рдЪреНрдпрд╛ repo рдЪреНрдпрд╛ root рд╡рд░ рдареЗрд╡рд▓реЗрд▓реА:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: exam-results
  description: Publishes exam results to students and parents
  annotations:
    github.com/project-slug: school/exam-results
spec:
  type: service
  lifecycle: production
  owner: group:science
  dependsOn:
    - resource:results-db
    - component:auth

Backstage рдЕрд╢рд╛ files рддреНрдпрд╛рдЪреНрдпрд╛ app-config.yaml рдордзрд▓реНрдпрд╛ catalog.locations рдордзреВрди, рдХрд┐рдВрд╡рд╛ рд╕рдВрд╕реНрдереЗрдЪреНрдпрд╛ repositories scan рдХрд░рдгрд╛рд▒реНрдпрд╛ discovery provider рдордзреВрди рд╢реЛрдзрддреЛ. рдЦрд▒реНрдпрд╛ instance рд╡рд░, catalog REST API рдорд╛рд▓рдХреАрдЪреНрдпрд╛ рдкреНрд░рд╢реНрдирд╛рдВрдЪреА рдереЗрдЯ рдЙрддреНрддрд░реЗ рджреЗрддреЛ:

curl -s "https://backstage.school.example/api/catalog/entities/by-query?filter=spec.owner=group:default/science" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.items[].metadata.name'

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: incident рдЪреНрдпрд╛ рд╡реЗрд│реА рдпреЛрдЧреНрдп рд▓реЛрдХрд╛рдВрдкрд░реНрдпрдВрдд рдкреЛрд╣реЛрдЪрдгреНрдпрд╛рдЪрд╛ рд╕рд░реНрд╡рд╛рдд рдЬрд▓рдж рдорд╛рд░реНрдЧ рдореНрд╣рдгрдЬреЗ catalog. "рдЕрд╕реНрддрд┐рддреНрд╡рд╛рдд рдЕрд╕рд▓реЗрд▓рд╛ owner рдЖрд╣реЗ" рд╣рд╛ рдПрдХ рдирд┐рдпрдо (рдзрдбрд╛ 09) рдЖрдгрд┐ рдПрдХ scorecard check (рдзрдбрд╛ 10) рдмрдирд╡рд╛, рдлрдХреНрдд рдЖрд╢рд╛ рдирд╛рд╣реА.

тПня╕П рдкреБрдвреЗ

рднрд╛рдЧ 1 рдкреВрд░реНрдг рдЭрд╛рд▓рд╛: рддреБрдореНрд╣рд╛рд▓рд╛ рдорд╛рд╣реАрдд рдЖрд╣реЗ рдХрд╛, рдЖрдзреА рдХрд╛рдп рдмрдирд╡рд╛рдпрдЪреЗ, service рдХрд╢реА рд╕реБрд░реВ рдХрд░рд╛рдпрдЪреА рдЖрдгрд┐ рддрд┐рдЪрд╛ owner рдХрд╕рд╛ рд╢реЛрдзрд╛рдпрдЪрд╛. рднрд╛рдЧ 2 рдзрдбрд╛ 02 рдордзрд▓реНрдпрд╛ рд╕рд░реНрд╡рд╛рдд рдореЛрдареНрдпрд╛ рддреНрд░рд╛рд╕рд╛рдкрд╛рд╕реВрди рд╕реБрд░реВ рд╣реЛрддреЛ: ticket рд╢рд┐рд╡рд╛рдп database рдорд┐рд│рд╡рдгреЗ.

git checkout lesson-05-self-service-infra

ЁЯУЗ Lesson 04 тАФ The service catalog: the register of every classroom

ЁЯУН You are here: Lesson 04 of 12 ┬╖ Previous: lesson-03-golden-paths ┬╖ Next: lesson-05-self-service-infra


ЁЯУж What's in this branch

Lessons 01тАУ03, plus the service catalog: one register of every service and resource, who owns it, and what it depends on. From that you can answer the questions that matter at 3 a.m.: who do I call, what else breaks if this breaks, and which services nobody owns any more. Catalog in idp/office.py, catalog() and school_catalog() in idp/demo.py.

ЁЯзТ Explain like I'm 5

The school office keeps a register ЁЯУЗ of every room: "Room 12 тАФ science lab тАФ owned by the science department тАФ needs the gas line and the fire exit."

One morning the gas line breaks. Who is affected? Dipika opens the register and follows the "needs" arrows backwards: the science lab needs the gas line, and the parent open day needs the science lab. So two things are in trouble, not one.

Then she finds two strange entries. The canteen orders room says its owner is "the 2019 hackathon team". That team broke up years ago. The fees room has no owner at all. These are orphans: rooms still in use that nobody looks after. If something breaks there, nobody gets the call.

And the fees room says it needs a "payments office" тАФ but there is no payments office in the register. A broken reference: either it is missing, or the note is wrong.

ЁЯЧ║я╕П Diagram

flowchart LR
    pp["parent-portal<br/>maths"] --> er["exam-results<br/>science"]
    pp --> tt["timetable<br/>maths"]
    er --> db[("results-db<br/>science")]
    er --> auth["auth<br/>facilities-office"]
    tt --> auth
    co["canteen-orders<br/>тЪая╕П team-2019-hackathon"] --> auth
    fe["fees<br/>тЪая╕П no owner"] --> auth
    fe -.-> pay["тЭУ payments-api<br/>not in the catalog"]
    ls["library-search<br/>library"] -.-> si["тЭУ search-index<br/>not in the catalog"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/platform-engineering/lesson-diagrams.html#l04

тЭУ What

ЁЯдФ Why

Because in a big organisation the most expensive question during an incident is "whose is this?". Without a catalog the answer lives in chat history and people's heads. With one, it is a lookup. The catalog is also the base for almost everything later in this course: scorecards (lesson 10) score catalog entries, the portal (lesson 12) is built on it, and policies (lesson 09) can require that every service has an owner.

ЁЯФз How (in this repo)

Catalog(groups) in idp/office.py keeps entries shaped like catalog-info.yaml: kind, name, owner, lifecycle, depends_on. owned_by(group) lists a group's entries; orphans() finds entries whose owner is missing or is not a known group; broken_refs() finds depends_on names the catalog does not hold; and dependents(name) walks the dependency arrows backwards, following chains, to give the blast radius. school_catalog() in idp/demo.py builds the school's 8 entries.

ЁЯзк Try it

python3 idp/demo.py catalog
python3 - <<'EOF'
import sys; sys.path.insert(0, "idp"); from demo import school_catalog
c = school_catalog()
print("if timetable breaks:", c.dependents("timetable"))
print("if exam-results breaks:", c.dependents("exam-results"))
c.register("Component", "canteen-orders", "facilities-office", lifecycle="experimental", depends_on=["auth"])   # adopted
c.register("Resource", "search-index", "library")                                                                # declared
print("orphans now:", c.orphans(), "┬╖ broken references now:", c.broken_refs())
print("library owns:", c.owned_by("library"))
EOF

тЬЕ Verify тАФ what you should see

catalog prints:

тФАтФА the catalog holds 8 entries owned by groups ['science', 'maths', 'library', 'facilities-office']
   science            owns ['exam-results', 'results-db']
   orphans (no owner, or the owner group is gone): ['canteen-orders', 'fees']
   broken references: [('fees', 'payments-api'), ('library-search', 'search-index')]
тФАтФА blast radius тАФ if auth breaks, these depend on it: ['canteen-orders', 'exam-results', 'fees', 'parent-portal', 'timetable']
   if results-db breaks: ['exam-results', 'parent-portal']

Your snippet prints:

if timetable breaks: ['parent-portal']
if exam-results breaks: ['parent-portal']
orphans now: ['fees'] ┬╖ broken references now: [('fees', 'payments-api')]
library owns: ['library-search', 'search-index']

ЁЯПБ What you just proved

results-db has only one direct user, exam-results, but its blast radius is two: parent-portal depends on exam-results, so it is hit too. Walking the chain matters. auth is the most shared piece in the school тАФ 5 entries depend on it тАФ which is exactly why the facilities office owns it. And the register gets better by doing, not announcing: one adoption fixed the canteen orphan, one declared resource fixed a broken reference. fees is still an orphan with a broken reference тАФ the next job.

тЪая╕П Common mistakes

ЁЯПн In production

A real Backstage catalog-info.yaml for exam-results, kept at the root of its repo:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: exam-results
  description: Publishes exam results to students and parents
  annotations:
    github.com/project-slug: school/exam-results
spec:
  type: service
  lifecycle: production
  owner: group:science
  dependsOn:
    - resource:results-db
    - component:auth

Backstage finds such files through catalog.locations in its app-config.yaml, or through a discovery provider that scans an organisation's repositories. On a real instance, the catalog REST API answers ownership questions directly:

curl -s "https://backstage.school.example/api/catalog/entities/by-query?filter=spec.owner=group:default/science" \
  -H "Authorization: Bearer $TOKEN" | jq -r '.items[].metadata.name'

ЁЯПн Why this matters in production: during an incident, the catalog is the fastest way to the right people. Make "has an owner that exists" a rule (lesson 09) and a scorecard check (lesson 10), not a hope.

тПня╕П Next

Part 1 is done: you know why, what to build first, how to start a service and how to find its owner. Part 2 starts with the biggest pain from lesson 02: getting a database without a ticket.

git checkout lesson-05-self-service-infra
тЖР Previousgolden pathsNext тЖТself service infra

This page is the lesson's README from the lesson-04-service-catalog branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.