Skip to content

Commit 5162bf7

Browse files
DavidHurtaclaude
authored andcommitted
test: Add CVO scenario for feature gate based inclusion
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 206c606 commit 5162bf7

7 files changed

Lines changed: 386 additions & 0 deletions

File tree

pkg/cvo/cvo_scenarios_test.go

Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
40494393
func verifyCVSingleUpdate(t *testing.T, actions []clientgotesting.Action) {
40504394
var count int

pkg/cvo/testdata/featuregatetest/manifests/.gitkeep

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
kind: ConfigMap
2+
apiVersion: v1
3+
metadata:
4+
name: always-included
5+
namespace: default
6+
annotations:
7+
include.release.openshift.io/self-managed-high-availability: "true"
8+
data:
9+
key: "always-present"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
kind: ConfigMap
2+
apiVersion: v1
3+
metadata:
4+
name: experimental-feature
5+
namespace: default
6+
annotations:
7+
include.release.openshift.io/self-managed-high-availability: "true"
8+
release.openshift.io/feature-gate: "ExperimentalFeature"
9+
data:
10+
key: "experimental-data"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
kind: ConfigMap
2+
apiVersion: v1
3+
metadata:
4+
name: legacy-excluded
5+
namespace: default
6+
annotations:
7+
include.release.openshift.io/self-managed-high-availability: "true"
8+
release.openshift.io/feature-gate: "-LegacyFeature"
9+
data:
10+
key: "new-implementation"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
kind: ImageStream
2+
apiVersion: image.openshift.io/v1
3+
metadata:
4+
name: 1.0.0-abc
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"kind": "cincinnati-metadata-v0",
3+
"version": "1.0.0-abc",
4+
"previous": [],
5+
"metadata": {
6+
"description": "",
7+
"url": "https://example.com/v1.0.0-abc"
8+
}
9+
}

0 commit comments

Comments
 (0)