diff --git a/backend/pkg/app/backend.go b/backend/pkg/app/backend.go index 0a91d48ff31..b6ebed375f4 100644 --- a/backend/pkg/app/backend.go +++ b/backend/pkg/app/backend.go @@ -42,7 +42,6 @@ import ( "github.com/Azure/ARO-HCP/backend/pkg/controllers/managementclustercontrollers" "github.com/Azure/ARO-HCP/backend/pkg/controllers/metricscontrollers" "github.com/Azure/ARO-HCP/backend/pkg/controllers/mismatchcontrollers" - "github.com/Azure/ARO-HCP/backend/pkg/controllers/nodepoolpropertiescontroller" "github.com/Azure/ARO-HCP/backend/pkg/controllers/operationcontrollers" "github.com/Azure/ARO-HCP/backend/pkg/controllers/upgradecontrollers" "github.com/Azure/ARO-HCP/backend/pkg/controllers/validationcontrollers" @@ -592,20 +591,6 @@ func (b *Backend) runBackendControllersUnderLeaderElection(ctx context.Context, backendInformers, ) - nodePoolPropertiesSyncController := nodepoolpropertiescontroller.NewNodePoolPropertiesSyncController( - b.options.ResourcesDBClient, - b.options.ClustersServiceClient, - activeOperationLister, - backendInformers, - ) - - nodePoolCustomerPropertiesMigrationController := nodepoolpropertiescontroller.NewNodePoolCustomerPropertiesMigrationController( - b.options.ResourcesDBClient, - b.options.ClustersServiceClient, - activeOperationLister, - backendInformers, - ) - le, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{ Lock: b.options.LeaderElectionLock, LeaseDuration: leaderElectionLeaseDuration, @@ -660,8 +645,6 @@ func (b *Backend) runBackendControllersUnderLeaderElection(ctx context.Context, go maestroReadAndPersistNodePoolScopedReadonlyBundlesContentController.Run(ctx, 20) go maestroDeleteOrphanedReadonlyBundlesController.Run(ctx, 20) go triggerNodePoolUpgradeController.Run(ctx, 20) - go nodePoolPropertiesSyncController.Run(ctx, 20) - go nodePoolCustomerPropertiesMigrationController.Run(ctx, 20) go operationPhaseMetricsController.Run(ctx, 1) go clusterMetricsController.Run(ctx, 1) go nodePoolMetricsController.Run(ctx, 1) diff --git a/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_customer_properties_migration.go b/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_customer_properties_migration.go deleted file mode 100644 index ad6c7da9753..00000000000 --- a/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_customer_properties_migration.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2026 Microsoft Corporation -// -// 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 nodepoolpropertiescontroller - -import ( - "context" - "fmt" - "time" - - "github.com/Azure/ARO-HCP/backend/pkg/controllers/controllerutils" - "github.com/Azure/ARO-HCP/backend/pkg/informers" - "github.com/Azure/ARO-HCP/backend/pkg/listers" - "github.com/Azure/ARO-HCP/internal/api" - controllerutil "github.com/Azure/ARO-HCP/internal/controllerutils" - "github.com/Azure/ARO-HCP/internal/database" - "github.com/Azure/ARO-HCP/internal/ocm" - "github.com/Azure/ARO-HCP/internal/utils" -) - -// nodePoolCustomerPropertiesMigrationController is a NodePool controller that migrates properties (customer properties) -// from cluster-service to cosmos DB. It uses the .platform.vmSize attribute to know that customerProperties are missing. -// Old records will lack those fields and once we read from cluster-service, we'll have the information we need. -type nodePoolCustomerPropertiesMigrationController struct { - cooldownChecker controllerutil.CooldownChecker - - nodePoolLister listers.NodePoolLister - resourcesDBClient database.ResourcesDBClient - clusterServiceClient ocm.ClusterServiceClientSpec -} - -var _ controllerutils.NodePoolSyncer = (*nodePoolCustomerPropertiesMigrationController)(nil) - -func NewNodePoolCustomerPropertiesMigrationController( - resourcesDBClient database.ResourcesDBClient, - clusterServiceClient ocm.ClusterServiceClientSpec, - activeOperationLister listers.ActiveOperationLister, - informers informers.BackendInformers, -) controllerutils.Controller { - _, nodePoolLister := informers.NodePools() - - syncer := &nodePoolCustomerPropertiesMigrationController{ - cooldownChecker: controllerutils.DefaultActiveOperationPrioritizingCooldown(activeOperationLister), - nodePoolLister: nodePoolLister, - resourcesDBClient: resourcesDBClient, - clusterServiceClient: clusterServiceClient, - } - - controller := controllerutils.NewNodePoolWatchingController( - "NodePoolCustomerPropertiesMigration", - resourcesDBClient, - informers, - 60*time.Minute, // Check every 60 minutes - syncer, - ) - - return controller -} - -func (c *nodePoolCustomerPropertiesMigrationController) CooldownChecker() controllerutil.CooldownChecker { - return c.cooldownChecker -} - -func (c *nodePoolCustomerPropertiesMigrationController) NeedsWork(ctx context.Context, existingNodePool *api.HCPOpenShiftClusterNodePool) bool { - // Check if we have a Clusters Service's NodePool service ID to query. We will lack this information for newly created records when we - // transition to async Clusters Service's NodePool creation. - if existingNodePool.ServiceProviderProperties.ClusterServiceID == nil || len(existingNodePool.ServiceProviderProperties.ClusterServiceID.String()) == 0 { - return false - } - - // We use .properties.platform.vmSize as the marker to know if customer properties - // need to be migrated for the NodePool being processed. - // .properties.platform.vmSize is a required attribute at ARM API level, so its - // absence in Cosmos signals that the customer properties of the NodePool are not - // migrated into Cosmos yet and we need to migrate them. - needsVMSize := len(existingNodePool.Properties.Platform.VMSize) == 0 - return needsVMSize -} - -func (c *nodePoolCustomerPropertiesMigrationController) SyncOnce(ctx context.Context, key controllerutils.HCPNodePoolKey) error { - logger := utils.LoggerFromContext(ctx) - - // do the super cheap cache check first - cachedNodePool, err := c.nodePoolLister.Get(ctx, key.SubscriptionID, key.ResourceGroupName, key.HCPClusterName, key.HCPNodePoolName) - if database.IsNotFoundError(err) { - // we'll be re-fired if it is created again - return nil - } - if err != nil { - return utils.TrackError(fmt.Errorf("failed to get nodePool from cache: %w", err)) - } - if !c.NeedsWork(ctx, cachedNodePool) { - // if the cache doesn't need work, then we'll be retriggered if those values change when the cache updates. - // if the values don't change, then we still have no work to do. - return nil - } - - // Get the nodePool from Cosmos - nodePoolCRUD := c.resourcesDBClient.HCPClusters(key.SubscriptionID, key.ResourceGroupName).NodePools(key.HCPClusterName) - existingNodePool, err := nodePoolCRUD.Get(ctx, key.HCPNodePoolName) - if database.IsNotFoundError(err) { - return nil // nodePool doesn't exist, no work to do - } - if err != nil { - return utils.TrackError(fmt.Errorf("failed to get nodePool: %w", err)) - } - // check if we need to do work again. Sometimes the live data is more fresh than the cache and obviates the need to any work - if !c.NeedsWork(ctx, existingNodePool) { - return nil - } - - // Fetch the NodePool from Cluster Service - csNodePool, err := c.clusterServiceClient.GetNodePool(ctx, *existingNodePool.ServiceProviderProperties.ClusterServiceID) - if err != nil { - return utils.TrackError(fmt.Errorf("failed to get nodePool from Cluster Service: %w", err)) - } - - // Use ConvertCStoNodePool to convert the nodePool and extract the Properties (customer properties) - convertedNodePool, err := ocm.ConvertCStoNodePool(existingNodePool.ID, existingNodePool.Location, csNodePool) - if err != nil { - return utils.TrackError(fmt.Errorf("failed to convert nodePool from Cluster Service: %w", err)) - } - - // Update only the Properties from the converted nodePool - existingNodePool.Properties = convertedNodePool.Properties - - // Write the updated nodePool back to Cosmos - if _, err := nodePoolCRUD.Replace(ctx, existingNodePool, nil); err != nil { - return utils.TrackError(fmt.Errorf("failed to replace nodePool: %w", err)) - } - - logger.Info("migrated nodePool properties from Cluster Service to Cosmos") - - return nil -} diff --git a/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_customer_properties_migration_test.go b/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_customer_properties_migration_test.go deleted file mode 100644 index 083041be3ed..00000000000 --- a/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_customer_properties_migration_test.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2026 Microsoft Corporation -// -// 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 nodepoolpropertiescontroller - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" - - arohcpv1alpha1 "github.com/openshift-online/ocm-sdk-go/arohcp/v1alpha1" - - "github.com/Azure/ARO-HCP/backend/pkg/controllers/controllerutils" - "github.com/Azure/ARO-HCP/backend/pkg/listertesting" - "github.com/Azure/ARO-HCP/internal/api" - "github.com/Azure/ARO-HCP/internal/databasetesting" - "github.com/Azure/ARO-HCP/internal/ocm" -) - -const ( - testLocation = "eastus" - testVersionID = "4.15" - testChannelGroup = "stable" - testSubnetID = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet" -) - -func TestNodePoolCustomerPropertiesMigrationController_SyncOnce(t *testing.T) { - const ( - testNodePoolVMSize = "Standard_D8s_v3" - ) - - testCases := []struct { - name string - cachedNodePool *api.HCPOpenShiftClusterNodePool // nodePool in cache, nil means use same as existingNodePool - existingCosmosNodePool *api.HCPOpenShiftClusterNodePool // nodePool in cosmos - csNodePool *arohcpv1alpha1.NodePool - csError error - expectCSCall bool - expectError bool - expectedVMSize string - }{ - { - name: "cache indicates no work needed - early return without cosmos lookup", - cachedNodePool: newTestNodePoolForMigration(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Platform.VMSize = testNodePoolVMSize - }), - existingCosmosNodePool: newTestNodePoolForMigration(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Platform.VMSize = testNodePoolVMSize - }), - expectCSCall: false, - expectError: false, - expectedVMSize: testNodePoolVMSize, - }, - { - name: "cache says work needed but live data says no work needed", - cachedNodePool: newTestNodePoolForMigration(t, func(np *api.HCPOpenShiftClusterNodePool) {}), // cache has no vmSize info - existingCosmosNodePool: newTestNodePoolForMigration(t, func(np *api.HCPOpenShiftClusterNodePool) { - // cosmos has the version info (cache is stale) - np.Properties.Platform.VMSize = testNodePoolVMSize - }), - expectCSCall: false, - expectError: false, - expectedVMSize: testNodePoolVMSize, - }, - { - name: "error reading from cluster-service", - existingCosmosNodePool: newTestNodePoolForMigration(t, func(np *api.HCPOpenShiftClusterNodePool) {}), - csError: fmt.Errorf("connection refused"), - expectCSCall: true, - expectError: true, - }, - { - name: "success - migrate vmSize when missing", - existingCosmosNodePool: newTestNodePoolForMigration(t, func(np *api.HCPOpenShiftClusterNodePool) {}), - csNodePool: newTestFullCSNodePool(testNodePoolVMSize), - expectCSCall: true, - expectError: false, - expectedVMSize: testNodePoolVMSize, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx := context.Background() - ctrl := gomock.NewController(t) - - // Setup mock DB - mockResourcesDBClient := databasetesting.NewMockResourcesDBClient() - - // Create the nodePool in the mock DB (cosmos) - nodePoolCRUD := mockResourcesDBClient.HCPClusters(testSubscriptionID, testResourceGroupName).NodePools(testClusterName) - _, err := nodePoolCRUD.Create(ctx, tc.existingCosmosNodePool, nil) - require.NoError(t, err) - - // Setup slice nodePool lister (cache) - // If cachedCluster is nil, use the same as existingCluster - cachedNodePool := tc.cachedNodePool - if cachedNodePool == nil { - cachedNodePool = tc.existingCosmosNodePool - } - sliceNodePoolLister := &listertesting.SliceNodePoolLister{ - NodePools: []*api.HCPOpenShiftClusterNodePool{cachedNodePool}, - } - - // Setup mock CS client - mockCSClient := ocm.NewMockClusterServiceClientSpec(ctrl) - - if tc.expectCSCall { - mockCSClient.EXPECT(). - GetNodePool(gomock.Any(), api.Must(api.NewInternalID(testNodePoolCSIDStr))). - Return(tc.csNodePool, tc.csError) - } - - // Create syncer - syncer := &nodePoolCustomerPropertiesMigrationController{ - cooldownChecker: &alwaysSyncCooldownChecker{}, - nodePoolLister: sliceNodePoolLister, - resourcesDBClient: mockResourcesDBClient, - clusterServiceClient: mockCSClient, - } - - // Execute - key := controllerutils.HCPNodePoolKey{ - SubscriptionID: testSubscriptionID, - ResourceGroupName: testResourceGroupName, - HCPClusterName: testClusterName, - HCPNodePoolName: testNodePoolName, - } - err = syncer.SyncOnce(ctx, key) - - if tc.expectError { - require.Error(t, err) - } else { - require.NoError(t, err) - } - - // Verify the cluster state in Cosmos - updatedNodePool, err := nodePoolCRUD.Get(ctx, testNodePoolName) - require.NoError(t, err) - assert.Equal(t, tc.expectedVMSize, updatedNodePool.Properties.Platform.VMSize) - }) - } -} - -// newTestNodePoolForMigration builds a node pool for customer-properties migration tests: it initializes a new -// test nodepool without customer properties, and it then applies opts on top of it. -func newTestNodePoolForMigration(t *testing.T, opts func(*api.HCPOpenShiftClusterNodePool)) *api.HCPOpenShiftClusterNodePool { - t.Helper() - nodePool := newTestNodePool(t, nil) - nodePool.Properties = api.HCPOpenShiftClusterNodePoolProperties{} - if opts != nil { - opts(nodePool) - } - return nodePool -} - -// newTestFullCSNodePool creates a mock Clusters Service NodePool with all fields -// used by ocm.ConvertCStoNodePool, using a fixed number of replicas. -func newTestFullCSNodePool(vmSize string) *arohcpv1alpha1.NodePool { - nodePool, err := arohcpv1alpha1.NewNodePool(). - ID(testNodePoolName). - Version(arohcpv1alpha1.NewVersion().RawID("test-version-id").ChannelGroup("test-channel-group")). - Subnet(testSubnetID). - AzureNodePool(arohcpv1alpha1.NewAzureNodePool(). - ResourceName(testNodePoolName). - VMSize(vmSize). - EncryptionAtHost(arohcpv1alpha1.NewAzureNodePoolEncryptionAtHost().State("disabled")). - OsDisk(arohcpv1alpha1.NewAzureNodePoolOsDisk().SizeGibibytes(64).StorageAccountType(string(api.DiskStorageAccountTypePremium_LRS)).Persistence("persistent"))). - AvailabilityZone("1"). - AutoRepair(true). - Labels(map[string]string{"key": "value"}). - Taints(arohcpv1alpha1.NewTaint().Key("key").Value("value").Effect("NoExecute")). - NodeDrainGracePeriod(arohcpv1alpha1.NewValue().Unit("seconds").Value(10)). - Replicas(3). - Build() - if err != nil { - panic(err) - } - return nodePool - -} diff --git a/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_properties_sync.go b/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_properties_sync.go deleted file mode 100644 index cf32488d736..00000000000 --- a/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_properties_sync.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2026 Microsoft Corporation -// -// 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 nodepoolpropertiescontroller - -import ( - "context" - "fmt" - "time" - - "github.com/blang/semver/v4" - - "k8s.io/apimachinery/pkg/api/equality" - - "github.com/Azure/ARO-HCP/backend/pkg/controllers/controllerutils" - "github.com/Azure/ARO-HCP/backend/pkg/informers" - "github.com/Azure/ARO-HCP/backend/pkg/listers" - controllerutil "github.com/Azure/ARO-HCP/internal/controllerutils" - "github.com/Azure/ARO-HCP/internal/database" - "github.com/Azure/ARO-HCP/internal/ocm" - "github.com/Azure/ARO-HCP/internal/utils" -) - -type nodePoolPropertiesSyncer struct { - cooldownChecker controllerutil.CooldownChecker - nodePoolLister listers.NodePoolLister - resourcesDBClient database.ResourcesDBClient - clusterServiceClient ocm.ClusterServiceClientSpec -} - -var _ controllerutils.NodePoolSyncer = (*nodePoolPropertiesSyncer)(nil) - -// NewNodePoolPropertiesSyncController creates a new controller that synchronizes -// node pool properties from Cluster Service to Cosmos DB. -func NewNodePoolPropertiesSyncController( - resourcesDBClient database.ResourcesDBClient, - clusterServiceClient ocm.ClusterServiceClientSpec, - activeOperationLister listers.ActiveOperationLister, - informers informers.BackendInformers, -) controllerutils.Controller { - _, nodePoolLister := informers.NodePools() - syncer := &nodePoolPropertiesSyncer{ - cooldownChecker: controllerutils.DefaultActiveOperationPrioritizingCooldown(activeOperationLister), - nodePoolLister: nodePoolLister, - resourcesDBClient: resourcesDBClient, - clusterServiceClient: clusterServiceClient, - } - - controller := controllerutils.NewNodePoolWatchingController( - "NodePoolPropertiesSync", - resourcesDBClient, - informers, - time.Hour, - syncer, - ) - - return controller -} - -func (c *nodePoolPropertiesSyncer) CooldownChecker() controllerutil.CooldownChecker { - return c.cooldownChecker -} - -// needsVersionIDSync returns true when versionID is empty or not valid semver (e.g. only x.y), so existing node pools are migrated from Cluster Service. -func (c *nodePoolPropertiesSyncer) needsVersionIDSync(versionID string) bool { - _, err := semver.Parse(versionID) - return err != nil -} - -// SyncOnce performs a single reconciliation of node pool properties from Cluster Service to Cosmos. -func (c *nodePoolPropertiesSyncer) SyncOnce(ctx context.Context, key controllerutils.HCPNodePoolKey) error { - logger := utils.LoggerFromContext(ctx) - - cachedNodePool, err := c.nodePoolLister.Get(ctx, key.SubscriptionID, key.ResourceGroupName, key.HCPClusterName, key.HCPNodePoolName) - if database.IsNotFoundError(err) { - return nil - } - if err != nil { - return utils.TrackError(fmt.Errorf("failed to get node pool from cache: %w", err)) - } - if cachedNodePool.ServiceProviderProperties.ClusterServiceID == nil || len(cachedNodePool.ServiceProviderProperties.ClusterServiceID.String()) == 0 { - return nil - } - - needsVersionSync := c.needsVersionIDSync(cachedNodePool.Properties.Version.ID) - needsChannelSync := len(cachedNodePool.Properties.Version.ChannelGroup) == 0 - if !needsVersionSync && !needsChannelSync { - return nil - } - - nodePoolCRUD := c.resourcesDBClient.HCPClusters(key.SubscriptionID, key.ResourceGroupName).NodePools(key.HCPClusterName) - existingNodePool, err := nodePoolCRUD.Get(ctx, key.HCPNodePoolName) - if database.IsNotFoundError(err) { - return nil - } - if err != nil { - return utils.TrackError(fmt.Errorf("failed to get NodePool: %w", err)) - } - if existingNodePool.ServiceProviderProperties.ClusterServiceID == nil || len(existingNodePool.ServiceProviderProperties.ClusterServiceID.String()) == 0 { - return nil - } - - needsVersionSync = c.needsVersionIDSync(existingNodePool.Properties.Version.ID) - needsChannelSync = len(existingNodePool.Properties.Version.ChannelGroup) == 0 - if !needsVersionSync && !needsChannelSync { - return nil - } - - csNodePool, err := c.clusterServiceClient.GetNodePool(ctx, *existingNodePool.ServiceProviderProperties.ClusterServiceID) - if err != nil { - return utils.TrackError(fmt.Errorf("failed to get node pool from Cluster Service: %w", err)) - } - - originalNodePool := existingNodePool.DeepCopy() - - version := csNodePool.Version() - if needsVersionSync { - existingNodePool.Properties.Version.ID = version.RawID() - } - if needsChannelSync { - existingNodePool.Properties.Version.ChannelGroup = version.ChannelGroup() - } - - if equality.Semantic.DeepEqual(originalNodePool, existingNodePool) { - return nil - } - - if _, err := nodePoolCRUD.Replace(ctx, existingNodePool, nil); err != nil { - return utils.TrackError(fmt.Errorf("failed to replace NodePool: %w", err)) - } - - logger.Info("synced node pool properties from Cluster Service") - return nil -} diff --git a/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_properties_sync_test.go b/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_properties_sync_test.go deleted file mode 100644 index bc1a8d151c9..00000000000 --- a/backend/pkg/controllers/nodepoolpropertiescontroller/node_pool_properties_sync_test.go +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright 2026 Microsoft Corporation -// -// 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 nodepoolpropertiescontroller - -import ( - "context" - "errors" - "testing" - - "github.com/go-logr/logr/testr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" - - "k8s.io/apimachinery/pkg/api/equality" - - azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - - arohcpv1alpha1 "github.com/openshift-online/ocm-sdk-go/arohcp/v1alpha1" - - "github.com/Azure/ARO-HCP/backend/pkg/controllers/controllerutils" - "github.com/Azure/ARO-HCP/backend/pkg/listertesting" - "github.com/Azure/ARO-HCP/internal/api" - "github.com/Azure/ARO-HCP/internal/api/arm" - "github.com/Azure/ARO-HCP/internal/databasetesting" - "github.com/Azure/ARO-HCP/internal/ocm" - "github.com/Azure/ARO-HCP/internal/utils" -) - -const ( - testSubscriptionID = "00000000-0000-0000-0000-000000000000" - testResourceGroupName = "test-rg" - testClusterName = "test-cluster" - testNodePoolName = "test-nodepool" - testClusterServiceIDStr = "/api/aro_hcp/v1alpha1/clusters/abc123" - testNodePoolCSIDStr = testClusterServiceIDStr + "/node_pools/" + testNodePoolName -) - -type alwaysSyncCooldownChecker struct{} - -func (c *alwaysSyncCooldownChecker) CanSync(ctx context.Context, key any) bool { - return true -} - -func TestNodePoolPropertiesSyncer_SyncOnce(t *testing.T) { - testCases := []struct { - name string - existingNodePool *api.HCPOpenShiftClusterNodePool - existingCluster *api.HCPOpenShiftCluster - cacheNodePool *api.HCPOpenShiftClusterNodePool // when set, lister uses this instead of existingNodePool (e.g. stale cache) - csNodePool *arohcpv1alpha1.NodePool - expectGetNodePool bool - wantNodePool *api.HCPOpenShiftClusterNodePool - getNodePoolErr error - wantErr bool - wantErrContain string - }{ - { - name: "short-circuit when version is valid semver and channel group set", - existingCluster: newTestCluster(t), - existingNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "stable" - }), - expectGetNodePool: false, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "stable" - }), - }, - { - name: "sync channel group from CS when version is valid semver and channel group empty", - existingCluster: newTestCluster(t), - existingNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - }), - csNodePool: newCSNodePoolWithVersion(t, "4.21.5", "stable"), - expectGetNodePool: true, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "stable" - }), - }, - { - name: "sync version from CS when version ID is empty", - existingCluster: newTestCluster(t), - existingNodePool: newTestNodePool(t, nil), - csNodePool: newCSNodePoolWithVersion(t, "4.21.5", "stable"), - expectGetNodePool: true, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "stable" - }), - }, - { - name: "sync version from CS when version ID is invalid", - existingCluster: newTestCluster(t), - existingNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { np.Properties.Version.ID = "4.20" }), - csNodePool: newCSNodePoolWithVersion(t, "4.20.16", "stable"), - expectGetNodePool: true, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.20.16" - np.Properties.Version.ChannelGroup = "stable" - }), - }, - { - name: "node pool not found", - existingCluster: newTestCluster(t), - existingNodePool: nil, - expectGetNodePool: false, - }, - { - name: "nil ClusterServiceID skips CS call", - existingCluster: newTestCluster(t), - existingNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "" - np.ServiceProviderProperties.ClusterServiceID = nil - }), - expectGetNodePool: false, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "" - np.ServiceProviderProperties.ClusterServiceID = nil - }), - }, - { - name: "empty ClusterServiceID skips CS call", - existingCluster: newTestCluster(t), - existingNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "" - np.ServiceProviderProperties.ClusterServiceID = &api.InternalID{} - }), - expectGetNodePool: false, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "" - np.ServiceProviderProperties.ClusterServiceID = &api.InternalID{} - }), - }, - { - name: "GetNodePool error", - existingCluster: newTestCluster(t), - existingNodePool: newTestNodePool(t, nil), - expectGetNodePool: true, - getNodePoolErr: errors.New("cs error"), - wantErr: true, - wantErrContain: "failed to get node pool from Cluster Service", - }, - { - name: "when cache indicates needs work but Cosmos already has properties, skip Cluster Service", - existingCluster: newTestCluster(t), - // Cosmos has the node pool with version already set (no work needed). - existingNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "stable" - }), - expectGetNodePool: false, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "stable" - }), - // Stale cache (empty version); Cosmos is up to date so we skip Cluster Service. - cacheNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "" - }), - }, - { - name: "Cosmos version and channel correct; CS RawID behind desired (upgrade in progress) — do not overwrite", - existingCluster: newTestCluster(t), - // During upgrades, desired version in Cosmos can be ahead of Cluster Service's reported RawID until CS catches up; we must not pull CS and overwrite. - existingNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.6" - np.Properties.Version.ChannelGroup = "stable" - }), - csNodePool: newCSNodePoolWithVersion(t, "4.21.5", "stable"), - expectGetNodePool: false, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.6" - np.Properties.Version.ChannelGroup = "stable" - }), - cacheNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "" - }), - }, - { - name: "Cosmos version and channel correct; CS channel group differs — do not overwrite", - existingCluster: newTestCluster(t), - existingNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "stable" - }), - csNodePool: newCSNodePoolWithVersion(t, "4.21.5", "candidate"), - expectGetNodePool: false, - wantNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "stable" - }), - // Stale cache: channel empty so we read Cosmos; Cosmos is authoritative once both fields are set. - cacheNodePool: newTestNodePool(t, func(np *api.HCPOpenShiftClusterNodePool) { - np.Properties.Version.ID = "4.21.5" - np.Properties.Version.ChannelGroup = "" - }), - }, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - ctx := utils.ContextWithLogger(context.Background(), testr.New(t)) - ctrl := gomock.NewController(t) - - resources := []any{tc.existingCluster} - if tc.existingNodePool != nil { - resources = append(resources, tc.existingNodePool) - } - mockResourcesDBClient, err := databasetesting.NewMockResourcesDBClientWithResources(ctx, resources) - require.NoError(t, err) - - mockCSClient := ocm.NewMockClusterServiceClientSpec(ctrl) - if tc.expectGetNodePool { - call := mockCSClient.EXPECT(). - GetNodePool(gomock.Any(), api.Must(api.NewInternalID(testNodePoolCSIDStr))) - if tc.getNodePoolErr != nil { - call.Return(nil, tc.getNodePoolErr) - } else { - call.Return(tc.csNodePool, nil) - } - } - - nodePoolsForLister := []*api.HCPOpenShiftClusterNodePool{} - if tc.cacheNodePool != nil { - nodePoolsForLister = append(nodePoolsForLister, tc.cacheNodePool) - } else if tc.existingNodePool != nil { - nodePoolsForLister = append(nodePoolsForLister, tc.existingNodePool) - } - syncer := &nodePoolPropertiesSyncer{ - cooldownChecker: &alwaysSyncCooldownChecker{}, - nodePoolLister: &listertesting.SliceNodePoolLister{NodePools: nodePoolsForLister}, - resourcesDBClient: mockResourcesDBClient, - clusterServiceClient: mockCSClient, - } - - key := controllerutils.HCPNodePoolKey{ - SubscriptionID: testSubscriptionID, - ResourceGroupName: testResourceGroupName, - HCPClusterName: testClusterName, - HCPNodePoolName: testNodePoolName, - } - - err = syncer.SyncOnce(ctx, key) - if tc.wantErr { - require.Error(t, err) - require.Greater(t, len(tc.wantErrContain), 0, "wantErrContain must be set when wantErr is true") - assert.ErrorContains(t, err, tc.wantErrContain) - return - } - require.NoError(t, err) - - if tc.wantNodePool != nil { - updated, err := mockResourcesDBClient.HCPClusters(testSubscriptionID, testResourceGroupName). - NodePools(testClusterName).Get(ctx, testNodePoolName) - require.NoError(t, err) - require.True(t, equality.Semantic.DeepEqual(tc.wantNodePool.Properties, updated.Properties), "updated node pool properties do not match expected") - } - }) - } -} - -func newTestCluster(t *testing.T) *api.HCPOpenShiftCluster { - t.Helper() - resourceID := api.Must(azcorearm.ParseResourceID( - "/subscriptions/" + testSubscriptionID + - "/resourceGroups/" + testResourceGroupName + - "/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/" + testClusterName)) - clusterInternalID := api.Must(api.NewInternalID(testClusterServiceIDStr)) - return &api.HCPOpenShiftCluster{ - TrackedResource: arm.TrackedResource{ - Resource: arm.Resource{ - ID: resourceID, - Name: testClusterName, - Type: api.ClusterResourceType.String(), - }, - Location: "eastus", - }, - ServiceProviderProperties: api.HCPOpenShiftClusterServiceProviderProperties{ - ClusterServiceID: &clusterInternalID, - }, - } -} - -func newTestNodePool(t *testing.T, opts func(*api.HCPOpenShiftClusterNodePool)) *api.HCPOpenShiftClusterNodePool { - t.Helper() - resourceID := api.Must(azcorearm.ParseResourceID( - "/subscriptions/" + testSubscriptionID + - "/resourceGroups/" + testResourceGroupName + - "/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/" + testClusterName + - "/nodePools/" + testNodePoolName)) - nodePoolInternalID := api.Ptr(api.Must(api.NewInternalID(testNodePoolCSIDStr))) - np := &api.HCPOpenShiftClusterNodePool{ - TrackedResource: arm.TrackedResource{ - Resource: arm.Resource{ - ID: resourceID, - Name: testNodePoolName, - Type: api.NodePoolResourceType.String(), - }, - Location: "eastus", - }, - Properties: api.HCPOpenShiftClusterNodePoolProperties{ - Version: api.NodePoolVersionProfile{}, - Platform: api.NodePoolPlatformProfile{ - OSDisk: api.OSDiskProfile{ - DiskStorageAccountType: api.DiskStorageAccountTypePremium_LRS, - DiskType: api.OsDiskTypeManaged, - }, - }, - }, - ServiceProviderProperties: api.HCPOpenShiftClusterNodePoolServiceProviderProperties{ - ClusterServiceID: nodePoolInternalID, - }, - } - if opts != nil { - opts(np) - } - return np -} - -func newCSNodePoolWithVersion(t *testing.T, versionID, channelGroup string) *arohcpv1alpha1.NodePool { - t.Helper() - np, err := arohcpv1alpha1.NewNodePool(). - ID(testNodePoolName). - Version(arohcpv1alpha1.NewVersion().RawID(versionID).ChannelGroup(channelGroup)). - Build() - require.NoError(t, err) - return np -} diff --git a/internal/database/convert_defaults_consistency_test.go b/internal/database/convert_defaults_consistency_test.go index 0090a7ebb6b..05b60d07874 100644 --- a/internal/database/convert_defaults_consistency_test.go +++ b/internal/database/convert_defaults_consistency_test.go @@ -21,13 +21,10 @@ import ( azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" - arohcpv1alpha1 "github.com/openshift-online/ocm-sdk-go/arohcp/v1alpha1" - "github.com/Azure/ARO-HCP/internal/api" "github.com/Azure/ARO-HCP/internal/api/arm" v20240610preview "github.com/Azure/ARO-HCP/internal/api/v20240610preview" v20251223preview "github.com/Azure/ARO-HCP/internal/api/v20251223preview" - "github.com/Azure/ARO-HCP/internal/ocm" ) // TestEnsureDefaultsConsistencyNodePool verifies that the defaults applied by @@ -197,96 +194,6 @@ func TestEnsureDefaultsConsistencyCluster(t *testing.T) { }) } -// TestCSToRPDefaultsConsistencyNodePool verifies that when Cluster Service -// returns the default value for DiskStorageAccountType, the CS→RP conversion -// produces the same value as the canonical default. -func TestCSToRPDefaultsConsistencyNodePool(t *testing.T) { - resourceID := api.Must(azcorearm.ParseResourceID( - "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/cluster/nodePools/np", - )) - - csNodePool, err := arohcpv1alpha1.NewNodePool(). - AzureNodePool(arohcpv1alpha1.NewAzureNodePool(). - OsDisk(arohcpv1alpha1.NewAzureNodePoolOsDisk(). - StorageAccountType("Premium_LRS"). - Persistence("persistent"))). - Build() - if err != nil { - t.Fatalf("failed to build CS nodepool: %v", err) - } - - rpNodePool, err := ocm.ConvertCStoNodePool(resourceID, "eastus", csNodePool) - if err != nil { - t.Fatalf("ConvertCStoNodePool failed: %v", err) - } - - ensuredDefault := &api.HCPOpenShiftClusterNodePool{} - ensuredDefault.EnsureDefaults() - - if string(rpNodePool.Properties.Platform.OSDisk.DiskStorageAccountType) != string(ensuredDefault.Properties.Platform.OSDisk.DiskStorageAccountType) { - t.Errorf("CS→RP default DiskStorageAccountType = %q, ensured default = %q", - rpNodePool.Properties.Platform.OSDisk.DiskStorageAccountType, - ensuredDefault.Properties.Platform.OSDisk.DiskStorageAccountType) - } - if string(rpNodePool.Properties.Platform.OSDisk.DiskType) != string(ensuredDefault.Properties.Platform.OSDisk.DiskType) { - t.Errorf("CS→RP default DiskType = %q, ensured default = %q", - rpNodePool.Properties.Platform.OSDisk.DiskType, - ensuredDefault.Properties.Platform.OSDisk.DiskType) - } -} - -func TestCSToRPDefaultsEmptyDiskStorageAccountType(t *testing.T) { - resourceID := api.Must(azcorearm.ParseResourceID( - "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/cluster/nodePools/np", - )) - - // Simulate a pre-existing CS node pool that has no DiskStorageAccountType set. - csNodePool, err := arohcpv1alpha1.NewNodePool(). - AzureNodePool(arohcpv1alpha1.NewAzureNodePool(). - OsDisk(arohcpv1alpha1.NewAzureNodePoolOsDisk(). - StorageAccountType(""))). - Build() - if err != nil { - t.Fatalf("failed to build CS nodepool: %v", err) - } - - rpNodePool, err := ocm.ConvertCStoNodePool(resourceID, "eastus", csNodePool) - if err != nil { - t.Fatalf("ConvertCStoNodePool failed: %v", err) - } - - if rpNodePool.Properties.Platform.OSDisk.DiskStorageAccountType != api.DiskStorageAccountTypePremium_LRS { - t.Errorf("CS→RP conversion must default empty StorageAccountType to Premium_LRS, got %q", - rpNodePool.Properties.Platform.OSDisk.DiskStorageAccountType) - } -} - -func TestCSToRPDefaultsEmptyDiskType(t *testing.T) { - resourceID := api.Must(azcorearm.ParseResourceID( - "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/cluster/nodePools/np", - )) - - // Simulate a pre-existing CS node pool that has no persistence set. - csNodePool, err := arohcpv1alpha1.NewNodePool(). - AzureNodePool(arohcpv1alpha1.NewAzureNodePool(). - OsDisk(arohcpv1alpha1.NewAzureNodePoolOsDisk(). - Persistence(""))). - Build() - if err != nil { - t.Fatalf("failed to build CS nodepool: %v", err) - } - - rpNodePool, err := ocm.ConvertCStoNodePool(resourceID, "eastus", csNodePool) - if err != nil { - t.Fatalf("ConvertCStoNodePool failed: %v", err) - } - - if rpNodePool.Properties.Platform.OSDisk.DiskType != api.OsDiskTypeManaged { - t.Errorf("CS→RP conversion must default empty Persistence to Managed, got %q", - rpNodePool.Properties.Platform.OSDisk.DiskType) - } -} - // TestPreExistingDataCluster verifies that CosmosToInternalCluster applies // canonical defaults when reading a Cosmos document that predates the // introduction of canonically-defaulted fields. diff --git a/internal/ocm/convert.go b/internal/ocm/convert.go index 73884b44db2..48081561f83 100644 --- a/internal/ocm/convert.go +++ b/internal/ocm/convert.go @@ -23,7 +23,6 @@ import ( "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/utils/ptr" azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" @@ -125,25 +124,6 @@ func convertOutboundTypeRPToCS(outboundTypeRP api.OutboundType) (string, error) } } -// convertDiskStorageAccountTypeCSToRP maps Cluster Service DiskStorageAccountType -// strings to RP enum values. An empty string from CS (pre-existing resources that -// predate the field) is mapped to the default. Must match the canonical default in -// HCPOpenShiftClusterNodePool.EnsureDefaults(). See docs/api-version-defaults-and-storage.md. -func convertDiskStorageAccountTypeCSToRP(storageAccountTypeCS string) (api.DiskStorageAccountType, error) { - switch storageAccountTypeCS { - case string(api.DiskStorageAccountTypePremium_LRS): - return api.DiskStorageAccountTypePremium_LRS, nil - case string(api.DiskStorageAccountTypeStandardSSD_LRS): - return api.DiskStorageAccountTypeStandardSSD_LRS, nil - case string(api.DiskStorageAccountTypeStandard_LRS): - return api.DiskStorageAccountTypeStandard_LRS, nil - case "": - return api.DiskStorageAccountTypePremium_LRS, nil - default: - return "", conversionError[api.DiskStorageAccountType](storageAccountTypeCS) - } -} - func convertDiskStorageAccountTypeRPToCS(storageAccountTypeRP api.DiskStorageAccountType) (string, error) { switch storageAccountTypeRP { case api.DiskStorageAccountTypePremium_LRS: @@ -161,21 +141,6 @@ func convertDiskStorageAccountTypeRPToCS(storageAccountTypeRP api.DiskStorageAcc } } -// convertDiskTypeCSToRP maps Cluster Service persistence strings to RP -// OsDiskType enum values. An empty string from CS (pre-existing resources that -// predate the field) is mapped to the default. Must match the storage default in -// applyNodePoolStorageDefaults. See docs/api-version-defaults-and-storage.md. -func convertDiskTypeCSToRP(persistence string) (api.OsDiskType, error) { - switch persistence { - case csOsDiskPersistencePersistent, "": - return api.OsDiskTypeManaged, nil - case csOsDiskPersistenceEphemeral: - return api.OsDiskTypeEphemeral, nil - default: - return "", conversionError[api.OsDiskType](persistence) - } -} - func convertDiskTypeRPToCS(diskType api.OsDiskType) (string, error) { switch diskType { case api.OsDiskTypeManaged: @@ -552,89 +517,6 @@ func withImmutableAttributes(clusterBuilder *arohcpv1alpha1.ClusterBuilder, hcpC return clusterBuilder, nil } -// ConvertCStoNodePool converts a CS NodePool object into an HCPOpenShiftClusterNodePool object. -func ConvertCStoNodePool(resourceID *azcorearm.ResourceID, azureLocation string, np *arohcpv1alpha1.NodePool) (*api.HCPOpenShiftClusterNodePool, error) { - var subnetID *azcorearm.ResourceID - if len(np.Subnet()) > 0 { - var err error - subnetID, err = azcorearm.ParseResourceID(np.Subnet()) - if err != nil { - return nil, utils.TrackError(err) - } - } - - diskStorageAccountType, err := convertDiskStorageAccountTypeCSToRP(np.AzureNodePool().OsDisk().StorageAccountType()) - if err != nil { - return nil, utils.TrackError(err) - } - - diskType, err := convertDiskTypeCSToRP(np.AzureNodePool().OsDisk().Persistence()) - if err != nil { - return nil, utils.TrackError(err) - } - - nodePool := &api.HCPOpenShiftClusterNodePool{ - TrackedResource: arm.TrackedResource{ - Resource: arm.Resource{ - ID: resourceID, - Name: resourceID.Name, - Type: resourceID.ResourceType.String(), - }, - Location: azureLocation, - }, - Properties: api.HCPOpenShiftClusterNodePoolProperties{ - Version: api.NodePoolVersionProfile{ - ID: ConvertOpenShiftVersionNoPrefix(np.Version().ID()), - ChannelGroup: np.Version().ChannelGroup(), - }, - Platform: api.NodePoolPlatformProfile{ - SubnetID: subnetID, - VMSize: np.AzureNodePool().VMSize(), - EnableEncryptionAtHost: np.AzureNodePool().EncryptionAtHost().State() == csEncryptionAtHostStateEnabled, - OSDisk: api.OSDiskProfile{ - SizeGiB: ptr.To(int32(np.AzureNodePool().OsDisk().SizeGibibytes())), - DiskStorageAccountType: diskStorageAccountType, - DiskType: diskType, - }, - AvailabilityZone: np.AvailabilityZone(), - }, - AutoRepair: np.AutoRepair(), - Labels: np.Labels(), - }, - } - - if replicas, ok := np.GetReplicas(); ok { - nodePool.Properties.Replicas = int32(replicas) - } - - if autoscaling, ok := np.GetAutoscaling(); ok { - nodePool.Properties.AutoScaling = &api.NodePoolAutoScaling{ - Min: int32(autoscaling.MinReplica()), - Max: int32(autoscaling.MaxReplica()), - } - } - - if np.Taints() != nil { - taints := make([]api.Taint, 0, len(np.Taints())) - for _, t := range np.Taints() { - taints = append(taints, api.Taint{ - Effect: api.Effect(t.Effect()), - Key: t.Key(), - Value: t.Value(), - }) - } - nodePool.Properties.Taints = taints - } - - if nodeDrainGracePeriod, ok := np.GetNodeDrainGracePeriod(); ok { - if unit, ok := nodeDrainGracePeriod.GetUnit(); ok && unit == csNodeDrainGracePeriodUnit { - nodePool.Properties.NodeDrainTimeoutMinutes = api.Ptr(int32(nodeDrainGracePeriod.Value())) - } - } - - return nodePool, nil -} - // BuildCSNodePool creates a CS NodePoolBuilder object from an HCPOpenShiftClusterNodePool object. func BuildCSNodePool(ctx context.Context, nodePool *api.HCPOpenShiftClusterNodePool, updating bool) (*arohcpv1alpha1.NodePoolBuilder, error) { nodePoolBuilder := arohcpv1alpha1.NewNodePool()