Implement ODA Component
A standalone skill for building complete TM Forum ODA Component implementations. All reference patterns and reusable template files are bundled within this skill in the templates/ directory, so the skill can be installed in any repository.
Provenance: vendored from create-oda-component (Apache License 2.0), renamed to fit this repo's naming family and adapted per spec/spec-skills-consumer.md §6.1 — Steps 1 and 4a below now check this repo's own knowledge/ cache before falling back to the upstream skill's original live-fetch behavior, so a component/API already cached here isn't re-fetched over the network for no reason. Everything else (the actual generation logic, templates, and Steps 6-9's build/deploy behavior) is unchanged from upstream.
This is the one skill in tm-forum-oda-consumer with real side effects. Every other skill in this plugin is read-only against knowledge/; this one writes source code and Helm charts to the working directory, and — if you carry it through Steps 7 and 9 — builds/pushes real Docker images and can helm install the result into a real cluster. State this plainly when the skill is invoked; don't let it blend in with every other read-only skill's posture.
What you will build
For each new component you generate:
- Source code (
source/{ComponentName}/) — Node.js microservices for each exposed API, plus initialization jobs and a metrics microservice - Helm chart (
charts/{ComponentName}/) — Kubernetes manifests including the ODA Component CRD, API deployments, MongoDB, services, jobs, and PVC
Before starting, read these reference files as needed:
references/component-list.md— Full list of ODA Components and spec URL patternreferences/source-patterns.md— Source code structure, Node.js patterns, dockerfilesreferences/chart-patterns.md— Helm chart structure and template patterns
Template files bundled in this skill:
templates/source/utils/— 14 shared Node.js utility files (copy verbatim into every microservice)templates/source/index.html_replacement— Swagger UI customization filetemplates/source/roleInitializationMicroservice/— Role init job implementation (copy verbatim)templates/source/openMetricsMicroservice/— Metrics microservice implementation (customize counter name)templates/source/componentInitializationMicroservice/— Component init job reference (customize API URL)templates/source/MCPServerMicroservice/— MCP server reference implementation (customize per component)templates/charts/templates/deployment-rolemanagement.yaml— Conditional role management deployment templatetemplates/charts/README.md— Chart README reference example
Step 1 — Component Selection
Ask the user which ODA Component they want to build. Read references/component-list.md and present the full table of ~35 components so the user can choose by code (e.g. TMFC006) or name (e.g. "Service Catalog Management").
Once the user picks a component, check this repo's own cache first: knowledge/components/{CODE}/component.yaml. If it exists, read it directly — no network call needed. Only if it isn't cached yet, fetch the official specification YAML from the upstream source:
https://raw.githubusercontent.com/tmforum-rand/TMForum-ODA-Ready-for-publication/v1.0.0/{CODE}-{ShortName}/Specification/{CODE}-{ShortName}.yaml
and, once fetched, run python tools/fetch_component.py {CODE} to cache it properly for next time — don't leave a component this skill just fetched live uncached while every other skill in this repo reads from knowledge/.
Parse the YAML (cached or freshly fetched — same shape either way) to understand:
spec.componentMetadata— id, name, description, functionalBlock, publicationDatespec.coreFunction.exposedAPIs— APIs the component exposes (checkrequiredfield)spec.coreFunction.dependentAPIs— APIs the component consumesspec.securityFunction.exposedAPIs— Security API (typically TMF672 PermissionSpecificationSet)spec.managementFunction.exposedAPIs— Management API (typically open metrics)spec.eventNotification— Published and subscribed events
Step 2 — API Selection
Identify which exposed APIs are mandatory (required: true) and which are optional (required: false).
Always implement:
- All mandatory exposed APIs (
required: truein the spec'scoreFunction.exposedAPIs) - The security API — check the component's own
spec.securityFunction.exposedAPIs[].idfirst, don't assume which one it is. Confirmed against every component currently cached inknowledge/components/: 26 of 26 declareTMF669(Party Role Management), zero declareTMF672(Permission Specification Set) — the opposite of a blanket "always default to TMF672" rule. Uselesterthomas/partyroleapi:1.1when the spec declaresTMF669(the common case in this corpus), orlesterthomas/permissionspecapi:0.20when it genuinely declaresTMF672— theroleInitializationMicroservice's owninitialization.jsalready supports both via theUSE_PERMISSION_SPECenv var (§4b), so this is a values/wiring choice, not a code change. Never hardcode one over the other without checking the actual spec. - The management API (open metrics — use a component-specific tag, e.g.
lesterthomas/{componentnamelower}metrics:0.1)
Ask the user about each optional API:
"The specification includes these optional exposed APIs. Which would you like to implement?
- {API name} ({TMFXXX}) — {short description from spec}
- {API name} ({TMFXXX}) — ..."
Also ask:
"Would you also like to generate an MCP server microservice for AI agent access to this component's APIs? (Following the Python/FastMCP pattern from ProductCatalog)"
Note which APIs the user selects — this determines what microservices and Helm templates to generate.
Step 3 — Gather Docker Registry Info
Ask the user for their Docker Hub namespace (or container registry) for the new images:
"What Docker Hub username or container registry namespace should I use for the new images? (e.g.
myorg)"
This is used in builddockerfile.sh and values.yaml.
Step 4 — Generate Source Code
Read references/source-patterns.md for all patterns before generating files.
Create source/{ComponentName}/ with this structure:
4a — For each selected exposed API (including mandatory ones)
Create {apiname}Microservice/implementation/ with:
api/swagger.yaml — Check this repo's own cache first: knowledge/apis/{TMFxxx}/{TMFxxx}_v{version}.json. If it exists, convert it to YAML (the cached file is JSON; this skill's Node.js server loads swagger.yaml specifically, never swagger.json — convert, don't just rename) and save it as swagger.yaml. Only if the API isn't cached yet, download the OpenAPI spec from the URL in the specification YAML's specification[0].url field directly, save it as swagger.yaml, and run python tools/fetch_api.py afterward so it's cached for next time.
- Always use YAML format. Never use JSON (
swagger.json) —swaggerUtils.jsreadsswagger.yamlexclusively. - Always remove the
host:field from the spec (delete the line entirely, whether it came from the cache or a fresh download). Leavinghostundefined causes swagger-ui to use the current page's host, which is correct behaviour when the API is accessed through the ODA Canvas ingress atlocalhost. A hardcoded value such asserverRootor any other placeholder will cause swagger-ui to show a wrong Base URL. - After obtaining the spec, add
x-swagger-router-controllerto every operation in it. Map each operation's first tag to the corresponding PascalCase controller filename (e.g. tagproductOrder→ controllerProductOrder). This is required because Linux's case-sensitive filesystem cannot match a lowercase tag name to a PascalCase.jsfile. Without this field,swagger-toolswill fail at runtime withCannot resolve the configured swagger-router handler. The mapping pattern is: take the tag, split on spaces (for multi-word tags), PascalCase each word, join — e.g.events subscription→EventsSubscription,notification listeners (client side)→NotificationListenersClientSide.
index.js — Follow the exact pattern from references/source-patterns.md. Key customizations:
- Set
componentNamedefault tor1-{componentnamelower}for local testing - The swagger basePath comes from the swagger spec (cached or freshly downloaded)
- Always register the entrypoint after swagger-ui:
app.use(swaggerDoc.basePath, entrypointUtils.entrypoint)— this provides a JSON_linksdocument at the API root - Always use
swagger-ui-distfor the swagger UI (passapiDocs,swaggerUi, andswaggerUiDirtomiddleware.swaggerUi()) - Always use the
TError/sendErrorerror handler pattern (not a simpleres.endhandler)
controllers/{Resource}.js — One file per resource defined in the swagger spec. Use the thin-passthrough pattern. Inspect the swagger spec's paths to identify which resources exist and which CRUD operations each supports.
service/{Resource}Service.js — One file per resource. Implement all CRUD operations defined in the swagger spec using the MongoDB promise chain pattern from references/source-patterns.md. Use listResource / retrieveResource utilities for GET operations.
Critical: registerListener and unregisterListener must always delegate to notificationUtils.register(req, res, next) and notificationUtils.unregister(req, res, next) respectively — never use the generic CRUD pattern for these. This ensures hub subscriptions are stored in the HUB collection that notificationUtils.publish reads from when dispatching events to registered listeners.
utils/ — Copy all 14 utility files from templates/source/utils/ (bundled in this skill) verbatim into each microservice. These are shared utilities that work across any TMF API.
package.json — Follow the pattern from references/source-patterns.md, updating name and description to match the specific API (e.g. "name": "service-catalog-management", "description": "TMF API Reference: TMF633 - Service Catalog Management").
config.json — {"strict_schema": true}
index.html_replacement — Copy from templates/source/index.html_replacement (bundled in this skill)
4b — Role Initialization Microservice
Create roleInitializationMicroservice/implementation/ using the pattern from references/source-patterns.md. This is identical across all components — copy from templates/source/roleInitializationMicroservice/ (bundled in this skill). The initialization.js supports both TMF669 and TMF672 via the USE_PERMISSION_SPEC env var.
4c — Open Metrics Microservice
Create openMetricsMicroservice/ using the pattern from references/source-patterns.md. This is nearly identical across all components — customize:
- Counter name:
{componentnamelower}_api_counter— Important: Prometheus metric names must match[a-zA-Z_:][a-zA-Z0-9_:]*(no hyphens). Always derive the metric name by replacing hyphens with underscores:const metricName = componentName.replace(/-/g, '_');then usemetricName + '_api_counter'. TheCOMPONENT_NAMEenv var at runtime is typically{release}-{componentname}(e.g.pi1-productinventory), which contains hyphens. - Description: reference the specific TMF API being monitored
4d — Dockerfiles
Create one dockerfile per microservice following the FROM node:16 / COPY / WORKDIR / RUN npm install / EXPOSE 8080 / CMD pattern.
Use FROM node:10.19 for roleInitializationMicroservice and openMetricsMicroservice (no port expose for role init).
4e — MCP Server (if user requested)
Follow the Python/FastMCP pattern from templates/source/MCPServerMicroservice/ (bundled in this skill — the product_catalog_mcp_server.py and product_catalog_api.py are ProductCatalog-specific examples to use as reference). Create:
{componentname}MCPServerMicroservice/{componentname}_mcp_server.py— FastMCP server with tools for each resource CRUD operation{componentname}MCPServerMicroservice/{componentname}_api.py— httpx async API client{componentname}MCPServerMicroservice/pyproject.toml— Python dependencies (fastmcp, httpx, uvicorn){componentname}-mcp-dockerfile—FROM python:3.13,RUN pip install .,CMD python {componentname}_mcp_server.py
4f — Build Script
Create builddockerfile.sh listing docker buildx build commands for all new images, using multi-platform linux/amd64,linux/arm64 builds. Follow the pattern from references/source-patterns.md.
Important naming rule for metrics image: Use a component-specific tag (e.g. {dockerhub-namespace}/{componentname}metrics:0.1) rather than the shared openmetrics:1.0 tag to avoid overwriting the ProductCatalog image. Update values.yaml metrics.image to match.
Do not include a permissionspecapi or partyroleapi build in builddockerfile.sh — both use pre-built shared images (lesterthomas/permissionspecapi:0.20 and lesterthomas/partyroleapi:1.1) with no source dockerfile in this directory.
Step 5 — Generate Helm Chart
Read references/chart-patterns.md for all patterns before generating templates.
Create charts/{ComponentName}/:
Chart.yaml
Fill in name (lowercase component name), description mentioning the component code and name, version: 1.0.0.
values.yaml
Fill in from the parsed specification YAML:
component.id— fromcomponentMetadata.idcomponent.name— lowercase component namecomponent.functionalBlock— fromcomponentMetadata.functionalBlockcomponent.publicationDate— fromcomponentMetadata.publicationDateapi.image—{dockerhub-namespace}/{componentname}api:0.1partyrole.image: lesterthomas/partyroleapi:1.1,partyrole.enabled: true— the common case (§2's finding: 26 of 26 currently-cached components declareTMF669, notTMF672) — enable whichever one the component's ownsecurityFunction.exposedAPIsactually declares, notpermissionspecby defaultpermissionspec.image: lesterthomas/permissionspecapi:0.20— kept for the genuineTMF672case; setpermissionspec.enabled: true/partyrole.enabled: falseonly when the spec actually declaresTMF672- Add sections for each optional API that was selected (e.g.
promotionmgmt.image,promotionmgmt.enabled: false)
templates/component-{componentname}.yaml (the ODA Component CRD)
This is the most important template. Build it from the fetched specification YAML:
spec.componentMetadata— pull description, functionalBlock, id, name from the specspec.coreFunction.exposedAPIs— add each selected API with propergatewayConfigurationblock using{{.Values.component.apipolicy.*}}Helm template expressions- For optional APIs, wrap in
{{- if .Values.{optionalapi}.enabled }}conditional - For MCP server, wrap in
{{- if .Values.component.MCPServer.enabled }} spec.coreFunction.dependentAPIs— wrap in{{- if .Values.component.dependentAPIs.enabled }}conditional with{{- else }}that outputs[]spec.managementFunction— standard open metrics blockspec.securityFunction— conditional block:{{- if .Values.partyrole.enabled }}for theTMF669case (the common one — §2),{{- if .Values.permissionspec.enabled }}for the genuineTMF672case
Critical: Every path must use {{.Release.Name}}-{{.Values.component.name}} as the prefix. The API path should follow the pattern: /{release}-{componentname}/tmf-api/{apiPath}/v{n}.
templates/ (remaining templates)
Generate the following, following references/chart-patterns.md patterns exactly:
deployment-{primary-api}api.yaml— customizestartupProbepath to a real endpoint of that APIdeployment-mongodb.yaml— standard, copy pattern verbatimdeployment-metricsapi.yaml— standard, copy pattern verbatimdeployment-rolemanagement.yaml— conditional permissionspec/partyroleservice-{primary-api}api.yaml— NodePort service for each APIservice-mongodb.yaml— standard MongoDB serviceservice-registerallevents.yaml— metrics service (port 4000, selector: metricsapi)service-rolemanagement.yaml— conditional service (permissionspecapi or partyroleapi)job-roleinitialization.yaml— standard role init job (uses lesterthomas/roleinitialization:0.1)job-{componentname}initialization.yaml— component-specific init jobpersistentVolumeClaim-mongodb.yaml— standard 5Gi PVC
For each optional API that the user chose to include, add corresponding deployment-{optionalapi}api.yaml and service-{optionalapi}api.yaml templates (conditional on {{- if .Values.{optionalapi}.enabled }}).
Step 6 — Verify
After generation, run:
helm lint charts/{ComponentName}/
Fix any lint errors before finishing. Common issues:
- Missing required values in
values.yamlthat templates reference - Indentation errors in generated YAML
helm lint does NOT catch a Service port name over Kubernetes' 15-character limit — confirmed by actually running it against a chart using the naming pattern this skill used to document: lint passed clean while the rendered manifest had an 18-character port name, which a real kubectl apply rejects outright. references/chart-patterns.md's Service templates now reuse the container's own short {abbrapi} name for the Service's port too, which stays within the limit — but always run helm template and check every spec.ports[].name length directly (len(name) <= 15) as an explicit extra check after helm lint passes clean, don't treat a clean lint as proof this is fine.
Step 7 — Build and Push Docker Images
Real side effect — this step builds and pushes container images to a real registry. After helm lint passes, build and push all Docker images. Run from the source/{ComponentName}/ directory:
cd source/{ComponentName}/
bash builddockerfile.sh
Or run each command individually to monitor build output per image:
cd source/{ComponentName}/
docker buildx build -t "{dockerhub-namespace}/{componentnamelower}api:0.1" --platform "linux/amd64,linux/arm64" -f {componentnamelower}-dockerfile . --push
docker buildx build -t "{dockerhub-namespace}/{componentnamelower}initialization:0.1" --platform "linux/amd64,linux/arm64" -f {componentnamelower}initialization-dockerfile . --push
docker buildx build -t "{dockerhub-namespace}/{componentnamelower}metrics:0.1" --platform "linux/amd64,linux/arm64" -f openMetricsMicroservice-dockerfile . --push
Wait for each build to complete and confirm the push succeeded (pushing manifest for docker.io/... in the output).
Note:
docker buildxrequires a multi-platform builder. If not already set up, rundocker buildx create --use --name multiarch-builderfirst.
Multi-Arch Build Reliability
Multi-platform builds can sometimes stall during the registry push phase despite successful local builds. If you encounter --push hanging indefinitely or manifest errors:
Add
--provenance=falseflag to alldocker buildx buildcommands. This reduces manifest complexity and improves registry upload reliability:docker buildx build -t "{namespace}/{image}:0.1" --platform "linux/amd64,linux/arm64" -f {dockerfile} . --push --provenance=falseFor persistent push failures, use the per-architecture workaround:
# Build separate tags for each architecture docker buildx build -t "{namespace}/{image}:0.1-amd64" --platform linux/amd64 -f {dockerfile} . --push --provenance=false docker buildx build -t "{namespace}/{image}:0.1-arm64" --platform linux/arm64 -f {dockerfile} . --push --provenance=false # Compose multi-arch manifest from per-arch images docker buildx imagetools create -t {namespace}/{image}:0.1 {namespace}/{image}:0.1-amd64 {namespace}/{image}:0.1-arm64Single-platform pushes are more reliable than multi-platform
--push, andimagetools createassembles the final manifest atomically.Verify successful push by inspecting the manifest:
docker buildx imagetools inspect {namespace}/{image}:0.1Both
linux/amd64andlinux/arm64manifests should be listed.
Step 8 — Generate README Files
Chart README (charts/{ComponentName}/README.md)
Model this on templates/charts/README.md (bundled in this skill). Include:
Title and intro —
# Example {ComponentName} componentwith a one-line description linking to the TM Forum component directory page.Functionality section — describe each function area:
- Core function — list mandatory and optional exposed APIs, and any dependent APIs. For each optional feature, show the
--setoverride to enable it:helm install <release name> oda-components/{componentnamelower} --set {feature}.enabled=true -n components - Management function — describe the open metrics endpoint and what business events are counted, mention Open Telemetry tracing with the OTLP config snippet from
values.yaml. - Security function — describe the conditional
TMF669/TMF672role management withpartyrole.enabled/permissionspec.enabled(whichever the spec actually declares).
- Core function — list mandatory and optional exposed APIs, and any dependent APIs. For each optional feature, show the
Microservices list — bullet list of all microservices deployed, one line each describing what they do.
Installation section — step-by-step:
helm install r1 .\{componentnamelower} -n componentsShow the
kubectl get components -n componentsverification command and expected output withDEPLOYMENT_STATUS: Complete.Configuration table — Markdown table of all configurable
values.yamlkeys with columnsVariable Name,Default,Explanation. Cover at minimum:mongodb.port,mongodb.databaseapi.imageapi.otlp.console.enabled,api.otlp.protobuffCollector.enabled,api.otlp.protobuffCollector.urlmetrics.imagepermissionspec.enabled- Any component-specific optional API flags (e.g.
promotionmgmt.enabled)
Source README (source/{ComponentName}/README.md)
Create a new file explaining the source code structure for developers who want to understand or extend it. Include:
Title and intro —
# {ComponentName} Source Code— explain this is the Node.js reference implementation of the ODA component, and link to the chart folder for deployment.Repository structure — a tree or table listing each top-level directory with a one-line description:
Directory Description {componentname}Microservice/Node.js implementation of the TMF{xxx} {API Name} Open API roleInitializationMicroservice/Bootstraps the initial role (PermissionSpecificationSet or PartyRole) on first deploy {componentname}InitializationMicroservice/Registers the metrics microservice as an event listener on first deploy openMetricsMicroservice/Prometheus/OpenMetrics endpoint that counts business events *-dockerfileDockerfile for each microservice builddockerfile.shScript to build and push all Docker images Architecture overview — a short paragraph describing how the microservices interact:
- The main API microservice stores data in MongoDB and publishes events to registered listeners via the hub endpoint.
- The initialization job registers the metrics microservice as a listener.
- The metrics microservice receives events and increments Prometheus counters.
- The role initialization job creates the initial role in the permission/partyrole API on startup.
Main API microservice deep-dive (
{componentname}Microservice/implementation/) — describe the layout:File/Folder Description index.jsEntry point — loads swagger, wires middleware, starts HTTP server api/swagger.yamlOpenAPI spec — defines all routes and schemas controllers/Thin passthrough — maps swagger operationIds to service functions service/Business logic — MongoDB CRUD, event publishing utils/Shared utilities (mongoUtils, notificationUtils, swaggerUtils, etc.) config.jsonRuntime config (strict_schema: true) package.jsonnpm dependencies Building Docker images — show commands:
cd source/{ComponentName}/ bash builddockerfile.shOr individually per image. Note the multi-platform build requires
docker buildx.Running locally (optional guidance) — briefly describe how a developer could run a single microservice locally for testing with a local MongoDB instance.
Step 9 — Summary
Real side effect — Step 9's own deploy command, if actually run, changes a real cluster's state. Tell the user what was created, what they need to do next:
- Deploy:
helm install r1 charts/{ComponentName}/ -n components - Verify:
kubectl get components -n components— expectDEPLOYMENT_STATUS: Complete
Key Rules
- Never hardcode the Helm release name in templates. Always use
{{.Release.Name}}. - Always put the
oda.tmforum.org/componentName: {{.Release.Name}}-{{.Values.component.name}}label on every Kubernetes resource. - Follow the spec: The mandatory/optional distinction in
exposedAPIs[].requiredmust be respected — always implementrequired: trueAPIs, always ask aboutrequired: falseAPIs. - Security API (
securityFunction.exposedAPIs) is always implemented — use the existing shared role management implementation images unless the user wants custom ones. - OpenAPI spec URLs: Get them from
exposedAPIs[].specification[0].urlin the specification YAML, or from this repo's ownknowledge/apis/{TMFxxx}/{TMFxxx}_v{version}.jsoncache when it's already there (Step 4a). The swagger spec must end up saved asapi/swagger.yamlin each microservice, in YAML, regardless of which source it came from. - Naming consistency: The Kubernetes service name for each API becomes the hostname for inter-service communication. It must match the
implementationfield in the Component CRD and the selector in the Service template. - Docker Buildx multi-arch: Always use
--provenance=falseon multi-platform builds. If standard multi-platform--pushstalls, use per-arch--pushfollowed bydocker buildx imagetools createto compose the final manifest.