-
Notifications
You must be signed in to change notification settings - Fork 4
HYPERFLEET-1406 - feat: implement the bundle controller and reconcile the API component #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,30 @@ kind: ClusterRole | |
| metadata: | ||
| name: manager-role | ||
| rules: | ||
| - apiGroups: | ||
| - "" | ||
| resources: | ||
| - configmaps | ||
| - serviceaccounts | ||
| - services | ||
| verbs: | ||
| - create | ||
| - get | ||
| - list | ||
| - patch | ||
| - update | ||
| - watch | ||
| - apiGroups: | ||
| - apps | ||
| resources: | ||
| - deployments | ||
| verbs: | ||
| - create | ||
| - get | ||
| - list | ||
| - patch | ||
| - update | ||
| - watch | ||
|
Comment on lines
+7
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift Scope operand permissions to the operator namespace. These rules live in a Generate a namespaced Also applies to: 57-68 🤖 Prompt for AI Agents |
||
| - apiGroups: | ||
| - hyperfleet.redhat.com | ||
| resources: | ||
|
|
@@ -30,3 +54,15 @@ rules: | |
| - get | ||
| - patch | ||
| - update | ||
| - apiGroups: | ||
| - rbac.authorization.k8s.io | ||
| resources: | ||
| - rolebindings | ||
| - roles | ||
| verbs: | ||
| - create | ||
| - get | ||
| - list | ||
| - patch | ||
| - update | ||
| - watch | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /* | ||
| Copyright 2026. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| // Package apply centralizes the idempotent server-side-apply upsert used to | ||
| // reconcile operands: stamp a controller owner reference, then apply. | ||
| package apply | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "k8s.io/apimachinery/pkg/runtime" | ||
| ctrl "sigs.k8s.io/controller-runtime" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
|
||
| hyperfleetv1alpha1 "github.com/openshift-hyperfleet/hyperfleet-operator/api/v1alpha1" | ||
| ) | ||
|
|
||
| // FieldManager is the server-side-apply field owner stamped on every operand the | ||
| // operator manages. It lets the API server distinguish operator-owned fields from | ||
| // any a human sets, which is what makes ForceOwnership drift-correction safe: the | ||
| // operator is the sole intended manager of these fields. | ||
| const FieldManager = "hyperfleet-operator" | ||
|
|
||
| // Objects upserts each desired operand via server-side apply. For every object it: | ||
| // | ||
| // 1. sets a controller owner reference back to the CR, so the object is | ||
| // garbage-collected with the CR and wakes the controller via its Owns() watch | ||
| // when it drifts (a cluster-scoped owner owning namespaced dependents is | ||
| // valid — the owner has no namespace); | ||
| // 2. applies the object with a fixed field manager and ForceOwnership, so | ||
| // re-applying identical state is a no-op (idempotent) while out-of-band edits | ||
| // to operator-owned fields are reclaimed. | ||
| // | ||
| // Each object must carry its GVK (TypeMeta) — server-side apply requires it. | ||
| // Errors are wrapped with the object's kind and name so the caller can see which | ||
| // operand failed. | ||
| func Objects( | ||
| ctx context.Context, | ||
| c client.Client, | ||
| owner *hyperfleetv1alpha1.HyperFleetConfig, | ||
| scheme *runtime.Scheme, | ||
| objs []client.Object, | ||
| ) error { | ||
| for _, obj := range objs { | ||
| kind := obj.GetObjectKind().GroupVersionKind().Kind | ||
| if err := ctrl.SetControllerReference(owner, obj, scheme); err != nil { | ||
| return fmt.Errorf("set controller reference on %s %q: %w", kind, obj.GetName(), err) | ||
| } | ||
| if err := c.Patch(ctx, obj, client.Apply, client.FieldOwner(FieldManager), client.ForceOwnership); err != nil { | ||
| return fmt.Errorf("apply %s %q: %w", kind, obj.GetName(), err) | ||
| } | ||
| } | ||
| return nil | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| /* | ||
| Copyright 2026. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| // Package bundle defines the component contract and resolves a bundle to its | ||
| // ordered component set. It is the in-operator "bundle definition": adding a | ||
| // component later is one new entry here plus its own package — never a new | ||
| // controller. | ||
| package bundle | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
|
||
| hyperfleetv1alpha1 "github.com/openshift-hyperfleet/hyperfleet-operator/api/v1alpha1" | ||
| "github.com/openshift-hyperfleet/hyperfleet-operator/internal/component/api" | ||
| ) | ||
|
|
||
| // Component is the contract every component satisfies. It is deliberately tiny — | ||
| // render the desired objects, report health — with no extension points until a | ||
| // second component exists to justify them. | ||
| // | ||
| // - Render is a pure function (CR → desired objects); it must not read or write | ||
| // the cluster. The controller applies what it returns. | ||
| // - Conditions reports component health. It is consumed starting in | ||
| // HYPERFLEET-1409; until then the controller does not roll it into status. | ||
| type Component interface { | ||
| Name() string | ||
| Render(ctx context.Context, cr *hyperfleetv1alpha1.HyperFleetConfig) ([]client.Object, error) | ||
| Conditions(ctx context.Context, cr *hyperfleetv1alpha1.HyperFleetConfig) ([]metav1.Condition, error) | ||
| } | ||
|
|
||
| // Config carries the inputs the resolver needs to construct components. | ||
| type Config struct { | ||
| // APIImage is the image for the API component (empty → its compiled-in default). | ||
| APIImage string | ||
| // Namespace is the operator's own namespace, where operands are created. | ||
| Namespace string | ||
| } | ||
|
|
||
| // sharedTier lists the components present in every bundle regardless of flavor. | ||
| // In phase 1 this is exactly [API], so every bundle resolves to [API]. | ||
| func sharedTier(cfg Config) []Component { | ||
| return []Component{ | ||
| api.New(cfg.APIImage, cfg.Namespace), | ||
| } | ||
| } | ||
|
|
||
| // bundleSpecific returns the components unique to a bundle beyond the shared | ||
| // tier. Phase 1 has none for either bundle; this is the single extension point | ||
| // where a future bundle-specific component is registered. | ||
| func bundleSpecific(_ hyperfleetv1alpha1.BundleType) []Component { | ||
| return nil | ||
| } | ||
|
|
||
| // Resolve maps a bundle to its ordered component set: the shared tier followed by | ||
| // any bundle-specific components. The shared tier is first so its components | ||
| // (currently the API) reconcile before anything that might depend on them. | ||
| func Resolve(b hyperfleetv1alpha1.BundleType, cfg Config) []Component { | ||
| return append(sharedTier(cfg), bundleSpecific(b)...) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| /* | ||
| Copyright 2026. | ||
|
|
||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
|
|
||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
|
|
||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| */ | ||
|
|
||
| package api | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
|
|
||
| hyperfleetv1alpha1 "github.com/openshift-hyperfleet/hyperfleet-operator/api/v1alpha1" | ||
| ) | ||
|
|
||
| // DefaultImage is the compiled-in fallback image used when the operator is not | ||
| // given RELATED_IMAGE_HYPERFLEET_API. Production deployments override it with a | ||
| // digest-pinned image via that env var (OLM relatedImages convention). | ||
| const DefaultImage = "quay.io/openshift-hyperfleet/hyperfleet-api:latest" | ||
|
Comment on lines
+28
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Require a digest-pinned API image in every configuration path. The compiled fallback is 📍 Affects 3 files
🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| // Component renders the HyperFleet API operand. It satisfies the bundle.Component | ||
| // contract structurally (no import of internal/bundle, avoiding an import cycle: | ||
| // bundle imports this package). It carries only rendering inputs — the Image and | ||
| // the target Namespace — and never touches the cluster; the controller applies | ||
| // what Render produces. | ||
| type Component struct { | ||
| // Image is the API container image. Empty falls back to DefaultImage. | ||
| Image string | ||
| // Namespace is the operator's own namespace, where all operands live. | ||
| Namespace string | ||
| } | ||
|
|
||
| // New constructs the API component for the given image and operator namespace. | ||
| func New(image, namespace string) *Component { | ||
| return &Component{Image: image, Namespace: namespace} | ||
| } | ||
|
|
||
| // Name identifies the component in logs and the app.kubernetes.io/component label. | ||
| func (c *Component) Name() string { | ||
| return ComponentName | ||
| } | ||
|
|
||
| // Render returns the full desired-state operand set for the API, in a | ||
| // dependency-friendly order (identity and RBAC before the workload that uses | ||
| // them). It is a pure function of its inputs: no cluster reads or writes. | ||
| // | ||
| // The returned objects are structurally complete but not yet configured from the | ||
| // CR spec — database/auth/tls env, config-file content and profile→resources are | ||
| // wired in HYPERFLEET-1408. The ctx is part of the contract (later components may | ||
| // need it) but is unused here. | ||
| func (c *Component) Render(_ context.Context, cr *hyperfleetv1alpha1.HyperFleetConfig) ([]client.Object, error) { | ||
| image := c.Image | ||
| if image == "" { | ||
| image = DefaultImage | ||
| } | ||
|
|
||
| return []client.Object{ | ||
| serviceAccount(cr, c.Namespace), | ||
| role(cr, c.Namespace), | ||
| roleBinding(cr, c.Namespace), | ||
| configMap(cr, c.Namespace), | ||
| service(cr, c.Namespace), | ||
| deployment(cr, image, c.Namespace), | ||
| }, nil | ||
| } | ||
|
|
||
| // Conditions reports the component's health as metav1.Conditions. The contract is | ||
| // defined now (HYPERFLEET-1407) so it need not be reopened next story, but its | ||
| // output is not yet rolled up into status.conditions — real health derivation and | ||
| // status wiring land in HYPERFLEET-1409. Returning nil until then keeps the | ||
| // reconciler from writing status prematurely. | ||
| func (c *Component) Conditions(_ context.Context, _ *hyperfleetv1alpha1.HyperFleetConfig) ([]metav1.Condition, error) { | ||
| return nil, nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not fall back to a hardcoded namespace when running in-cluster.
POD_NAMESPACEis optional here. If the downward-API entry is dropped or renamed in a deployment overlay, the operator silently targetshyperfleet-operator-system. The ClusterRole grants operand write verbs cluster-wide, so the operator then creates the API Deployment, Service, Role, and RoleBinding in a namespace it does not run in. That is a misconfiguration that no runtime error reports.Gate the fallback on running outside a cluster. Read the service-account namespace file first, and exit non-zero if the process runs in-cluster with no namespace resolved. The hardcoded value is also duplicated in
internal/controller/hyperfleetconfig_controller_test.goat Line 68; move it to a shared constant.Suggested fail-fast wiring
operatorNamespace := os.Getenv("POD_NAMESPACE") if operatorNamespace == "" { + const saNamespaceFile = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" + if b, readErr := os.ReadFile(saNamespaceFile); readErr == nil { + setupLog.Error(nil, "POD_NAMESPACE is not set but the operator runs in-cluster; fix the downward API env", + "serviceAccountNamespace", strings.TrimSpace(string(b))) + os.Exit(1) + } operatorNamespace = "hyperfleet-operator-system" setupLog.Info("POD_NAMESPACE not set; falling back to default operator namespace", "namespace", operatorNamespace) }As per path instructions: "Configuration validation at startup (fail-fast)".
🤖 Prompt for AI Agents
Source: Path instructions