-
Notifications
You must be signed in to change notification settings - Fork 6
HYPERFLEET-1408 - feat: map CR fields to API config, trigger rollout on change #6
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,14 @@ rules: | |
| - patch | ||
| - update | ||
| - watch | ||
| - apiGroups: | ||
| - "" | ||
| resources: | ||
| - secrets | ||
| verbs: | ||
| - get | ||
| - list | ||
| - watch | ||
|
Comment on lines
+20
to
+27
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 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- role.yaml ---'
cat -n config/rbac/role.yaml | sed -n '1,80p'
printf '%s\n' '--- RBAC bindings and kustomize references ---'
rg -n -C 3 'manager-role|ClusterRoleBinding|RoleBinding|roleRef|serviceAccountName|OperatorNamespace|cache|Secrets|secrets' config controllers internal api --glob '!**/zz_generated.*' || true
printf '%s\n' '--- relevant file map ---'
git ls-files 'config/**' 'controllers/**' 'internal/**' 'api/**' | sed -n '1,160p'Repository: openshift-hyperfleet/hyperfleet-operator Length of output: 25384 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- manager binding ---'
cat -n config/rbac/role_binding.yaml
printf '%s\n' '--- namespace and manager cache setup ---'
rg -n -C 4 'OPERATOR_NAMESPACE|NewCache|DefaultNamespaces|Namespace:|cache\.Options|ctrl\.NewManager|ClusterRoleBinding' main.go cmd internal config
printf '%s\n' '--- RBAC kustomization and deployment namespace wiring ---'
cat -n config/rbac/kustomization.yaml
cat -n config/manager/kustomization.yaml
cat -n config/manager/manager.yaml | sed -n '1,125p'Repository: openshift-hyperfleet/hyperfleet-operator Length of output: 19926 Scope Secret permissions to the operator namespace.
🤖 Prompt for AI Agents |
||
| - apiGroups: | ||
| - apps | ||
| resources: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,13 +50,71 @@ type Config struct { | |
| APIImage string | ||
| // Namespace is the operator's own namespace, where operands are created. | ||
| Namespace string | ||
| // ResolvedJWKSURL is the JWKS URL the controller derived via OIDC discovery. | ||
| // It is threaded through to the API component, which needs it only when auth | ||
| // is enabled and the CR pins neither a JWKS URL nor a JWKS Secret. Empty | ||
| // otherwise (the component reads the CR field/Secret path directly). | ||
| ResolvedJWKSURL string | ||
| } | ||
|
|
||
| // cloudCAPIEntities is the entity registration set for the cloud-capi bundle. | ||
| // It is lifted verbatim from the HyperFleet API's shipped | ||
| // configs/config.yaml.example so the operator-rendered config.yaml registers the | ||
| // same resource types the API expects for this deployment flavor. Keep it in | ||
| // sync with the API's example if the entity set changes. | ||
| var cloudCAPIEntities = []api.EntityDescriptor{ | ||
| { | ||
| Kind: "Cluster", | ||
| Plural: "clusters", | ||
| SpecSchemaName: "ClusterSpec", | ||
| RequiredAdapters: []string{"validation", "dns", "pullsecret", "hypershift"}, | ||
| NameMinLen: 3, | ||
| NameMaxLen: 53, | ||
| RequireSpecSchema: true, | ||
| }, | ||
| { | ||
| Kind: "NodePool", | ||
| Plural: "nodepools", | ||
| ParentKind: "Cluster", | ||
| OnParentDelete: "cascade", | ||
| SpecSchemaName: "NodePoolSpec", | ||
| RequiredAdapters: []string{"validation", "hypershift"}, | ||
| NameMinLen: 3, | ||
| NameMaxLen: 15, | ||
| RequireSpecSchema: true, | ||
| }, | ||
| {Kind: "Channel", Plural: "channels", SpecSchemaName: "ChannelSpec"}, | ||
| {Kind: "Version", Plural: "versions", ParentKind: "Channel", OnParentDelete: "restrict", SpecSchemaName: "VersionSpec"}, | ||
| {Kind: "WifConfig", Plural: "wifconfigs", SpecSchemaName: "WifConfigSpec"}, | ||
| } | ||
|
|
||
| // entitiesForBundle returns the entity registration set for a bundle. | ||
| func entitiesForBundle(b hyperfleetv1alpha1.BundleType) []api.EntityDescriptor { | ||
| switch b { | ||
| case hyperfleetv1alpha1.BundleCloudCAPI: | ||
| return cloudCAPIEntities | ||
| case hyperfleetv1alpha1.BundleOnPremAgent: | ||
|
Contributor
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. The comment is honest about what happens, but the CRD enum still accepts |
||
| // Intentionally empty: the on-prem/agent bundle's entity set is not yet | ||
| // defined. Leaving it nil renders no `entities:` key, and the API then | ||
| // registers NO entity types at all (LoadDescriptors ranges over the slice; | ||
| // there is no built-in default set), so it serves zero resource routes — it | ||
| // does NOT fall back to cloud-capi or any default entities. The on-prem | ||
| // bundle must supply an explicit entity set here before it is usable. | ||
| return nil | ||
|
Comment on lines
+96
to
+103
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. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Restore API entities for
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Linked repositories |
||
| default: | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // 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 { | ||
| // In phase 1 this is exactly [API], so every bundle resolves to [API]. It takes | ||
| // the bundle so the API component can be given the bundle-specific entity set. | ||
| func sharedTier(b hyperfleetv1alpha1.BundleType, cfg Config) []Component { | ||
| return []Component{ | ||
| api.New(cfg.APIImage, cfg.Namespace), | ||
| api.New(cfg.APIImage, cfg.Namespace, api.Options{ | ||
| Entities: entitiesForBundle(b), | ||
| ResolvedJWKSURL: cfg.ResolvedJWKSURL, | ||
| }), | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -71,5 +129,5 @@ func bundleSpecific(_ hyperfleetv1alpha1.BundleType) []Component { | |
| // 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)...) | ||
| return append(sharedTier(b, cfg), bundleSpecific(b)...) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| /* | ||
| 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 | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| . "github.com/onsi/gomega" | ||
|
|
||
| hyperfleetv1alpha1 "github.com/openshift-hyperfleet/hyperfleet-operator/api/v1alpha1" | ||
| "github.com/openshift-hyperfleet/hyperfleet-operator/internal/component/api" | ||
| ) | ||
|
|
||
| func TestEntitiesForBundleCloudCAPI(t *testing.T) { | ||
| g := NewWithT(t) | ||
|
|
||
| ents := entitiesForBundle(hyperfleetv1alpha1.BundleCloudCAPI) | ||
| g.Expect(ents).To(Equal(cloudCAPIEntities)) | ||
| g.Expect(ents).NotTo(BeEmpty()) | ||
|
|
||
| // Sanity: the cloud-capi set registers the core entities the API expects for | ||
| // this flavor. | ||
| kinds := map[string]bool{} | ||
| for _, e := range ents { | ||
| kinds[e.Kind] = true | ||
| } | ||
| g.Expect(kinds).To(HaveKey("Cluster")) | ||
| g.Expect(kinds).To(HaveKey("NodePool")) | ||
| } | ||
|
|
||
| func TestEntitiesForBundleOnPremAgentIsEmpty(t *testing.T) { | ||
| g := NewWithT(t) | ||
|
|
||
| // The on-prem/agent bundle has no entity set yet: it must be nil (renders no | ||
| // entities: key, so the API registers zero entity types), NOT the cloud-capi | ||
| // set. This pins the contract the corrected comment describes. | ||
| g.Expect(entitiesForBundle(hyperfleetv1alpha1.BundleOnPremAgent)).To(BeNil()) | ||
| } | ||
|
|
||
| func TestEntitiesForBundleUnknownIsEmpty(t *testing.T) { | ||
| g := NewWithT(t) | ||
|
|
||
| // An unrecognized bundle falls through the switch default and registers no | ||
| // entities rather than silently defaulting to cloud-capi. | ||
| g.Expect(entitiesForBundle(hyperfleetv1alpha1.BundleType("does-not-exist"))).To(BeNil()) | ||
| } | ||
|
|
||
| func TestResolveWiresSharedTierAPIComponent(t *testing.T) { | ||
| g := NewWithT(t) | ||
|
|
||
| const jwks = "https://issuer.example.com/keys" | ||
| comps := Resolve(hyperfleetv1alpha1.BundleCloudCAPI, Config{ | ||
| APIImage: "example.com/api:test", | ||
| Namespace: "hyperfleet-system", | ||
| ResolvedJWKSURL: jwks, | ||
| }) | ||
|
|
||
| // Phase 1: every bundle resolves to exactly [API]. | ||
| g.Expect(comps).To(HaveLen(1)) | ||
|
|
||
| comp, ok := comps[0].(*api.Component) | ||
| g.Expect(ok).To(BeTrue()) | ||
| g.Expect(comp.Image).To(Equal("example.com/api:test")) | ||
| g.Expect(comp.Namespace).To(Equal("hyperfleet-system")) | ||
| g.Expect(comp.ResolvedJWKSURL).To(Equal(jwks)) | ||
| g.Expect(comp.Entities).To(Equal(cloudCAPIEntities)) | ||
| } | ||
|
|
||
| func TestResolveOnPremAgentHasNoEntities(t *testing.T) { | ||
| g := NewWithT(t) | ||
|
|
||
| comps := Resolve(hyperfleetv1alpha1.BundleOnPremAgent, Config{Namespace: "ns"}) | ||
| g.Expect(comps).To(HaveLen(1)) | ||
|
|
||
| comp, ok := comps[0].(*api.Component) | ||
| g.Expect(ok).To(BeTrue()) | ||
| g.Expect(comp.Entities).To(BeEmpty()) | ||
| } |
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.
Just want to confirm the thinking on
jwkCertURLas a CR field.jwkCertSecretRefI buy, air-gapped is real partner intent. When does a partner have an OIDC issuer that doesn't serve.well-known? Reading the tests, its main job is letting fixtures skip the network call.Every field here is a forever contract (ADR-0019 is pretty explicit about keeping the CR minimal), and per ADR-0020 the gateway owns JWT validation, in-app is defense-in-depth, so this grows the contract to configure the fallback layer. Adding later is compatible, removing isn't. Could we keep discovery + Secret as the two paths for v1alpha1 and leave the URL out until someone actually needs it? If it stays, the CA-file question from the discovery comment will show up as a third field pretty quickly.