@@ -4045,6 +4045,350 @@ func TestCVO_VerifyUpdatingPayloadState(t *testing.T) {
40454045 }
40464046}
40474047
4048+ // TestCVO_FeatureGateManifestInclusion tests that manifest inclusion changes dynamically
4049+ // when feature gates are updated, triggering payload refresh and re-filtering of manifests.
4050+ func TestCVO_FeatureGateManifestInclusion (t * testing.T ) {
4051+ o , cvs , client , _ , shutdownFn := setupCVOTest ("testdata/featuregatetest" )
4052+
4053+ ctx , cancel := context .WithCancel (context .Background ())
4054+ defer cancel ()
4055+
4056+ defer shutdownFn ()
4057+ worker := o .configSync .(* SyncWorker )
4058+ go worker .Start (ctx , 1 )
4059+
4060+ // Step 1: Start with no feature gates enabled
4061+ // Expected manifests: always-included (no annotation), legacy-excluded (requires -LegacyFeature, which is not enabled)
4062+ // NOT included: experimental-feature (requires ExperimentalFeature)
4063+ o .release .Image = "image/image:1"
4064+ o .release .Version = "1.0.0-abc"
4065+ desired := configv1.Release {Version : "1.0.0-abc" , Image : "image/image:1" }
4066+ uid , _ := uuid .NewRandom ()
4067+ clusterUID := configv1 .ClusterID (uid .String ())
4068+ cvs ["version" ] = & configv1.ClusterVersion {
4069+ ObjectMeta : metav1.ObjectMeta {
4070+ Name : "version" ,
4071+ },
4072+ Spec : configv1.ClusterVersionSpec {
4073+ ClusterID : clusterUID ,
4074+ Channel : "fast" ,
4075+ },
4076+ }
4077+
4078+ // Sync with no feature gates
4079+ client .ClearActions ()
4080+ err := o .sync (ctx , o .queueKey ())
4081+ if err != nil {
4082+ t .Fatal (err )
4083+ }
4084+
4085+ // Wait for payload to load
4086+ verifyAllStatus (t , worker .StatusCh (),
4087+ SyncWorkerStatus {
4088+ Actual : desired ,
4089+ loadPayloadStatus : LoadPayloadStatus {
4090+ Step : "RetrievePayload" ,
4091+ Message : "Retrieving and verifying payload version=\" 1.0.0-abc\" image=\" image/image:1\" " ,
4092+ LastTransitionTime : time .Unix (1 , 0 ),
4093+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4094+ },
4095+ EnabledFeatureGates : sets .New [string ](),
4096+ },
4097+ SyncWorkerStatus {
4098+ Actual : desired ,
4099+ LastProgress : time .Unix (1 , 0 ),
4100+ loadPayloadStatus : LoadPayloadStatus {
4101+ Step : "PayloadLoaded" ,
4102+ Message : "Payload loaded version=\" 1.0.0-abc\" image=\" image/image:1\" architecture=\" " + architecture + "\" " ,
4103+ LastTransitionTime : time .Unix (2 , 0 ),
4104+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4105+ },
4106+ EnabledFeatureGates : sets .New [string ](),
4107+ },
4108+ SyncWorkerStatus {
4109+ Total : 2 , // only always-included and legacy-excluded
4110+ Initial : true ,
4111+ VersionHash : "YAJ_K7RyH7U=" , Architecture : architecture ,
4112+ Actual : configv1.Release {
4113+ Version : "1.0.0-abc" ,
4114+ Image : "image/image:1" ,
4115+ URL : "https://example.com/v1.0.0-abc" ,
4116+ },
4117+ LastProgress : time .Unix (2 , 0 ),
4118+ loadPayloadStatus : LoadPayloadStatus {
4119+ Step : "PayloadLoaded" ,
4120+ Message : "Payload loaded version=\" 1.0.0-abc\" image=\" image/image:1\" architecture=\" " + architecture + "\" " ,
4121+ LastTransitionTime : time .Unix (3 , 0 ),
4122+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4123+ },
4124+ CapabilitiesStatus : CapabilityStatus {
4125+ Status : configv1.ClusterVersionCapabilitiesStatus {
4126+ EnabledCapabilities : sortedCaps ,
4127+ KnownCapabilities : sortedKnownCaps ,
4128+ },
4129+ },
4130+ EnabledFeatureGates : sets .New [string ](),
4131+ },
4132+ )
4133+
4134+ // Wait for Step 1 to complete
4135+ waitForStatusCompleted (t , worker )
4136+
4137+ // Verify 2 manifests in payload
4138+ if worker .payload == nil {
4139+ t .Fatal ("Expected payload to be loaded" )
4140+ }
4141+ if len (worker .payload .Manifests ) != 2 {
4142+ t .Fatalf ("Expected 2 manifests (without ExperimentalFeature), got %d" , len (worker .payload .Manifests ))
4143+ }
4144+
4145+ // Step 2: Enable ExperimentalFeature gate
4146+ // Expected manifests: always-included, legacy-excluded, AND experimental-feature
4147+
4148+ // Clear any pending status updates from Step 1
4149+ clearAllStatus (t , worker .StatusCh ())
4150+
4151+ o .updateEnabledFeatureGates (& configv1.FeatureGate {
4152+ Status : configv1.FeatureGateStatus {
4153+ FeatureGates : []configv1.FeatureGateDetails {
4154+ {
4155+ Version : "1.0.0-abc" ,
4156+ Enabled : []configv1.FeatureGateAttributes {
4157+ {Name : "ExperimentalFeature" },
4158+ },
4159+ },
4160+ },
4161+ },
4162+ })
4163+
4164+ // Trigger another sync - this should cause payload refresh
4165+ client .ClearActions ()
4166+ err = o .sync (ctx , o .queueKey ())
4167+ if err != nil {
4168+ t .Fatal (err )
4169+ }
4170+
4171+ // Verify feature gates changed and payload was refreshed
4172+ // Note: updateLoadStatus preserves apply status fields (Done, Total, Completed, Initial, VersionHash, LastProgress)
4173+ // from the previous status, so these will have Step 1's completed values
4174+ verifyAllStatus (t , worker .StatusCh (),
4175+ SyncWorkerStatus {
4176+ Done : 2 ,
4177+ Total : 2 ,
4178+ Completed : 1 ,
4179+ Reconciling : true ,
4180+ Initial : false ,
4181+ VersionHash : "YAJ_K7RyH7U=" ,
4182+ Architecture : architecture ,
4183+ Actual : configv1.Release {
4184+ Version : "1.0.0-abc" ,
4185+ Image : "image/image:1" ,
4186+ URL : "https://example.com/v1.0.0-abc" ,
4187+ },
4188+ LastProgress : time .Unix (1 , 0 ),
4189+ loadPayloadStatus : LoadPayloadStatus {
4190+ Step : "RetrievePayload" ,
4191+ Message : "Retrieving and verifying payload version=\" 1.0.0-abc\" image=\" image/image:1\" " ,
4192+ LastTransitionTime : time .Unix (1 , 0 ),
4193+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4194+ },
4195+ CapabilitiesStatus : CapabilityStatus {
4196+ Status : configv1.ClusterVersionCapabilitiesStatus {
4197+ EnabledCapabilities : sortedCaps ,
4198+ KnownCapabilities : sortedKnownCaps ,
4199+ },
4200+ },
4201+ EnabledFeatureGates : sets .New [string ](),
4202+ },
4203+ SyncWorkerStatus {
4204+ Done : 2 ,
4205+ Total : 2 ,
4206+ Completed : 1 ,
4207+ Reconciling : true ,
4208+ Initial : false ,
4209+ VersionHash : "YAJ_K7RyH7U=" ,
4210+ Architecture : architecture ,
4211+ Actual : configv1.Release {
4212+ Version : "1.0.0-abc" ,
4213+ Image : "image/image:1" ,
4214+ URL : "https://example.com/v1.0.0-abc" ,
4215+ },
4216+ LastProgress : time .Unix (2 , 0 ),
4217+ loadPayloadStatus : LoadPayloadStatus {
4218+ Step : "PayloadLoaded" ,
4219+ Message : "Payload loaded version=\" 1.0.0-abc\" image=\" image/image:1\" architecture=\" " + architecture + "\" " ,
4220+ LastTransitionTime : time .Unix (2 , 0 ),
4221+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4222+ },
4223+ CapabilitiesStatus : CapabilityStatus {
4224+ Status : configv1.ClusterVersionCapabilitiesStatus {
4225+ EnabledCapabilities : sortedCaps ,
4226+ KnownCapabilities : sortedKnownCaps ,
4227+ },
4228+ },
4229+ EnabledFeatureGates : sets .New [string ](),
4230+ },
4231+ SyncWorkerStatus {
4232+ Total : 3 , // now includes experimental-feature
4233+ Initial : false ,
4234+ VersionHash : "yrh5CWG1KPI=" , Architecture : architecture ,
4235+ Actual : configv1.Release {
4236+ Version : "1.0.0-abc" ,
4237+ Image : "image/image:1" ,
4238+ URL : "https://example.com/v1.0.0-abc" ,
4239+ },
4240+ LastProgress : time .Unix (3 , 0 ),
4241+ loadPayloadStatus : LoadPayloadStatus {
4242+ Step : "PayloadLoaded" ,
4243+ Message : "Payload loaded version=\" 1.0.0-abc\" image=\" image/image:1\" architecture=\" " + architecture + "\" " ,
4244+ LastTransitionTime : time .Unix (3 , 0 ),
4245+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4246+ },
4247+ CapabilitiesStatus : CapabilityStatus {
4248+ Status : configv1.ClusterVersionCapabilitiesStatus {
4249+ EnabledCapabilities : sortedCaps ,
4250+ KnownCapabilities : sortedKnownCaps ,
4251+ },
4252+ ImplicitlyEnabledCaps : []configv1.ClusterVersionCapability {},
4253+ },
4254+ EnabledFeatureGates : sets.New [string ]("ExperimentalFeature" ),
4255+ },
4256+ )
4257+
4258+ // Wait for Step 2 to complete
4259+ waitForStatusCompleted (t , worker )
4260+
4261+ // Verify 3 manifests now (experimental-feature is now included)
4262+ if worker .payload == nil {
4263+ t .Fatal ("Expected payload to be loaded" )
4264+ }
4265+ if len (worker .payload .Manifests ) != 3 {
4266+ t .Fatalf ("Expected 3 manifests (with ExperimentalFeature), got %d" , len (worker .payload .Manifests ))
4267+ }
4268+
4269+ // Step 3: Enable LegacyFeature gate
4270+ // Expected manifests: always-included, experimental-feature
4271+ // NOT included: legacy-excluded (requires -LegacyFeature, but LegacyFeature is now enabled)
4272+
4273+ // Clear any pending status updates from Step 2
4274+ clearAllStatus (t , worker .StatusCh ())
4275+
4276+ o .updateEnabledFeatureGates (& configv1.FeatureGate {
4277+ Status : configv1.FeatureGateStatus {
4278+ FeatureGates : []configv1.FeatureGateDetails {
4279+ {
4280+ Version : "1.0.0-abc" ,
4281+ Enabled : []configv1.FeatureGateAttributes {
4282+ {Name : "ExperimentalFeature" },
4283+ {Name : "LegacyFeature" },
4284+ },
4285+ },
4286+ },
4287+ },
4288+ })
4289+
4290+ // Trigger another sync
4291+ client .ClearActions ()
4292+ err = o .sync (ctx , o .queueKey ())
4293+ if err != nil {
4294+ t .Fatal (err )
4295+ }
4296+
4297+ // Verify feature gates changed and payload was refreshed again
4298+ // Note: updateLoadStatus preserves apply status fields from Step 2's completed state
4299+ verifyAllStatus (t , worker .StatusCh (),
4300+ SyncWorkerStatus {
4301+ Done : 3 ,
4302+ Total : 3 ,
4303+ Completed : 2 ,
4304+ Reconciling : true ,
4305+ Initial : false ,
4306+ VersionHash : "yrh5CWG1KPI=" ,
4307+ Architecture : architecture ,
4308+ Actual : configv1.Release {
4309+ Version : "1.0.0-abc" ,
4310+ Image : "image/image:1" ,
4311+ URL : "https://example.com/v1.0.0-abc" ,
4312+ },
4313+ LastProgress : time .Unix (1 , 0 ),
4314+ loadPayloadStatus : LoadPayloadStatus {
4315+ Step : "RetrievePayload" ,
4316+ Message : "Retrieving and verifying payload version=\" 1.0.0-abc\" image=\" image/image:1\" " ,
4317+ LastTransitionTime : time .Unix (1 , 0 ),
4318+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4319+ },
4320+ CapabilitiesStatus : CapabilityStatus {
4321+ Status : configv1.ClusterVersionCapabilitiesStatus {
4322+ EnabledCapabilities : sortedCaps ,
4323+ KnownCapabilities : sortedKnownCaps ,
4324+ },
4325+ },
4326+ EnabledFeatureGates : sets.New [string ]("ExperimentalFeature" ),
4327+ },
4328+ SyncWorkerStatus {
4329+ Done : 3 ,
4330+ Total : 3 ,
4331+ Completed : 2 ,
4332+ Reconciling : true ,
4333+ Initial : false ,
4334+ VersionHash : "yrh5CWG1KPI=" ,
4335+ Architecture : architecture ,
4336+ Actual : configv1.Release {
4337+ Version : "1.0.0-abc" ,
4338+ Image : "image/image:1" ,
4339+ URL : "https://example.com/v1.0.0-abc" ,
4340+ },
4341+ LastProgress : time .Unix (2 , 0 ),
4342+ loadPayloadStatus : LoadPayloadStatus {
4343+ Step : "PayloadLoaded" ,
4344+ Message : "Payload loaded version=\" 1.0.0-abc\" image=\" image/image:1\" architecture=\" " + architecture + "\" " ,
4345+ LastTransitionTime : time .Unix (2 , 0 ),
4346+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4347+ },
4348+ CapabilitiesStatus : CapabilityStatus {
4349+ Status : configv1.ClusterVersionCapabilitiesStatus {
4350+ EnabledCapabilities : sortedCaps ,
4351+ KnownCapabilities : sortedKnownCaps ,
4352+ },
4353+ },
4354+ EnabledFeatureGates : sets.New [string ]("ExperimentalFeature" ),
4355+ },
4356+ SyncWorkerStatus {
4357+ Total : 2 , // legacy-excluded is now excluded
4358+ Initial : false ,
4359+ VersionHash : "ge54Uoy7v5o=" , Architecture : architecture ,
4360+ Actual : configv1.Release {
4361+ Version : "1.0.0-abc" ,
4362+ Image : "image/image:1" ,
4363+ URL : "https://example.com/v1.0.0-abc" ,
4364+ },
4365+ LastProgress : time .Unix (3 , 0 ),
4366+ loadPayloadStatus : LoadPayloadStatus {
4367+ Step : "PayloadLoaded" ,
4368+ Message : "Payload loaded version=\" 1.0.0-abc\" image=\" image/image:1\" architecture=\" " + architecture + "\" " ,
4369+ LastTransitionTime : time .Unix (3 , 0 ),
4370+ Update : configv1.Update {Version : "1.0.0-abc" , Image : "image/image:1" },
4371+ },
4372+ CapabilitiesStatus : CapabilityStatus {
4373+ Status : configv1.ClusterVersionCapabilitiesStatus {
4374+ EnabledCapabilities : sortedCaps ,
4375+ KnownCapabilities : sortedKnownCaps ,
4376+ },
4377+ ImplicitlyEnabledCaps : []configv1.ClusterVersionCapability {},
4378+ },
4379+ EnabledFeatureGates : sets .New [string ]("ExperimentalFeature" , "LegacyFeature" ),
4380+ },
4381+ )
4382+
4383+ // Verify 2 manifests now (legacy-excluded is now filtered out)
4384+ if worker .payload == nil {
4385+ t .Fatal ("Expected payload to be loaded" )
4386+ }
4387+ if len (worker .payload .Manifests ) != 2 {
4388+ t .Fatalf ("Expected 2 manifests (legacy-excluded should be filtered), got %d" , len (worker .payload .Manifests ))
4389+ }
4390+ }
4391+
40484392// verifyCVSingleUpdate ensures that the only object to be updated is a ClusterVersion type and it is updated only once
40494393func verifyCVSingleUpdate (t * testing.T , actions []clientgotesting.Action ) {
40504394 var count int
0 commit comments