CLI¶
ocf generates the custom-resource wrapper pattern that Custom Resources describes. Templates are
embedded in the binary, so generated code matches the framework version the CLI was built from.
Installation¶
ocf version prints the framework version the binary was built from, so you can confirm which template set a generated
package came from.
ocf scaffold wrapper¶
ocf scaffold wrapper generates a custom-resource wrapper package for a Kubernetes kind the built-in primitives do not
cover.
| Flag | Required | Default | Meaning |
|---|---|---|---|
--type |
yes | Wrapped Go type as <import-path>.<TypeName>, split on the last dot |
|
--variant |
yes | static, workload, task, or integration |
|
--group |
yes | API group as a DNS subdomain. Pass --group "" for core API group types |
|
--version |
no | last import-path segment when it looks like an API version; required otherwise | API version, for example v1, v2beta1 |
--kind |
no | the type name | Kind used in the identity string |
--cluster-scoped |
no | false |
Omit the namespace segment and require an empty namespace |
--alias |
no | derived | Import alias for the wrapped type's package |
--package |
no | lowercased kind | Go package name of the generated package |
--out |
no | ./<package> |
Output directory |
--force |
no | false |
Write into a non-empty directory |
Group and version validation¶
Both values end up verbatim in the generated identity string, so ocf checks them before it writes anything.
--group must be a DNS subdomain, the way Kubernetes defines API groups: lowercase letters, digits, - and ., with
every dot-separated label starting and ending in a letter or digit. apps, cert-manager.io and
rbac.authorization.k8s.io all pass. The one exception is --group "", which selects the core API group and makes the
identity string a bare <version>/<Kind>/....
--version must be a lowercase v followed by digits, optionally followed by alpha or beta and more digits, for
example v1, v2beta1, or v1alpha3. The same check applies whether you pass --version yourself or ocf derives it
from the import path, so an explicit version and a derived one always mean the same thing.
A value that fails either check is rejected with an error naming the flag, for example
--version "1.0" is not a valid API version, and no files are written.
Choosing a variant¶
--variant selects the generic resource category the wrapper builds on. Each category maps to a different set of
lifecycle interfaces, so the fields your wrapped kind reports readiness through determine which one fits. See
Choose a resource category for the full explanation.
| Category | Generic type | Lifecycle interfaces | Use when |
|---|---|---|---|
| Workload | generic.WorkloadResource |
Alive, Graceful, Suspendable, Guardable, DataExtractable |
Long-running processes with replica-based health |
| Static | generic.StaticResource |
Guardable, DataExtractable |
Configuration objects with no runtime health semantics |
| Task | generic.TaskResource |
Completable, Suspendable, Guardable, DataExtractable |
Run-to-completion workloads |
| Integration | generic.IntegrationResource |
Operational, Graceful, Suspendable, Guardable, DataExtractable |
External-dependency objects (services, ingresses) |
Worked example¶
This scaffolds a wrapper for cert-manager's Certificate CRD, an external-dependency object, so it uses the integration
variant:
ocf scaffold wrapper \
--type github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1.Certificate \
--variant integration \
--group cert-manager.io
Generated integration wrapper package "certificate" in ./certificate:
builder.go
builder_test.go
mutator.go
resource.go
Next steps:
1. Run go mod tidy so github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 resolves in your module.
2. Run go test ./certificate/... to verify the generated package.
3. Replace the scaffolded default handlers in builder.go with Certificate-specific logic.
Neither --kind, --package, nor --version were passed: the kind defaults to Certificate (the type name), the
package defaults to certificate (the lowercased kind), and the version defaults to v1 (the import path's last
segment, which matches the API-version pattern). The four generated files:
builder.goregisters the scaffolded default handlers, exposes the fluent configuration API (WithMutation,WithGuard,WithDataGuard,WithOptionalData, theWithCustom*status setters), andBuild()returns theResource.builder_test.gotestsBuild()validation, that a registered mutation applies through the mutator, declared data extraction withExtractInto, andWithDataGuard/WithOptionalDatagating.mutator.godefinesMutator, which records metadata and object edits and applies them in a single pass whenApply()runs.resource.godefinesResource, which delegates every lifecycle method to the generic base:Identity,Object,Mutate, the variant's status and suspension methods,GuardStatus,ExtractData,ProducedData,ConsumedData,RecordObservation,Preview,RegisteredMutations, andFiringSet.
Follow the printed next steps: run go mod tidy so github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1
resolves in your module, then go test ./certificate/... to verify the generated package builds and passes before you
start replacing the scaffolded defaults.
Import handling¶
The import path in --type is everything before the last dot, and all four generated files import it, so ocf checks
its shape before it writes anything. It must be slash-separated elements of ASCII letters, digits and -, ., _, ~
or +, with no empty element and no element starting or ending in a dot. Anything else is rejected with
--type import path "..." is not a valid Go import path and no files are written. Only the shape is checked, so a
well-formed path to a package that does not exist still passes here.
--alias defaults to a derived name when omitted: the sanitized second-to-last import-path segment concatenated with
the last segment, lowercased and with every character that cannot appear in a Go identifier stripped. In the example
above, github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 derives certmanagerv1 from certmanager and
v1. When the last segment does not look like an API version, the sanitized last segment is used alone. Pass --alias
to override the derived name. If no valid Go identifier can be derived at all, ocf exits with an error and --alias
must be passed explicitly.
--version defaults to the import path's last segment only when that segment matches the API-version pattern described
in Group and version validation. When the last segment does not match, for example an
import path ending in /api or /types, ocf exits with --version is required and you must pass --version
explicitly.
ocf never edits your go.mod. It only prints a next-steps block telling you to run go mod tidy (or go get) if
your module does not already depend on the wrapped type's package.
What the generated defaults do¶
Which Default*Handler functions get generated depends on the variant. Static generates none of them: it has no status,
grace, or suspension semantics, so its builder registers nothing beyond mutation and data-extraction support.
| Handler | Generated for | Reports unconditionally |
|---|---|---|
DefaultConvergingStatusHandler |
Workload, Task | Workload: Healthy. Task: Completed. |
DefaultOperationalStatusHandler |
Integration | Operational |
DefaultGraceStatusHandler |
Workload, Integration | Healthy |
DefaultSuspensionStatusHandler |
Workload, Task, Integration | Suspended |
DefaultSuspendMutationHandler |
Workload, Task, Integration | No mutation; the object is left untouched while suspended |
DefaultDeleteOnSuspendHandler |
Workload, Task, Integration | false; the object is kept, not deleted |
Every reason string these handlers return starts with "Scaffolded default", so they are easy to grep for once you start
replacing them. The status handler (DefaultConvergingStatusHandler or DefaultOperationalStatusHandler) is required
by the generic layer: Build() fails without one, so the scaffold registers it in NewBuilder and the builder's setter
can only replace it, never clear it. If you replace the status handler, keep the grace handler consistent with it: see
Keeping convergence and grace consistent.
What the CLI does not check¶
ocf never loads the target Go package. It does not verify that --type refers to a real, exported struct, that the
type satisfies client.Object, or that your module depends on the package that defines it. A wrong --type, a type
that does not satisfy client.Object, or a missing module dependency all surface at your first go build.
Regenerating¶
ocf scaffold wrapper refuses to write into a non-empty output directory unless --force is passed. With --force, it
overwrites the four generated files (builder.go, builder_test.go, mutator.go, resource.go) and leaves every
other file in the directory alone.