ODH Module Scaffold
Generate a complete standalone module operator repository for the ODH platform.
Step 1: Parse arguments
Extract the component name from $ARGUMENTS. If not provided, ask the user.
Derive these values from the component name (example: model-registry):
| Variable | Example Value |
|----------|---------------|
| COMPONENT | model-registry |
| COMPONENT_SNAKE | model_registry |
| COMPONENT_PASCAL | ModelRegistry |
| COMPONENT_LOWER | modelregistry |
| COMPONENT_LOWER_PLURAL | modelregistries |
| MODULE_REPO | <COMPONENT>-operator |
| API_GROUP | components.platform.opendatahub.io |
| API_VERSION | v1alpha1 |
| SINGLETON_NAME | default-<COMPONENT> |
Ask the user:
- Which API group to use:
components.platform.opendatahub.ioorservices.platform.opendatahub.io? Default:components. - Target directory? Default:
./<MODULE_REPO>.
Step 2: Create directory structure
Generate this layout:
<MODULE_REPO>/
├── api/<API_VERSION>/
│ ├── <COMPONENT_SNAKE>_types.go
│ ├── <COMPONENT_SNAKE>_common.go
│ ├── groupversion_info.go
│ └── doc.go
├── internal/
│ ├── controller/
│ │ ├── <COMPONENT_SNAKE>_controller.go
│ │ └── <COMPONENT_SNAKE>_controller_actions.go
│ └── webhook/
│ └── singleton.go
├── charts/<MODULE_REPO>/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ ├── deployment.yaml
│ ├── serviceaccount.yaml
│ ├── clusterrole.yaml
│ ├── clusterrolebinding.yaml
│ ├── webhook-service.yaml
│ ├── webhook-certificate.yaml
│ └── _helpers.tpl
├── cmd/
│ └── main.go
├── config/
│ └── samples/
│ └── <COMPONENT_SNAKE>.yaml
├── go.mod
├── Makefile
├── Containerfile
├── .github/workflows/
│ └── ci.yaml
├── .rules/
│ ├── api-types.md
│ ├── controller.md
│ ├── helm-chart.md
│ ├── security.md
│ └── testing.md
├── AGENTS.md
├── README.md
└── .gitignore
Step 3: Generate CRD types
Use the templates from ${CLAUDE_SKILL_DIR}/references/templates.md to
generate each file. Key requirements:
api/<API_VERSION>/<COMPONENT_SNAKE>_types.go
The CR type must:
- Embed
metav1.TypeMetaandmetav1.ObjectMeta - Have
Specfield using<COMPONENT_PASCAL>Spec - Have
Statusfield using<COMPONENT_PASCAL>Status - Include kubebuilder markers:
+kubebuilder:object:root=true,+kubebuilder:subresource:status,+kubebuilder:resource:scope=Cluster - Include CEL singleton validation for the name
- Include compile-time assertion:
var _ common.PlatformObject = (*<COMPONENT_PASCAL>)(nil) - Implement all PlatformObject methods (GetStatus, GetConditions, SetConditions, GetReleaseStatus, SetReleaseStatus)
The status struct must embed common.Status and
common.ComponentReleaseStatus both as anonymous (unnamed) fields with
json:",inline" tags. Use common.ComponentReleaseStatus \json:",inline"`instead of a named field likeReleases common.ComponentReleaseStatus json:"releases,omitempty"because a named field serializes as a nested object{releases: {releases: [...]}}instead of the flat array{releases: [...]}` the CRD expects, causing
status update failures.
api/<API_VERSION>/<COMPONENT_SNAKE>_common.go
Define <COMPONENT_PASCAL>CommonSpec with common.ManagementSpec embedded
inline. This struct is inlined into <COMPONENT_PASCAL>Spec.
api/<API_VERSION>/groupversion_info.go
Standard kubebuilder groupversion registration using the chosen API group and version.
api/<API_VERSION>/doc.go
Package doc comment with +groupName=<API_GROUP> marker.
Step 4: Generate controller
internal/controller/<COMPONENT_SNAKE>_controller.go
The controller setup must:
- Use
reconciler.NewReconcilerwith functional options pattern - Register render, deploy, and gc actions (gc MUST be last)
- Use
ReconcilerForbuilder pattern withWithReconcilerOptsfor release info. The default conditions (Ready + ProvisioningSucceeded) are sufficient for the scaffold. AddWithConditions("Degraded")only when real actions explicitly manage it viarr.Conditions.MarkFalse/MarkTrue - Import from
odh-platform-utilitiespackages:framework/controller/reconcilerfor reconciler setupframework/controller/conditionsfor condition managementframework/controller/actions/deployfor resource deploymentframework/controller/actions/gcfor garbage collectionframework/controller/actions/renderfor manifest renderingframework/apifor Release typepkg/clusterfor platform detectionpkg/metadatafor label/annotation constants
internal/controller/<COMPONENT_SNAKE>_controller_actions.go
Define three action functions:
- renderAction -- use
framework/controller/actions/render - deployAction -- use
framework/controller/actions/deploywith SSA mode - gcAction -- use
framework/controller/actions/gc
Each action follows the signature:
func(ctx context.Context, rr *types.ReconciliationRequest) error
Step 5: Generate Helm chart
charts/<MODULE_REPO>/Chart.yaml
apiVersion: v2
name: <MODULE_REPO>
description: Helm chart for the <COMPONENT_PASCAL> module controller
version: 0.1.0
appVersion: "0.1.0"
type: application
charts/<MODULE_REPO>/values.yaml
Provide defaults for: image repository/tag, replicas (1), resources (requests: 100m CPU, 128Mi memory; limits: 500m CPU, 512Mi memory), service account name, node selector, tolerations.
Templates
Generate ONLY controller infrastructure manifests:
- deployment.yaml -- Controller Deployment with leader election, health/readiness probes, security context (non-root, read-only rootfs)
- serviceaccount.yaml -- ServiceAccount for the controller
- clusterrole.yaml -- ClusterRole with module-specific RBAC (no wildcards)
- clusterrolebinding.yaml -- Bind the ClusterRole to the ServiceAccount
- webhook-service.yaml -- Service exposing webhook port 443 for the singleton admission webhook
- webhook-certificate.yaml -- cert-manager Certificate for webhook TLS (uses a self-signed Issuer scoped to the release namespace)
- _helpers.tpl -- Standard Helm helpers (name, labels, selectors)
Do NOT generate application-level manifests.
Step 6: Generate supporting files
cmd/main.go
Standard controller-runtime manager setup with:
- Scheme registration for the module API
- Health/readiness endpoints
- Leader election support
- Controller and webhook registration
Makefile
Include targets: build, test, lint, manifests, generate, fmt,
vet, docker-build, docker-push, helm-lint.
Containerfile
Multi-stage build: Go builder stage + distroless runtime stage.
.github/workflows/ci.yaml
GitHub Actions workflow with: lint, test, build jobs. Pin actions to full
commit SHAs. Set permissions: read-all.
AGENTS.md
Use the comprehensive template from
${CLAUDE_SKILL_DIR}/../module-compliance/references/agents-md-template.md
as a starting point. Replace <COMPONENT_PASCAL> and other placeholders with
the actual component values. This file provides passive guidance to AI agents
working in the module repo -- covering PlatformObject contract, controller
patterns, Helm chart rules, security, and separation of concerns.
.rules/ directory
Copy the rule files from
${CLAUDE_SKILL_DIR}/../module-compliance/references/rules/ into the
generated repo's .rules/ directory. These are path-scoped rules that
fire automatically when agents edit matching files (e.g., api/**/*.go,
internal/controller/**/*.go, charts/**). Files to copy:
api-types.md-- CRD type conventionscontroller.md-- Controller patterns and GC orderinghelm-chart.md-- Chart content restrictionssecurity.md-- Webhook, TLS, RBAC, and metadata rulestesting.md-- Test patterns and conventions
config/samples/<COMPONENT_SNAKE>.yaml
A sample CR manifest with the singleton name and managementState: Managed.
README.md
Brief README with: project description, prerequisites (Go 1.25+, controller-runtime, cert-manager), build instructions, usage instructions.
.gitignore
Standard Go gitignore (bin/, vendor/, *.exe, etc.).
Step 7: Initialize Go module
Run the following commands in the generated directory:
cd <MODULE_REPO>
go mod init github.com/opendatahub-io/<MODULE_REPO>
go mod edit -require github.com/opendatahub-io/odh-platform-utilities@v0.1.0
go mod edit -require github.com/opendatahub-io/odh-platform-utilities/framework@v0.1.0
The root module (api/common, pkg/) is tagged at v0.1.0. The framework
sub-module has its own go.mod and requires a separate dependency line.
Note: go mod tidy will fail until the controller code compiles. Inform the
user that they should run go mod tidy after filling in the controller
implementation details and installing controller-gen.
Step 8: Validate
Suggest the user run the module-compliance skill against the generated repo
to verify all contract rules pass. Also suggest running make lint and
make test once the Go dependencies are resolved.
Gotchas
go mod tidywill fail until controller-gen generates deepcopy methods and all placeholder code compiles. Runmake generatefirst.- The Helm chart must contain ONLY controller manifests. Application workloads are embedded in the controller binary and applied by the controller at runtime.
- Do not import
github.com/openshift/apifor ManagementState. The shared library uses a plain string type that is wire-compatible. - The GC action must always be the last
.WithAction()in the reconciler builder chain. Deploy stamps annotations that GC reads -- reversing the order deletes resources that were just deployed. - The singleton name in the CEL rule must exactly match what the orchestrator
creates. Convention is
default-<component>. common.ComponentReleaseStatusMUST be an anonymous inline embed in the status struct:common.ComponentReleaseStatus \json:",inline"`. A named field likeReleases common.ComponentReleaseStatuswraps the innerreleasesarray in an extra JSON object, producing{releases: {releases: [...]}}` which fails CRD validation.- RBAC resource names (ClusterRole, ClusterRoleBinding) must be unique and
must not collide with names the in-tree operator handler creates for the
same component. The in-tree handler typically creates resources like
<component>-operator-manager-roleand will overwrite any resource with the same name via SSA, stripping the standalone controller's permissions. Use the Helmfullnamehelper (which defaults to the chart name) to generate unique names like<MODULE_REPO>instead of common patterns like*-manager-role.