This guide defines the repository contract for adding an operand component to the HyperFleet Operator. A component is a package that renders Kubernetes objects and reports health; it is not a new controller.
In this document, operator bundle definition means the ordered component set
selected by spec.bundle in internal/bundle. It is different from the OLM
bundle, the release artifact under bundle/config/manifests.
Every component implements internal/bundle.Component:
type Component interface {
Name() string
Render(ctx context.Context, cr *hyperfleetv1alpha1.HyperFleetConfig) ([]client.Object, error)
Conditions(ctx context.Context, cr *hyperfleetv1alpha1.HyperFleetConfig, applied []client.Object) ([]metav1.Condition, error)
Rollout() component.RolloutTarget
ReferencedSecrets(cr *hyperfleetv1alpha1.HyperFleetConfig) []component.SecretReference
}Render is a pure desired-state function. It must not read from or write to the
cluster. Return typed objects with complete TypeMeta, stable names and
namespaces, labels/selectors, and deterministic ordering. The controller adds
the HyperFleetConfig controller owner reference and applies objects with
server-side apply and force ownership.
Conditions is also cluster-I/O-free. internal/apply.Objects refreshes the
rendered objects in place after applying them, so this method must derive health
from the supplied applied slice. It should return the component-level
Available and Progressing observations needed by the controller's rollup and
must use the published reason constants. Controller errors and missing Secret
references are rolled into Degraded centrally.
Rollout tells the controller which ConfigMaps feed the component's config-hash
annotation and which Deployment carries it. A change to one of those ConfigMaps
therefore rolls only the component that owns it.
ReferencedSecrets returns a list of the Secrets that this component's pods
consume. The controller computes a hash from them and writes it as an annotation
in the Deployment's pod template (spec.template.metadata.annotations). When a
Secret rotates, only the affected component's pods roll. If Secrets are missing,
the missing Secrets are listed in the Degraded condition.
Decide whether the operand is:
- Shared tier: present for every usable bundle and registered by
sharedTier. - Bundle-specific: selected only for one or more bundle values and
registered by
bundleSpecific.
Do not expose internal machinery in HyperFleetConfig merely because a new
component needs configuration. The CR contains durable partner intent, not
implementation layout. Any new field requires an API compatibility review,
validation/defaulting tests, regenerated CRDs, sample and reference updates,
and an explanation of why an operator-internal default is insufficient.
Add a focused package under internal/component/<name>. Follow the API
component's separation between component construction/health and manifest
rendering when useful. The constructor should accept only resolved inputs it
needs, such as image, namespace, or operator-derived configuration.
Add unit tests for at least:
- stable component name and default image behavior;
- the complete rendered object set and TypeMeta;
- labels, selectors, service accounts, volumes, ports, probes, resources, and security context relevant to the workload;
- optional and invalid configuration paths;
- missing, unavailable, partially ready, rolling, and stable status states;
- deterministic rendering.
Add constructor inputs to internal/bundle.Config, wire them from
HyperFleetConfigReconciler, and populate them in cmd/main.go where
appropriate. Register the component in sharedTier or bundleSpecific in its
dependency-safe order. Update resolver tests for every bundle value, component
order, constructor error, and incomplete component configuration. A bundle
definition must never silently resolve to a nonfunctional component set.
If the component introduces a Kubernetes kind not already returned by
controller.NamespacedOperandTypes, add a fresh typed object there. That list
drives both controller .Owns(...) watches and cache scoping; adding RBAC alone
is insufficient.
Add or adjust kubebuilder RBAC markers in the controller and run
make manifests. Confirm the generated cluster role, namespaced operand role,
and role bindings grant only required verbs. Update tests that assert watched
types, cache configuration, or RBAC parity. Cluster-scoped operand kinds need an
explicit architecture and ownership review rather than being inserted into the
namespaced list.
A component opts into config-driven rollouts through two methods. The
controller's stampConfigHash does the rest, driven entirely by what the
component returns; no component names live in the controller.
Rollout()tells the controller which ConfigMaps feed the component's config-hash annotation and which Deployment carries it, so a ConfigMap change rolls only the component that owns it. It returns acomponent.RolloutTarget: oneConfigEntry(ConfigMap name and data key) for every ConfigMap the component's pods consume, plus the name of the Deployment to stamp. The controller hashes each entry's rendered data and writes the result to that Deployment's pod-template annotationhyperfleet.redhat.com/config-hash. A ConfigMap left out of the entries can change without restarting the pods.ReferencedSecrets()returns acomponent.SecretReference(a role and the Secret name) for every Secret the pods consume, including conditional ones. The controller folds each Secret'sresourceVersioninto that component's own hash, so a rotation rolls only the components that reference it.
Constraints:
- A missing Secret must stay distinguishable from a present but empty one.
computeConfigHashhandles this; do not bypass it with a custom hash. - Hash only the
resourceVersion. Never copy Secret values into hashes. - Cover the new component with tests: a change to each of its ConfigMaps rolls
only that component, a Secret rotation rolls only the components that
reference it, and a missing Secret is hashed as absent and reported as
Degraded.
TestStampConfigHash*inhyperfleetconfig_rollout_test.goand the rollout specs inhyperfleetconfig_controller_test.goare the models to follow.
Use current Deployment generation, desired/updated/available replica counts, and old replicas to distinguish unavailable, partially ready, progressing, and complete rollout states. If a component is not Deployment-backed, define an equivalent, testable readiness contract before implementation.
Prefer the existing operator-level condition types and reasons. If a genuinely
new published reason is necessary, add a constant, update every writer and
rollup test, and update the
HyperFleetConfig reference
in the same change.
Ensure operator-level reconcile duration and error metrics retain the standard
component="operator" label. Operand readiness and rollout metrics carry the
stable component name in their operand label, using the same identity exposed
as app.kubernetes.io/component on rendered operand objects. Ensure the
applied-configuration hash includes relevant rendered and referenced inputs.
Update the metrics reference for any new metric or label value
contract. Never place credentials or Secret data in labels.
If the component has a container image, define one stable
RELATED_IMAGE_HYPERFLEET_<COMPONENT> environment variable and trace it all the
way through:
- Component default and environment-variable constant.
cmd/main.go, reconciler fields, andbundle.Config.- Makefile default/export and image/build targets.
config/manager/kustomization.yaml.config/manifests/dev/patch-images.yaml.config/manifests/prod/kustomization.yamland the generated CSV'sspec.relatedImages.- Unit/e2e expectations and disconnected-delivery documentation where the release workflow changes.
The dev and prod overlays currently use positional JSON-patch paths for manager environment variables. Inserting or reordering entries can patch the wrong array item while still producing valid YAML. Render and inspect both overlays; do not validate only the base.
For production, related images must be digest-pinned and must match every
operand image and every RELATED_IMAGE_ value in the CSV. The validator under
validators/related-images enforces that relationship. See the
OLM guide for the release command and cross-repository image nudge
workflow.
Run these from the repository root:
make manifests generate
make lint
make test
make test-e2e KIND_CLUSTER=hyperfleet-operator-component-devUse a dedicated e2e cluster name; the successful target deletes it, while a failed Go test leaves it behind for diagnosis. See the developer guide.
Render the manager and production OLM overlay with the repository-pinned kustomize tool and inspect all images, environment variables, RBAC objects, and CSV related images:
go tool -modfile=tools/go.mod kustomize build config/manager
go tool -modfile=tools/go.mod kustomize build config/manifests/prodThe development OLM overlay is intentionally generated and git-ignored; a fresh
checkout contains only config/manifests/dev/patch-images.yaml. Materialize the
same kustomization.yaml that make bundle-build uses before rendering it:
export RELATED_IMAGE_HYPERFLEET_API=<api-image>
export RELATED_IMAGE_HYPERFLEET_OPERATOR=<operator-image>
export RELATED_IMAGE_HYPERFLEET_SENTINEL=<sentinel-image>
envsubst < config/manifests/dev/patch-images.yaml > config/manifests/dev/kustomization.yaml
go tool -modfile=tools/go.mod kustomize build config/manifests/devThe production overlay may require the digest-valued substitutions documented in the OLM guide. A successful YAML render alone does not prove its image contract is valid.
Before requesting review, verify the change as one complete vertical slice:
- The component has a single responsibility and obeys the pure render/health contract.
- Every supported bundle resolves to the intended ordered set, and incomplete bundles fail explicitly.
- New kinds are watched, cached, owned, and authorized consistently.
- Apply behavior, garbage collection, immutable-field handling, and rollout triggers are tested.
- Status rollup stays correct with mixed healthy, progressing, and failed components.
- Image values have one unbroken path from build input to rendered operand and
OLM
relatedImages. - Development, production, connected, and disconnected render paths are not accidentally coupled to local defaults.
- CRD, RBAC, deep-copy, samples, documentation, and generated OLM output agree.
- Unit, envtest, lint, and dedicated-cluster e2e checks pass.
- A reviewer who did not implement the component can use the docs and tests to explain its registration, lifecycle, health, and release impact.