Agent Skills: ODH Module Scaffold

>-

UncategorizedID: opendatahub-io/ai-helpers/module-scaffold

Repository

opendatahub-ioLicense: Apache-2.0
3674

Install this agent skill to your local

pnpm dlx add-skill https://github.com/opendatahub-io/ai-helpers/tree/HEAD/helpers/skills/module-scaffold

Skill Files

Browse the full folder contents for module-scaffold.

Download Skill

Loading file tree…

helpers/skills/module-scaffold/SKILL.md

Skill Metadata

Name
module-scaffold
Description
>-

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:

  1. Which API group to use: components.platform.opendatahub.io or services.platform.opendatahub.io? Default: components.
  2. 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.TypeMeta and metav1.ObjectMeta
  • Have Spec field using <COMPONENT_PASCAL>Spec
  • Have Status field 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.NewReconciler with functional options pattern
  • Register render, deploy, and gc actions (gc MUST be last)
  • Use ReconcilerFor builder pattern with WithReconcilerOpts for release info. The default conditions (Ready + ProvisioningSucceeded) are sufficient for the scaffold. Add WithConditions("Degraded") only when real actions explicitly manage it via rr.Conditions.MarkFalse/MarkTrue
  • Import from odh-platform-utilities packages:
    • framework/controller/reconciler for reconciler setup
    • framework/controller/conditions for condition management
    • framework/controller/actions/deploy for resource deployment
    • framework/controller/actions/gc for garbage collection
    • framework/controller/actions/render for manifest rendering
    • framework/api for Release type
    • pkg/cluster for platform detection
    • pkg/metadata for label/annotation constants

internal/controller/<COMPONENT_SNAKE>_controller_actions.go

Define three action functions:

  1. renderAction -- use framework/controller/actions/render
  2. deployAction -- use framework/controller/actions/deploy with SSA mode
  3. 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 conventions
  • controller.md -- Controller patterns and GC ordering
  • helm-chart.md -- Chart content restrictions
  • security.md -- Webhook, TLS, RBAC, and metadata rules
  • testing.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 tidy will fail until controller-gen generates deepcopy methods and all placeholder code compiles. Run make generate first.
  • 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/api for 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.ComponentReleaseStatus MUST be an anonymous inline embed in the status struct: common.ComponentReleaseStatus \json:",inline"`. A named field like Releases 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-role and will overwrite any resource with the same name via SSA, stripping the standalone controller's permissions. Use the Helm fullname helper (which defaults to the chart name) to generate unique names like <MODULE_REPO> instead of common patterns like *-manager-role.