@@ -3113,134 +3113,201 @@ func TestGenerate_SourceOnlyRefChartFallsBackToName(t *testing.T) {
31133113// base.yaml and all ship the NodeFeature CRDs, so replacing unconditionally
31143114// would have several HelmReleases rewrite the same CRD every reconcile.
31153115//
3116- // Three directions are asserted, and each fails for a different reason:
3116+ // Refs are built by running ApplyRegistryDefaults over an empty ref, which is
3117+ // what a stock recipe actually resolves to. Constructing them by copying
3118+ // registry fields directly is what an earlier revision did, and it silently
3119+ // diverged: ApplyRegistryDefaults strips a defaultChart to its bare name, so a
3120+ // hand-built ref carrying "gatekeeper/gatekeeper" never matched the real
3121+ // resolved "gatekeeper" and the test could not see that gatekeeper emitted no
3122+ // policy at all. Do not hand-build these refs.
31173123//
3118- // owner -> emits the policy
3119- // sharer -> must not, because replacing would race another release
3120- // coordinates moved -> must not, because ownsCRDs records an audit of the
3121- // registry's pinned chart and a ref that overrides
3122- // source/chart/version points somewhere unaudited
3123- //
3124- // A positive-only test would pass against an unconditional template, which is
3125- // the defect this replaced, so the negative cases are the load-bearing ones.
3124+ // The owner cases are discovered from the registry rather than hardcoded, so a
3125+ // component enrolled in ownsCRDs later is covered without touching this test.
31263126//
31273127// NOTE ON COVERAGE: this exercises the sourceRef template only. An upstream
31283128// Helm component renders spec.chart.spec.sourceRef regardless of
3129- // OCISourceName; the chartRef template is reached by local, vendored and
3130- // manifest-backed components, and those carry no registry chart coordinates,
3131- // so they cannot satisfy usesRegistryChart and cannot emit the policy today.
3132- // Earlier revisions of this test claimed chartRef coverage by setting
3133- // OCISourceName on a normal Helm ref, which renders sourceRef and tested the
3134- // same path twice. Do not reintroduce that shape.
3129+ // OCISourceName. TestUsesRegistryChartRejectsRefsWithoutCoordinates covers
3130+ // why the chartRef shape cannot emit the policy today.
31353131func TestGenerate_HelmReleaseCRDUpgradePolicyIsOptIn (t * testing.T ) {
31363132 registry , regErr := recipe .GetComponentRegistry ()
31373133 if regErr != nil {
31383134 t .Fatalf ("GetComponentRegistry: %v" , regErr )
31393135 }
31403136
3141- // refFor builds a ref at the component's exact registry coordinates, which
3142- // is what a stock recipe resolves to .
3143- refFor := func (t * testing.T , name string ) recipe.ComponentRef {
3137+ // resolvedRef mirrors stock resolution: an empty ref, then registry
3138+ // defaults. Anything else risks testing a shape the resolver never emits .
3139+ resolvedRef := func (t * testing.T , name string ) recipe.ComponentRef {
31443140 t .Helper ()
31453141 cfg := registry .Get (name )
31463142 if cfg == nil {
31473143 t .Fatalf ("%s missing from registry" , name )
31483144 }
3149- return recipe.ComponentRef {
3150- Name : name ,
3151- Namespace : name ,
3152- Type : recipe .ComponentTypeHelm ,
3153- Source : cfg .Helm .DefaultRepository ,
3154- Chart : cfg .Helm .DefaultChart ,
3155- Version : cfg .Helm .DefaultVersion ,
3145+ ref := recipe.ComponentRef {Name : name , Namespace : name , Type : recipe .ComponentTypeHelm }
3146+ ref .ApplyRegistryDefaults (cfg )
3147+ return ref
3148+ }
3149+
3150+ // Every enrolled owner, discovered rather than listed.
3151+ var owners []string
3152+ for _ , name := range registry .Names () {
3153+ if cfg := registry .Get (name ); cfg != nil && cfg .OwnsCRDs {
3154+ owners = append (owners , name )
3155+ }
3156+ }
3157+ if len (owners ) == 0 {
3158+ t .Fatal ("no ownsCRDs components in the registry; this test would prove nothing" )
3159+ }
3160+
3161+ t .Run ("owners emit the policy" , func (t * testing.T ) {
3162+ for _ , name := range owners {
3163+ t .Run (name , func (t * testing.T ) {
3164+ assertCRDPolicy (t , resolvedRef (t , name ), name , "CreateReplace" )
3165+ })
31563166 }
3167+ })
3168+
3169+ // nfd shares the NodeFeature CRDs with gpu-operator and network-operator,
3170+ // all three of which co-exist in base.yaml.
3171+ t .Run ("sharer does not" , func (t * testing.T ) {
3172+ assertCRDPolicy (t , resolvedRef (t , "nfd" ), "nfd" , "" )
3173+ })
3174+
3175+ // ownsCRDs records an audit of the registry's pinned chart. A ref pointing
3176+ // anywhere else is unaudited, so the policy must not carry over.
3177+ t .Run ("coordinate overrides disable it" , func (t * testing.T ) {
3178+ owner := owners [0 ]
3179+ for _ , tc := range []struct {
3180+ name string
3181+ mutate func (* recipe.ComponentRef )
3182+ }{
3183+ {"version" , func (r * recipe.ComponentRef ) { r .Version = "0.0.1-unaudited" }},
3184+ {"chart" , func (r * recipe.ComponentRef ) { r .Chart = "some-fork" }},
3185+ {"source" , func (r * recipe.ComponentRef ) { r .Source = "oci://example.invalid/charts" }},
3186+ } {
3187+ t .Run (tc .name , func (t * testing.T ) {
3188+ ref := resolvedRef (t , owner )
3189+ tc .mutate (& ref )
3190+ assertCRDPolicy (t , ref , owner , "" )
3191+ })
3192+ }
3193+ })
3194+ }
3195+
3196+ // assertCRDPolicy renders one component and asserts spec.upgrade.crds.
3197+ //
3198+ // Decoded as a map, not a typed struct: a string field cannot tell an absent
3199+ // crds key from one explicitly rendered as "", so a negative case would pass
3200+ // against a template emitting an empty value. Presence is the assertion.
3201+ func assertCRDPolicy (t * testing.T , ref recipe.ComponentRef , component , want string ) {
3202+ t .Helper ()
3203+ outputDir := t .TempDir ()
3204+
3205+ recipeResult := & recipe.RecipeResult {}
3206+ recipeResult .Metadata .Version = testVersion
3207+ recipeResult .ComponentRefs = []recipe.ComponentRef {ref }
3208+
3209+ g := & Generator {
3210+ RecipeResult : recipeResult ,
3211+ ComponentValues : map [string ]map [string ]any {component : {}},
3212+ Version : "v0.9.0" ,
3213+ }
3214+ if _ , err := g .Generate (context .Background (), outputDir ); err != nil {
3215+ t .Fatalf ("Generate() error = %v" , err )
3216+ }
3217+
3218+ raw := readFile (t , filepath .Join (outputDir , component , "helmrelease.yaml" ))
3219+ var doc struct {
3220+ Spec map [string ]any `yaml:"spec"`
3221+ }
3222+ if err := yaml .Unmarshal ([]byte (raw ), & doc ); err != nil {
3223+ t .Fatalf ("parse HelmRelease: %v" , err )
3224+ }
3225+ upgrade , hasUpgrade := doc .Spec ["upgrade" ]
3226+
3227+ if want == "" {
3228+ if hasUpgrade {
3229+ t .Errorf ("spec.upgrade present (%v), want the key absent entirely\n %s" , upgrade , raw )
3230+ }
3231+ return
3232+ }
3233+ if ! hasUpgrade {
3234+ t .Fatalf ("spec.upgrade absent, want crds = %q\n %s" , want , raw )
3235+ }
3236+ upgradeMap , ok := upgrade .(map [string ]any )
3237+ if ! ok {
3238+ t .Fatalf ("spec.upgrade is %T, want a mapping\n %s" , upgrade , raw )
3239+ }
3240+ crds , hasCRDs := upgradeMap ["crds" ]
3241+ if ! hasCRDs {
3242+ t .Fatalf ("spec.upgrade.crds absent, want %q\n %s" , want , raw )
3243+ }
3244+ if crds != want {
3245+ t .Errorf ("spec.upgrade.crds = %v, want %q\n %s" , crds , want , raw )
3246+ }
3247+ }
3248+
3249+ // TestUsesRegistryChartRejectsRefsWithoutCoordinates pins the property that
3250+ // makes the chartRef template's UpgradeCRDs block unreachable today.
3251+ //
3252+ // chartRef is reached by local, vendored and manifest-backed components. Those
3253+ // carry no registry chart coordinates, so usesRegistryChart is false for them
3254+ // and the policy cannot render. Asserting that directly is preferable to a
3255+ // render test: an attempt at the render route produced a sourceRef HelmRelease
3256+ // instead, so such a test would have passed without exercising anything, which
3257+ // is the ambiguous-condition shape this suite already avoids elsewhere.
3258+ //
3259+ // If a future chartRef path does carry registry coordinates, this test still
3260+ // holds but the block becomes reachable — at which point the chartRef template
3261+ // needs its own render coverage.
3262+ func TestUsesRegistryChartRejectsRefsWithoutCoordinates (t * testing.T ) {
3263+ registry , regErr := recipe .GetComponentRegistry ()
3264+ if regErr != nil {
3265+ t .Fatalf ("GetComponentRegistry: %v" , regErr )
3266+ }
3267+ cfg := registry .Get ("k8s-aibom" )
3268+ if cfg == nil || ! cfg .OwnsCRDs {
3269+ t .Fatal ("k8s-aibom must be an ownsCRDs component for this test to mean anything" )
31573270 }
31583271
31593272 tests := []struct {
3160- name string
3161- component string
3162- mutate func (* recipe.ComponentRef )
3163- want string
3273+ name string
3274+ ref recipe.ComponentRef
3275+ want bool
31643276 }{
31653277 {
3166- // k8s-aibom is registry ownsCRDs: sole owner, no webhook conversion.
3167- name : "owner at registry coordinates" , component : "k8s-aibom" , want : "CreateReplace" ,
3168- },
3169- {
3170- // nfd shares the NodeFeature CRDs with gpu-operator and
3171- // network-operator, all three of which co-exist in base.yaml.
3172- name : "sharer" , component : "nfd" , want : "" ,
3173- },
3174- {
3175- name : "owner with overridden version" , component : "k8s-aibom" ,
3176- mutate : func (r * recipe.ComponentRef ) { r .Version = "1.2.0" }, want : "" ,
3278+ name : "manifest-only ref has no chart or source" ,
3279+ ref : recipe.ComponentRef {
3280+ Name : "k8s-aibom" , Type : recipe .ComponentTypeHelm ,
3281+ ManifestFiles : []string {"components/k8s-aibom/values.yaml" },
3282+ },
3283+ want : false ,
31773284 },
31783285 {
3179- name : "owner with overridden chart" , component : "k8s-aibom" ,
3180- mutate : func (r * recipe.ComponentRef ) { r .Chart = "some-fork" }, want : "" ,
3286+ name : "local chart path is not the registry chart" ,
3287+ ref : recipe.ComponentRef {
3288+ Name : "k8s-aibom" , Type : recipe .ComponentTypeHelm ,
3289+ Chart : "./k8s-aibom" , Version : cfg .Helm .DefaultVersion ,
3290+ },
3291+ want : false ,
31813292 },
31823293 {
3183- name : "owner with overridden source" , component : "k8s-aibom" ,
3184- mutate : func (r * recipe.ComponentRef ) { r .Source = "oci://example.invalid/charts" }, want : "" ,
3294+ // The control: without this, every case above could pass because
3295+ // usesRegistryChart always returns false.
3296+ name : "fully resolved registry ref matches" ,
3297+ ref : func () recipe.ComponentRef {
3298+ r := recipe.ComponentRef {Name : "k8s-aibom" , Type : recipe .ComponentTypeHelm }
3299+ r .ApplyRegistryDefaults (cfg )
3300+ return r
3301+ }(),
3302+ want : true ,
31853303 },
31863304 }
31873305
31883306 for _ , tt := range tests {
31893307 t .Run (tt .name , func (t * testing.T ) {
3190- outputDir := t .TempDir ()
3191- ref := refFor (t , tt .component )
3192- if tt .mutate != nil {
3193- tt .mutate (& ref )
3194- }
3195-
3196- recipeResult := & recipe.RecipeResult {}
3197- recipeResult .Metadata .Version = testVersion
3198- recipeResult .ComponentRefs = []recipe.ComponentRef {ref }
3199-
3200- g := & Generator {
3201- RecipeResult : recipeResult ,
3202- ComponentValues : map [string ]map [string ]any {tt .component : {}},
3203- Version : "v0.9.0" ,
3204- }
3205- if _ , err := g .Generate (context .Background (), outputDir ); err != nil {
3206- t .Fatalf ("Generate() error = %v" , err )
3207- }
3208-
3209- raw := readFile (t , filepath .Join (outputDir , tt .component , "helmrelease.yaml" ))
3210-
3211- // Decoded as a map, not a typed struct: a struct field of type
3212- // string cannot tell an absent `crds` key from one explicitly
3213- // rendered as "", so a negative case would pass against a
3214- // template that emits an empty value. Presence is the assertion.
3215- var doc struct {
3216- Spec map [string ]any `yaml:"spec"`
3217- }
3218- if err := yaml .Unmarshal ([]byte (raw ), & doc ); err != nil {
3219- t .Fatalf ("parse HelmRelease: %v" , err )
3220- }
3221- upgrade , hasUpgrade := doc .Spec ["upgrade" ]
3222-
3223- if tt .want == "" {
3224- if hasUpgrade {
3225- t .Errorf ("spec.upgrade present (%v), want the key absent entirely\n %s" ,
3226- upgrade , raw )
3227- }
3228- return
3229- }
3230-
3231- if ! hasUpgrade {
3232- t .Fatalf ("spec.upgrade absent, want crds = %q\n %s" , tt .want , raw )
3233- }
3234- upgradeMap , ok := upgrade .(map [string ]any )
3235- if ! ok {
3236- t .Fatalf ("spec.upgrade is %T, want a mapping\n %s" , upgrade , raw )
3237- }
3238- crds , hasCRDs := upgradeMap ["crds" ]
3239- if ! hasCRDs {
3240- t .Fatalf ("spec.upgrade.crds absent, want %q\n %s" , tt .want , raw )
3241- }
3242- if crds != tt .want {
3243- t .Errorf ("spec.upgrade.crds = %v, want %q\n %s" , crds , tt .want , raw )
3308+ if got := usesRegistryChart (tt .ref , cfg ); got != tt .want {
3309+ t .Errorf ("usesRegistryChart() = %v, want %v (chart=%q source=%q version=%q)" ,
3310+ got , tt .want , tt .ref .EffectiveChart (), tt .ref .Source , tt .ref .Version )
32443311 }
32453312 })
32463313 }
0 commit comments