diff --git a/internal/admission/admit_nodepool.go b/internal/admission/admit_nodepool.go index bc6069ed8c9..f2cd5b78875 100644 --- a/internal/admission/admit_nodepool.go +++ b/internal/admission/admit_nodepool.go @@ -92,6 +92,11 @@ func mutateNodePoolServiceProviderProperties(ctx context.Context, admissionConte errs = append(errs, mutateNodePoolExperimentalTags(ctx, admissionContext, op)...) errs = append(errs, mutateNodePoolCreateOperationCompletionDeadline(ctx, admissionContext, op, fldPath.Child("createOperationCompletionDeadline"), &newObj.CreateOperationCompletionDeadline)...) + if op.Type == operation.Create { + subscription := admissionContext.Subscription + newObj.ExperimentalFeaturesEnabled = subscription != nil && subscription.HasRegisteredFeature(metadataapi.FeatureExperimentalReleaseFeatures) + } + return errs } diff --git a/internal/admission/admit_nodepool_test.go b/internal/admission/admit_nodepool_test.go index d44dcce0d32..fe845fe7923 100644 --- a/internal/admission/admit_nodepool_test.go +++ b/internal/admission/admit_nodepool_test.go @@ -347,6 +347,68 @@ func TestMutateNodePoolCreateOperationCompletionDeadline(t *testing.T) { } } +func TestMutateNodePoolExperimentalFeaturesEnabled(t *testing.T) { + afecRegistered := &coreapi.Subscription{ + Properties: &coreapi.SubscriptionProperties{ + RegisteredFeatures: &[]coreapi.Feature{ + { + Name: ptr.To(metadataapi.FeatureExperimentalReleaseFeatures), + State: ptr.To("Registered"), + }, + }, + }, + } + + tests := []struct { + name string + op operation.Type + subscription *coreapi.Subscription + expected bool + }{ + { + name: "create: AFEC registered sets ExperimentalFeaturesEnabled=true", + op: operation.Create, + subscription: afecRegistered, + expected: true, + }, + { + name: "create: no AFEC leaves ExperimentalFeaturesEnabled=false", + op: operation.Create, + subscription: &coreapi.Subscription{Properties: &coreapi.SubscriptionProperties{}}, + expected: false, + }, + { + name: "create: nil subscription leaves ExperimentalFeaturesEnabled=false", + op: operation.Create, + subscription: nil, + expected: false, + }, + { + name: "update: AFEC registered does not set ExperimentalFeaturesEnabled", + op: operation.Update, + subscription: afecRegistered, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + nodePool := &coreapi.HCPOpenShiftClusterNodePool{} + admissionContext := &NodePoolAdmissionContext{ + Clock: utilsclock.RealClock{}, + Subscription: tt.subscription, + Cluster: &coreapi.HCPOpenShiftCluster{}, + } + if tt.op == operation.Update { + admissionContext.OriginalNodePool = &coreapi.HCPOpenShiftClusterNodePool{} + } + errs := MutateNodePool(context.Background(), admissionContext, operation.Operation{Type: tt.op}, nodePool, admissionContext.OriginalNodePool) + require.Empty(t, errs) + assert.Equal(t, tt.expected, nodePool.ServiceProviderProperties.ExperimentalFeaturesEnabled) + }) + } +} + func TestAdmitNodePool_SubnetVNet(t *testing.T) { const ( clusterSubnet = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/cluster-vnet/subnets/cluster-subnet" diff --git a/internal/api/coreapi/types_nodepool.go b/internal/api/coreapi/types_nodepool.go index 7234b8c0c8e..4f3cdde797a 100644 --- a/internal/api/coreapi/types_nodepool.go +++ b/internal/api/coreapi/types_nodepool.go @@ -124,6 +124,13 @@ type HCPOpenShiftClusterNodePoolServiceProviderProperties struct { // The operation node pool create controller uses this value to decide about marking the install as failed. // The e2e tests set this value to one minute less than the default timeout. CreateOperationCompletionDeadline *metav1.Time `json:"createOperationCompletionDeadline,omitempty"` + + // ExperimentalFeaturesEnabled records whether the FeatureExperimentalReleaseFeatures AFEC was + // registered on the subscription at node pool creation time. Used by internal/ocm/convert.go + // to gate experimental CS fields (e.g. SseEncryptionSetResourceId on the OS disk) that are + // only sent to Cluster Service when the AFEC is registered. + // Written by: Frontend PUT NodePool (Create) + ExperimentalFeaturesEnabled bool `json:"experimentalFeaturesEnabled,omitempty"` } // NodePoolVersionProfile represents the worker node pool version. diff --git a/internal/apitesting/coreapitesting/fuzz.go b/internal/apitesting/coreapitesting/fuzz.go index d3ff1714233..85bdb5e73fa 100644 --- a/internal/apitesting/coreapitesting/fuzz.go +++ b/internal/apitesting/coreapitesting/fuzz.go @@ -196,6 +196,7 @@ func CommonRoundTripFuzzFuncs() []interface{} { j.ActiveOperationID = "" j.ClusterServiceID = nil j.UsesNewNodePoolDeletionApproach = false + j.ExperimentalFeaturesEnabled = false }, func(j *coreapi.HCPOpenShiftClusterExternalAuthServiceProviderProperties, c randfill.Continue) { c.FillNoCustom(j) diff --git a/internal/ocm/convert.go b/internal/ocm/convert.go index 9b63f26c7c1..822914830b5 100644 --- a/internal/ocm/convert.go +++ b/internal/ocm/convert.go @@ -224,6 +224,17 @@ func convertEnableEncryptionAtHostToCSBuilder(in coreapi.NodePoolPlatformProfile return arohcpv1alpha1.NewAzureNodePoolEncryptionAtHost().State(state) } +func buildCSOsDisk(osDisk coreapi.OSDiskProfile, storageAccountType, persistence string, experimentalFeaturesEnabled bool) *arohcpv1alpha1.AzureNodePoolOsDiskBuilder { + builder := arohcpv1alpha1.NewAzureNodePoolOsDisk(). + SizeGibibytes(int(*osDisk.SizeGiB)). + StorageAccountType(storageAccountType). + Persistence(persistence) + if experimentalFeaturesEnabled && osDisk.EncryptionSetID != nil { + builder.SseEncryptionSetResourceId(osDisk.EncryptionSetID.String()) + } + return builder +} + func convertClusterImageRegistryStateRPToCS(in coreapi.ClusterImageRegistryProfile) (string, error) { switch in.State { case metadataapi.ClusterImageRegistryStateDisabled: @@ -629,10 +640,7 @@ func BuildCSNodePool(ctx context.Context, nodePool *coreapi.HCPOpenShiftClusterN ResourceName(strings.ToLower(nodePool.Name)). VMSize(nodePool.Properties.Platform.VMSize). EncryptionAtHost(convertEnableEncryptionAtHostToCSBuilder(nodePool.Properties.Platform)). - OsDisk(arohcpv1alpha1.NewAzureNodePoolOsDisk(). - SizeGibibytes(int(*nodePool.Properties.Platform.OSDisk.SizeGiB)). - StorageAccountType(csDiskStorageAccountType). - Persistence(csPersistence))). + OsDisk(buildCSOsDisk(nodePool.Properties.Platform.OSDisk, csDiskStorageAccountType, csPersistence, nodePool.ServiceProviderProperties.ExperimentalFeaturesEnabled))). AvailabilityZone(nodePool.Properties.Platform.AvailabilityZone). AutoRepair(nodePool.Properties.AutoRepair) } diff --git a/internal/ocm/convert_test.go b/internal/ocm/convert_test.go index b5aae7fffc2..669adb315d4 100644 --- a/internal/ocm/convert_test.go +++ b/internal/ocm/convert_test.go @@ -440,6 +440,46 @@ func TestBuildCSNodePool(t *testing.T) { ), ), }, + { + name: "passes disk encryption set ID to CS when AFEC is enabled", + hcpNodePool: getHCPNodePoolResource( + func(hsc *coreapi.HCPOpenShiftClusterNodePool) { + hsc.Properties.Platform.OSDisk.EncryptionSetID = metadataapi.Must(azcorearm.ParseResourceID( + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-des")) + hsc.ServiceProviderProperties.ExperimentalFeaturesEnabled = true + }, + ), + expectedCSNodePool: getBaseCSNodePoolBuilder(). + AzureNodePool(arohcpv1alpha1.NewAzureNodePool(). + ResourceName(""). + VMSize(""). + EncryptionAtHost( + arohcpv1alpha1.NewAzureNodePoolEncryptionAtHost(). + State(csEncryptionAtHostStateDisabled), + ). + OsDisk(arohcpv1alpha1.NewAzureNodePoolOsDisk(). + SizeGibibytes(64). + StorageAccountType(string(metadataapi.DiskStorageAccountTypePremium_LRS)). + Persistence("persistent"). + SseEncryptionSetResourceId("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-des"), + ), + ), + }, + { + name: "disk encryption set ID not passed to CS when AFEC is disabled", + hcpNodePool: getHCPNodePoolResource( + func(hsc *coreapi.HCPOpenShiftClusterNodePool) { + hsc.Properties.Platform.OSDisk.EncryptionSetID = metadataapi.Must(azcorearm.ParseResourceID( + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-des")) + }, + ), + expectedCSNodePool: getBaseCSNodePoolBuilder(), + }, + { + name: "nil disk encryption set ID does not set SSE field", + hcpNodePool: getHCPNodePoolResource(), + expectedCSNodePool: getBaseCSNodePoolBuilder(), + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) {