diff --git a/pkg/frontend/openshiftcluster_preflightvalidation.go b/pkg/frontend/openshiftcluster_preflightvalidation.go index f9cea144baa..edd0c11c306 100644 --- a/pkg/frontend/openshiftcluster_preflightvalidation.go +++ b/pkg/frontend/openshiftcluster_preflightvalidation.go @@ -110,6 +110,7 @@ func (f *frontend) _preflightValidation(ctx context.Context, log *logrus.Entry, converter := f.apis[apiVersion].OpenShiftClusterConverter staticValidator := f.apis[apiVersion].OpenShiftClusterStaticValidator ext := converter.ToExternal(oc) + converter.ExternalNoReadOnly(ext) if err = json.Unmarshal(raw, &ext); err != nil { log.Warning(err.Error()) return api.ValidationResult{ @@ -148,6 +149,19 @@ func (f *frontend) _preflightValidation(ctx context.Context, log *logrus.Entry, } } } + converter.ToInternal(ext, oc) + if oc.UsesWorkloadIdentity() { + if err := f.validatePlatformWorkloadIdentities(oc); err != nil { + return api.ValidationResult{ + Status: api.ValidationStatusFailed, + Error: &api.CloudErrorBody{ + Code: api.CloudErrorCodeInvalidParameter, + Message: err.Error(), + }, + } + } + } + return validationSuccess } diff --git a/pkg/frontend/openshiftcluster_preflightvalidation_test.go b/pkg/frontend/openshiftcluster_preflightvalidation_test.go index e0ffc2686ce..e79c76d39ad 100644 --- a/pkg/frontend/openshiftcluster_preflightvalidation_test.go +++ b/pkg/frontend/openshiftcluster_preflightvalidation_test.go @@ -20,7 +20,7 @@ import ( func TestPreflightValidation(t *testing.T) { ctx := context.Background() mockSubID := "00000000-0000-0000-0000-000000000000" - apiVersion := "2020-04-30" + apiVersion := "2024-08-12-preview" clusterId := "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.RedHatOpenShift/openShiftClusters/resourceName" location := "eastus" defaultProfile := "default" @@ -30,7 +30,8 @@ func TestPreflightValidation(t *testing.T) { masterSub := "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/master" workerSub := "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/vnetResourceGroup/providers/Microsoft.Network/virtualNetworks/vnet/subnets/worker" - preflightPayload := []byte(fmt.Sprintf(` + ingressProfileIP := fmt.Sprintf(`"IP": "%s",`, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.IngressProfiles[0].IP) + preflightPayloadTemplate := ` { "apiVersion": "%s", "id": "%s", @@ -76,13 +77,15 @@ func TestPreflightValidation(t *testing.T) { "ingressProfiles": [ { "name": "%s", - "visibility": "%s", - "IP": "%s" + "__CustomField": "ingressProfile IP", + %s + "visibility": "%s" } ] } } - `, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + ` + preflightPayload := []byte(fmt.Sprintf(preflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, location, defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, version.DefaultInstallStream.Version.String(), mockSubID, mockSubID, netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, @@ -91,26 +94,132 @@ func TestPreflightValidation(t *testing.T) { api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, api.EncryptionAtHostEnabled, workerSub, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, - encryptionSet, api.VisibilityPublic, defaultProfile, api.VisibilityPublic, - api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.IngressProfiles[0].IP)) + encryptionSet, api.VisibilityPublic, defaultProfile, ingressProfileIP, api.VisibilityPublic)) - defaultVersionChangeFeed := map[string]*api.OpenShiftVersion{ - version.DefaultInstallStream.Version.String(): { - Properties: api.OpenShiftVersionProperties{ - Version: version.DefaultInstallStream.Version.String(), - Enabled: true, + managedIdentityClusterPreflightPayloadTemplate := ` + { + "apiVersion": "%s", + "id": "%s", + "name": "%s", + "type": "%s", + "location": "%s", + "__CustomField1": "identity", + %s + "properties": { + "clusterProfile": { + "domain": "%s", + "resourceGroupId": "%s", + "fipsValidatedModules": "%s", + "version": "%s" }, - }, + "__CustomField2": "platformWorkloadIdentityProfile", + %s + "networkProfile": { + "podCidr": "%s", + "serviceCidr": "%s" + }, + "masterProfile": { + "vmSize": "%s", + "subnetId": "%s", + "encryptionAtHost": "%s", + "diskEncryptionSetId": "%s" + }, + "workerProfiles": [ + { + "name": "%s", + "vmSize": "%s", + "diskSizeGB": %v, + "encryptionAtHost": "%s", + "subnetId": "%s", + "count": %v, + "diskEncryptionSetId": "%s" + } + ], + "apiserverProfile": { + "visibility": "%s" + }, + "ingressProfiles": [ + { + "name": "%s", + "__CustomField3": "ingressProfile IP", + %s + "visibility": "%s" + } + ] + } } + ` + platformIdentities := `"platformWorkloadIdentityProfile": { + "platformWorkloadIdentities": { + "file-csi-driver": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-AzureFilesStorageOperator" }, + "cloud-controller-manager": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-CloudControllerManager" }, + "ingress": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ClusterIngressOperator" }, + "image-registry": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ImageRegistryOperator" }, + "machine-api": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-MachineApiOperator" }, + "cloud-network-config": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-NetworkOperator" }, + "aro-operator": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ServiceOperator" }, + "disk-csi-driver": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-StorageOperator" } + } + }, + ` + missingPlatformIdentities := `"platformWorkloadIdentityProfile": { + "platformWorkloadIdentities": { + "file-csi-driver": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-AzureFilesStorageOperator" }, + "cloud-controller-manager": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-CloudControllerManager" }, + "ingress": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ClusterIngressOperator" }, + "image-registry": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ImageRegistryOperator" }, + "machine-api": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-MachineApiOperator" }, + "cloud-network-config": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-NetworkOperator" }, + "aro-operator": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ServiceOperator" } + } + }, +` + incorrectPlatformIdentities := `"platformWorkloadIdentityProfile": { + "platformWorkloadIdentities": { + "file-csi-driver": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-AzureFilesStorageOperator" }, + "cloud-controller-manager": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-CloudControllerManager" }, + "ingress": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ClusterIngressOperator" }, + "image-registry": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ImageRegistryOperator" }, + "machine-api": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-MachineApiOperator" }, + "cloud-network-config": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-NetworkOperator" }, + "aro-operator": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ServiceOperator" }, + "wrong-operator": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-StorageOperator" } + } +}, +` + extraPlatformIdentities := `"platformWorkloadIdentityProfile": { + "__CustomField1": "UpgradeableTo", + %s + "platformWorkloadIdentities": { + "file-csi-driver": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-AzureFilesStorageOperator" }, + "cloud-controller-manager": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-CloudControllerManager" }, + "ingress": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ClusterIngressOperator" }, + "image-registry": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ImageRegistryOperator" }, + "machine-api": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-MachineApiOperator" }, + "cloud-network-config": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-NetworkOperator" }, + "aro-operator": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ServiceOperator" }, + "disk-csi-driver": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-StorageOperator" }, + "extra-new-operator": { "resourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/extra-NewOperator" } + } +}, +` + upgradeableTo := fmt.Sprintf(`"upgradeableTo": "%s",`, getMIWIUpgradeableToVersion()) + clusterMSI := `"identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/cmsi": {} + } + }, + ` type test struct { - name string - preflightRequest func() *api.PreflightRequest - fixture func(*testdatabase.Fixture) - changeFeed map[string]*api.OpenShiftVersion - wantStatusCode int - wantError string - wantResponse *api.ValidationResult + name string + preflightRequest func() *api.PreflightRequest + fixture func(*testdatabase.Fixture) + ocpVersionsChangeFeed map[string]*api.OpenShiftVersion + wantStatusCode int + wantError string + wantResponse *api.ValidationResult } for _, tt := range []*test{ { @@ -118,7 +227,7 @@ func TestPreflightValidation(t *testing.T) { fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) }, - changeFeed: defaultVersionChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), preflightRequest: func() *api.PreflightRequest { return &api.PreflightRequest{ Resources: []json.RawMessage{ @@ -131,6 +240,186 @@ func TestPreflightValidation(t *testing.T) { Status: api.ValidationStatusSucceeded, }, }, + { + name: "Successful Preflight Create - MIWI", + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + preflightRequest: func() *api.PreflightRequest { + return &api.PreflightRequest{ + Resources: []json.RawMessage{ + []byte(fmt.Sprintf(managedIdentityClusterPreflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, clusterMSI, defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, defaultVersion, + platformIdentities, netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, ingressProfileIP, api.VisibilityPublic)), + }, + } + }, + wantStatusCode: http.StatusOK, + wantResponse: &api.ValidationResult{ + Status: api.ValidationStatusSucceeded, + }, + }, + { + name: "Failed Preflight Create - MIWI - PWI and Cluster MSI need each other", + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + preflightRequest: func() *api.PreflightRequest { + return &api.PreflightRequest{ + Resources: []json.RawMessage{ + []byte(fmt.Sprintf(managedIdentityClusterPreflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, "", defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, defaultVersion, + platformIdentities, netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, ingressProfileIP, api.VisibilityPublic)), + }, + } + }, + wantStatusCode: http.StatusOK, + wantResponse: &api.ValidationResult{ + Status: api.ValidationStatusFailed, + Error: &api.CloudErrorBody{ + Message: "400: InvalidParameter: identity: Cluster identity and platform workload identities require each other.", + }, + }, + }, + { + name: "Failed Preflight Create - MIWI - PWI and Service Principal both not provided", + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + preflightRequest: func() *api.PreflightRequest { + return &api.PreflightRequest{ + Resources: []json.RawMessage{ + []byte(fmt.Sprintf(managedIdentityClusterPreflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, "", defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, defaultVersion, + "", netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, ingressProfileIP, api.VisibilityPublic)), + }, + } + }, + wantStatusCode: http.StatusOK, + wantResponse: &api.ValidationResult{ + Status: api.ValidationStatusFailed, + Error: &api.CloudErrorBody{ + Message: "400: InvalidParameter: properties.servicePrincipalProfile: Must provide either an identity or service principal credentials.", + }, + }, + }, + { + name: "Failed Preflight Create - MIWI - Missing Workload Identity", + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + preflightRequest: func() *api.PreflightRequest { + return &api.PreflightRequest{ + Resources: []json.RawMessage{ + []byte(fmt.Sprintf(managedIdentityClusterPreflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, clusterMSI, defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, defaultVersion, + missingPlatformIdentities, netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, ingressProfileIP, api.VisibilityPublic)), + }, + } + }, + wantStatusCode: http.StatusOK, + wantResponse: &api.ValidationResult{ + Status: api.ValidationStatusFailed, + Error: &api.CloudErrorBody{ + Code: api.CloudErrorCodeInvalidParameter, + Message: unexpectedWorkloadIdentitiesError, + }, + }, + }, + { + name: "Failed Preflight Create - MIWI - Incorrect Workload Identity", + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + preflightRequest: func() *api.PreflightRequest { + return &api.PreflightRequest{ + Resources: []json.RawMessage{ + []byte(fmt.Sprintf(managedIdentityClusterPreflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, clusterMSI, defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, defaultVersion, + incorrectPlatformIdentities, netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, ingressProfileIP, api.VisibilityPublic)), + }, + } + }, + wantStatusCode: http.StatusOK, + wantResponse: &api.ValidationResult{ + Status: api.ValidationStatusFailed, + Error: &api.CloudErrorBody{ + Code: api.CloudErrorCodeInvalidParameter, + Message: unexpectedWorkloadIdentitiesError, + }, + }, + }, + { + name: "Failed Preflight Create - MIWI - Extra Workload Identity", + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + preflightRequest: func() *api.PreflightRequest { + return &api.PreflightRequest{ + Resources: []json.RawMessage{ + []byte(fmt.Sprintf(managedIdentityClusterPreflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, clusterMSI, defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, defaultVersion, + fmt.Sprintf(extraPlatformIdentities, ""), netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, ingressProfileIP, api.VisibilityPublic)), + }, + } + }, + wantStatusCode: http.StatusOK, + wantResponse: &api.ValidationResult{ + Status: api.ValidationStatusFailed, + Error: &api.CloudErrorBody{ + Code: api.CloudErrorCodeInvalidParameter, + Message: unexpectedWorkloadIdentitiesError, + }, + }, + }, { name: "Failed Preflight Static Invalid ResourceGroup", fixture: func(f *testdatabase.Fixture) { @@ -151,13 +440,17 @@ func TestPreflightValidation(t *testing.T) { "domain": "%s", "resourceGroupId": "/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/resourcenameTest", "fipsValidatedModules": "%s" + }, + "servicePrincipalProfile": { + "clientId": "%s", + "clientSecret": "%s" } } } `, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, - location, defaultProfile, api.FipsValidatedModulesEnabled)), + location, defaultProfile, api.FipsValidatedModulesEnabled, mockSubID, mockSubID)), }, } }, @@ -206,7 +499,7 @@ func TestPreflightValidation(t *testing.T) { fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) }, - changeFeed: defaultVersionChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), preflightRequest: func() *api.PreflightRequest { return &api.PreflightRequest{ Resources: []json.RawMessage{ @@ -341,7 +634,16 @@ func TestPreflightValidation(t *testing.T) { preflightRequest: func() *api.PreflightRequest { return &api.PreflightRequest{ Resources: []json.RawMessage{ - preflightPayload, + []byte(fmt.Sprintf(preflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, version.DefaultInstallStream.Version.String(), + mockSubID, mockSubID, netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, "", api.VisibilityPublic)), }, } }, @@ -386,6 +688,250 @@ func TestPreflightValidation(t *testing.T) { }, }, }, + { + name: "Failed Preflight Update Invalid Workload Identity", + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) + f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{ + Key: strings.ToLower(testdatabase.GetResourcePath(api.ExampleOpenShiftClusterDocument().ID, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name)), + OpenShiftCluster: &api.OpenShiftCluster{ + ID: testdatabase.GetResourcePath(api.ExampleOpenShiftClusterDocument().ID, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name), + Name: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + Type: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + Tags: map[string]string{"tag": "will-be-kept"}, + Location: location, + Identity: &api.ManagedServiceIdentity{ + Type: "UserAssigned", + UserAssignedIdentities: map[string]api.UserAssignedIdentity{ + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/cmsi": {}, + }, + }, + Properties: api.OpenShiftClusterProperties{ + ProvisioningState: api.ProvisioningStateSucceeded, + ClusterProfile: api.ClusterProfile{ + Domain: defaultProfile, + FipsValidatedModules: api.FipsValidatedModulesEnabled, + ResourceGroupID: resourceGroup, + Version: version.DefaultInstallStream.Version.String(), + }, + PlatformWorkloadIdentityProfile: &api.PlatformWorkloadIdentityProfile{ + PlatformWorkloadIdentities: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-AzureFilesStorageOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "cloud-controller-manager": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-CloudControllerManager", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "ingress": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ClusterIngressOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "image-registry": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ImageRegistryOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "machine-api": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-MachineApiOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "cloud-network-config": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-NetworkOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "aro-operator": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ServiceOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "disk-csi-driver": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-StorageOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + }, + }, + NetworkProfile: api.NetworkProfile{ + PodCIDR: netProfile, + ServiceCIDR: netProfile, + }, + MasterProfile: api.MasterProfile{ + VMSize: api.VMSizeStandardD32sV3, + SubnetID: masterSub, + DiskEncryptionSetID: encryptionSet, + EncryptionAtHost: api.EncryptionAtHostEnabled, + }, + WorkerProfiles: []api.WorkerProfile{ + { + Name: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, + VMSize: api.VMSizeStandardD32sV3, + DiskSizeGB: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + EncryptionAtHost: api.EncryptionAtHostEnabled, + SubnetID: workerSub, + Count: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + DiskEncryptionSetID: encryptionSet, + }, + }, + APIServerProfile: api.APIServerProfile{ + Visibility: api.VisibilityPublic, + }, + IngressProfiles: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.IngressProfiles, + }, + }, + }) + }, + preflightRequest: func() *api.PreflightRequest { + return &api.PreflightRequest{ + Resources: []json.RawMessage{ + []byte(fmt.Sprintf(managedIdentityClusterPreflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, clusterMSI, defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, defaultVersion, + fmt.Sprintf(extraPlatformIdentities, ""), netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, "", api.VisibilityPublic)), + }, + } + }, + wantStatusCode: http.StatusOK, + wantResponse: &api.ValidationResult{ + Status: api.ValidationStatusFailed, + Error: &api.CloudErrorBody{ + Code: api.CloudErrorCodeInvalidParameter, + Message: unexpectedWorkloadIdentitiesError, + }, + }, + }, + { + name: "Success Preflight Update - New Workload Identity with UpgradeableTo", + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(api.ExampleSubscriptionDocument()) + f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{ + Key: strings.ToLower(testdatabase.GetResourcePath(api.ExampleOpenShiftClusterDocument().ID, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name)), + OpenShiftCluster: &api.OpenShiftCluster{ + ID: testdatabase.GetResourcePath(api.ExampleOpenShiftClusterDocument().ID, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name), + Name: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + Type: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + Tags: map[string]string{"tag": "will-be-kept"}, + Location: location, + Identity: &api.ManagedServiceIdentity{ + Type: "UserAssigned", + UserAssignedIdentities: map[string]api.UserAssignedIdentity{ + "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/cmsi": {}, + }, + }, + Properties: api.OpenShiftClusterProperties{ + ProvisioningState: api.ProvisioningStateSucceeded, + ClusterProfile: api.ClusterProfile{ + Domain: defaultProfile, + FipsValidatedModules: api.FipsValidatedModulesEnabled, + ResourceGroupID: resourceGroup, + Version: version.DefaultInstallStream.Version.String(), + }, + PlatformWorkloadIdentityProfile: &api.PlatformWorkloadIdentityProfile{ + PlatformWorkloadIdentities: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-AzureFilesStorageOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "cloud-controller-manager": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-CloudControllerManager", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "ingress": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ClusterIngressOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "image-registry": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ImageRegistryOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "machine-api": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-MachineApiOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "cloud-network-config": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-NetworkOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "aro-operator": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-ServiceOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "disk-csi-driver": { + ResourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aro-StorageOperator", + ClientID: mockGuid, + ObjectID: mockGuid, + }, + }, + }, + NetworkProfile: api.NetworkProfile{ + PodCIDR: netProfile, + ServiceCIDR: netProfile, + }, + MasterProfile: api.MasterProfile{ + VMSize: api.VMSizeStandardD32sV3, + SubnetID: masterSub, + DiskEncryptionSetID: encryptionSet, + EncryptionAtHost: api.EncryptionAtHostEnabled, + }, + WorkerProfiles: []api.WorkerProfile{ + { + Name: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, + VMSize: api.VMSizeStandardD32sV3, + DiskSizeGB: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + EncryptionAtHost: api.EncryptionAtHostEnabled, + SubnetID: workerSub, + Count: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + DiskEncryptionSetID: encryptionSet, + }, + }, + APIServerProfile: api.APIServerProfile{ + Visibility: api.VisibilityPublic, + }, + IngressProfiles: api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.IngressProfiles, + }, + }, + }) + }, + preflightRequest: func() *api.PreflightRequest { + return &api.PreflightRequest{ + Resources: []json.RawMessage{ + []byte(fmt.Sprintf(managedIdentityClusterPreflightPayloadTemplate, apiVersion, clusterId, api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Name, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Type, + location, clusterMSI, defaultProfile, resourceGroup, api.EncryptionAtHostEnabled, defaultVersion, + fmt.Sprintf(extraPlatformIdentities, upgradeableTo), netProfile, netProfile, api.VMSizeStandardD32sV3, masterSub, + api.EncryptionAtHostEnabled, encryptionSet, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Name, api.VMSizeStandardD32sV3, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].DiskSizeGB, + api.EncryptionAtHostEnabled, workerSub, + api.ExampleOpenShiftClusterDocument().OpenShiftCluster.Properties.WorkerProfiles[0].Count, + encryptionSet, api.VisibilityPublic, defaultProfile, "", api.VisibilityPublic)), + }, + } + }, + wantStatusCode: http.StatusOK, + wantResponse: &api.ValidationResult{ + Status: api.ValidationStatusSucceeded, + }, + }, } { t.Run(tt.name, func(t *testing.T) { ti := newTestInfra(t). @@ -406,22 +952,19 @@ func TestPreflightValidation(t *testing.T) { go f.Run(ctx, nil, nil) f.ocpVersionsMu.Lock() - f.defaultOcpVersion = "4.13.40" - f.enabledOcpVersions = map[string]*api.OpenShiftVersion{ - f.defaultOcpVersion: { - Properties: api.OpenShiftVersionProperties{ - Version: f.defaultOcpVersion, - }, - }, - } + f.enabledOcpVersions = tt.ocpVersionsChangeFeed f.ocpVersionsMu.Unlock() + f.platformWorkloadIdentityRoleSetsMu.Lock() + f.availablePlatformWorkloadIdentityRoleSets = getPlatformWorkloadIdentityRolesChangeFeed() + f.platformWorkloadIdentityRoleSetsMu.Unlock() + headers := http.Header{ "Content-Type": []string{"application/json"}, } resp, b, err := ti.request(http.MethodPost, - "https://server"+testdatabase.GetPreflightPath(api.ExampleOpenShiftClusterDocument().ID, "deploymentName")+"?api-version=2020-04-30", + "https://server"+testdatabase.GetPreflightPath(api.ExampleOpenShiftClusterDocument().ID, "deploymentName")+"?api-version=2024-08-12-preview", headers, oc) if err != nil { t.Error(err) diff --git a/pkg/frontend/openshiftcluster_putorpatch.go b/pkg/frontend/openshiftcluster_putorpatch.go index 0658a34bc51..3eb22439819 100644 --- a/pkg/frontend/openshiftcluster_putorpatch.go +++ b/pkg/frontend/openshiftcluster_putorpatch.go @@ -239,6 +239,12 @@ func (f *frontend) _putOrPatchOpenShiftCluster(ctx context.Context, log *logrus. // is not provided in the header must be preserved f.systemDataClusterDocEnricher(doc, putOrPatchClusterParameters.systemData) + if doc.OpenShiftCluster.UsesWorkloadIdentity() { + if err := f.validatePlatformWorkloadIdentities(doc.OpenShiftCluster); err != nil { + return nil, err + } + } + if isCreate { err = f.validateInstallVersion(ctx, doc.OpenShiftCluster) if err != nil { diff --git a/pkg/frontend/openshiftcluster_putorpatch_test.go b/pkg/frontend/openshiftcluster_putorpatch_test.go index 068bb42c6f0..fe581374a62 100644 --- a/pkg/frontend/openshiftcluster_putorpatch_test.go +++ b/pkg/frontend/openshiftcluster_putorpatch_test.go @@ -27,10 +27,19 @@ import ( "github.com/Azure/ARO-RP/pkg/util/bucket" "github.com/Azure/ARO-RP/pkg/util/cmp" mock_frontend "github.com/Azure/ARO-RP/pkg/util/mocks/frontend" + "github.com/Azure/ARO-RP/pkg/util/pointerutils" "github.com/Azure/ARO-RP/pkg/util/version" testdatabase "github.com/Azure/ARO-RP/test/database" ) +const mockGuid = "00000000-0000-0000-0000-000000000000" + +var ( + defaultVersion = version.DefaultInstallStream.Version.String() + defaultMinorVersion = version.DefaultInstallStream.Version.MinorVersion() + unexpectedWorkloadIdentitiesError = fmt.Sprintf(`400: PlatformWorkloadIdentityMismatch: properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities: There's a mismatch between the required and expected set of platform workload identities for the requested OpenShift minor version '%s'. The required platform workload identities are '[aro-operator cloud-controller-manager cloud-network-config disk-csi-driver file-csi-driver image-registry ingress machine-api]'`, defaultMinorVersion) +) + type dummyOpenShiftClusterValidator struct{} func (*dummyOpenShiftClusterValidator) Static(interface{}, *api.OpenShiftCluster, string, string, bool, string) error { @@ -1958,8 +1967,6 @@ func TestPutOrPatchOpenShiftClusterAdminAPI(t *testing.T) { func TestPutOrPatchOpenShiftCluster(t *testing.T) { ctx := context.Background() - defaultVersion := "4.10.0" - apis := map[string]*api.Version{ "2024-08-12-preview": { OpenShiftClusterConverter: api.APIs["2024-08-12-preview"].OpenShiftClusterConverter, @@ -1968,24 +1975,6 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, } - ocpVersionsChangeFeed := map[string]*api.OpenShiftVersion{ - defaultVersion: { - Properties: api.OpenShiftVersionProperties{ - Version: defaultVersion, - Enabled: true, - Default: true, - }, - }, - "4.14.16": { - Properties: api.OpenShiftVersionProperties{ - Version: "4.14.16", - Enabled: true, - Default: false, - }, - }, - } - - mockGuid := "00000000-0000-0000-0000-000000000000" mockMiResourceId := "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/not-a-real-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/not-a-real-mi" mockCurrentTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) @@ -1994,7 +1983,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { request func(*v20240812preview.OpenShiftCluster) isPatch bool fixture func(*testdatabase.Fixture) - changeFeed map[string]*api.OpenShiftVersion + ocpVersionsChangeFeed map[string]*api.OpenShiftVersion quotaValidatorError error skuValidatorError error providersValidatorError error @@ -2027,7 +2016,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), wantSystemDataEnriched: true, wantDocuments: func(c *testdatabase.Checker) { c.AddAsyncOperationDocuments(&api.AsyncOperationDocument{ @@ -2144,7 +2133,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), wantSystemDataEnriched: true, wantDocuments: func(c *testdatabase.Checker) { c.AddAsyncOperationDocuments(&api.AsyncOperationDocument{ @@ -2260,10 +2249,130 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }, }, + { + name: "create a new workload identity cluster - unexpected workload identity provided", + request: func(oc *v20240812preview.OpenShiftCluster) { + oc.Properties.ClusterProfile.Version = defaultVersion + oc.Identity = &v20240812preview.ManagedServiceIdentity{ + Type: "UserAssigned", + UserAssignedIdentities: map[string]v20240812preview.UserAssignedIdentity{ + mockMiResourceId: {}, + }, + } + oc.Properties.PlatformWorkloadIdentityProfile = &v20240812preview.PlatformWorkloadIdentityProfile{ + PlatformWorkloadIdentities: map[string]v20240812preview.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "unexpected-identity": {ResourceID: mockMiResourceId}, + }, + } + }, + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(&api.SubscriptionDocument{ + ID: mockGuid, + Subscription: &api.Subscription{ + State: api.SubscriptionStateRegistered, + Properties: &api.SubscriptionProperties{ + TenantID: "11111111-1111-1111-1111-111111111111", + }, + }, + }) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + wantSystemDataEnriched: true, + wantEnriched: []string{}, + wantStatusCode: http.StatusBadRequest, + wantError: unexpectedWorkloadIdentitiesError, + }, + { + name: "create a new workload identity cluster - missing workload identity provided", + request: func(oc *v20240812preview.OpenShiftCluster) { + oc.Properties.ClusterProfile.Version = defaultVersion + oc.Identity = &v20240812preview.ManagedServiceIdentity{ + Type: "UserAssigned", + UserAssignedIdentities: map[string]v20240812preview.UserAssignedIdentity{ + mockMiResourceId: {}, + }, + } + oc.Properties.PlatformWorkloadIdentityProfile = &v20240812preview.PlatformWorkloadIdentityProfile{ + PlatformWorkloadIdentities: map[string]v20240812preview.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + }, + } + }, + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(&api.SubscriptionDocument{ + ID: mockGuid, + Subscription: &api.Subscription{ + State: api.SubscriptionStateRegistered, + Properties: &api.SubscriptionProperties{ + TenantID: "11111111-1111-1111-1111-111111111111", + }, + }, + }) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + wantSystemDataEnriched: true, + wantEnriched: []string{}, + wantStatusCode: http.StatusBadRequest, + wantError: unexpectedWorkloadIdentitiesError, + }, + { + name: "create a new workload identity cluster - extra workload identity provided", + request: func(oc *v20240812preview.OpenShiftCluster) { + oc.Properties.ClusterProfile.Version = defaultVersion + oc.Identity = &v20240812preview.ManagedServiceIdentity{ + Type: "UserAssigned", + UserAssignedIdentities: map[string]v20240812preview.UserAssignedIdentity{ + mockMiResourceId: {}, + }, + } + oc.Properties.PlatformWorkloadIdentityProfile = &v20240812preview.PlatformWorkloadIdentityProfile{ + PlatformWorkloadIdentities: map[string]v20240812preview.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + "extra-identity": {ResourceID: mockMiResourceId}, + }, + } + }, + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(&api.SubscriptionDocument{ + ID: mockGuid, + Subscription: &api.Subscription{ + State: api.SubscriptionStateRegistered, + Properties: &api.SubscriptionProperties{ + TenantID: "11111111-1111-1111-1111-111111111111", + }, + }, + }) + }, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + wantSystemDataEnriched: true, + wantEnriched: []string{}, + wantStatusCode: http.StatusBadRequest, + wantError: unexpectedWorkloadIdentitiesError, + }, { name: "create a new cluster vm not supported", request: func(oc *v20240812preview.OpenShiftCluster) { - oc.Properties.ClusterProfile.Version = "4.10.20" + oc.Properties.ClusterProfile.Version = defaultVersion }, fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(&api.SubscriptionDocument{ @@ -2276,16 +2385,16 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, - quotaValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The provided VM SKU %s is not supported.", "something"), - wantEnriched: []string{}, - wantStatusCode: http.StatusBadRequest, - wantError: "400: InvalidParameter: : The provided VM SKU something is not supported.", + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + quotaValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The provided VM SKU %s is not supported.", "something"), + wantEnriched: []string{}, + wantStatusCode: http.StatusBadRequest, + wantError: "400: InvalidParameter: : The provided VM SKU something is not supported.", }, { name: "create a new cluster quota fails", request: func(oc *v20240812preview.OpenShiftCluster) { - oc.Properties.ClusterProfile.Version = "4.10.20" + oc.Properties.ClusterProfile.Version = defaultVersion }, fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(&api.SubscriptionDocument{ @@ -2298,16 +2407,16 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, - quotaValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeQuotaExceeded, "", "Resource quota of vm exceeded. Maximum allowed: 0, Current in use: 0, Additional requested: 1."), - wantEnriched: []string{}, - wantStatusCode: http.StatusBadRequest, - wantError: "400: QuotaExceeded: : Resource quota of vm exceeded. Maximum allowed: 0, Current in use: 0, Additional requested: 1.", + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + quotaValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeQuotaExceeded, "", "Resource quota of vm exceeded. Maximum allowed: 0, Current in use: 0, Additional requested: 1."), + wantEnriched: []string{}, + wantStatusCode: http.StatusBadRequest, + wantError: "400: QuotaExceeded: : Resource quota of vm exceeded. Maximum allowed: 0, Current in use: 0, Additional requested: 1.", }, { name: "create a new cluster sku unavailable", request: func(oc *v20240812preview.OpenShiftCluster) { - oc.Properties.ClusterProfile.Version = "4.10.20" + oc.Properties.ClusterProfile.Version = defaultVersion }, fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(&api.SubscriptionDocument{ @@ -2320,16 +2429,16 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, - skuValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The selected SKU '%v' is unavailable in region '%v'", "Standard_Sku", "somewhere"), - wantEnriched: []string{}, - wantStatusCode: http.StatusBadRequest, - wantError: "400: InvalidParameter: : The selected SKU 'Standard_Sku' is unavailable in region 'somewhere'", + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + skuValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The selected SKU '%v' is unavailable in region '%v'", "Standard_Sku", "somewhere"), + wantEnriched: []string{}, + wantStatusCode: http.StatusBadRequest, + wantError: "400: InvalidParameter: : The selected SKU 'Standard_Sku' is unavailable in region 'somewhere'", }, { name: "create a new cluster sku restricted", request: func(oc *v20240812preview.OpenShiftCluster) { - oc.Properties.ClusterProfile.Version = "4.10.20" + oc.Properties.ClusterProfile.Version = defaultVersion }, fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(&api.SubscriptionDocument{ @@ -2342,17 +2451,17 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, - skuValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The selected SKU '%v' is restricted in region '%v' for selected subscription", "Standard_Sku", "somewhere"), - wantEnriched: []string{}, - wantStatusCode: http.StatusBadRequest, - wantError: "400: InvalidParameter: : The selected SKU 'Standard_Sku' is restricted in region 'somewhere' for selected subscription", + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), + skuValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeInvalidParameter, "", "The selected SKU '%v' is restricted in region '%v' for selected subscription", "Standard_Sku", "somewhere"), + wantEnriched: []string{}, + wantStatusCode: http.StatusBadRequest, + wantError: "400: InvalidParameter: : The selected SKU 'Standard_Sku' is restricted in region 'somewhere' for selected subscription", }, { name: "create a new cluster Microsoft.Authorization provider not registered", request: func(oc *v20240812preview.OpenShiftCluster) { - oc.Properties.ClusterProfile.Version = "4.10.20" + oc.Properties.ClusterProfile.Version = defaultVersion }, fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(&api.SubscriptionDocument{ @@ -2365,7 +2474,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), providersValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeResourceProviderNotRegistered, "", "The resource provider '%s' is not registered.", "Microsoft.Authorization"), wantEnriched: []string{}, wantStatusCode: http.StatusBadRequest, @@ -2374,7 +2483,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { { name: "create a new cluster Microsoft.Compute provider not registered", request: func(oc *v20240812preview.OpenShiftCluster) { - oc.Properties.ClusterProfile.Version = "4.10.20" + oc.Properties.ClusterProfile.Version = defaultVersion }, fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(&api.SubscriptionDocument{ @@ -2387,7 +2496,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), providersValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeResourceProviderNotRegistered, "", "The resource provider '%s' is not registered.", "Microsoft.Compute"), wantEnriched: []string{}, wantStatusCode: http.StatusBadRequest, @@ -2396,7 +2505,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { { name: "create a new cluster Microsoft.Network provider not registered", request: func(oc *v20240812preview.OpenShiftCluster) { - oc.Properties.ClusterProfile.Version = "4.10.20" + oc.Properties.ClusterProfile.Version = defaultVersion }, fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(&api.SubscriptionDocument{ @@ -2409,7 +2518,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), providersValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeResourceProviderNotRegistered, "", "The resource provider '%s' is not registered.", "Microsoft.Network"), wantEnriched: []string{}, wantStatusCode: http.StatusBadRequest, @@ -2418,7 +2527,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { { name: "create a new cluster Microsoft.Storage provider not registered", request: func(oc *v20240812preview.OpenShiftCluster) { - oc.Properties.ClusterProfile.Version = "4.10.20" + oc.Properties.ClusterProfile.Version = defaultVersion }, fixture: func(f *testdatabase.Fixture) { f.AddSubscriptionDocuments(&api.SubscriptionDocument{ @@ -2431,7 +2540,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), providersValidatorError: api.NewCloudError(http.StatusBadRequest, api.CloudErrorCodeResourceProviderNotRegistered, "", "The resource provider '%s' is not registered.", "Microsoft.Storage"), wantEnriched: []string{}, wantStatusCode: http.StatusBadRequest, @@ -2879,8 +2988,9 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { "cloud-network-config": {ResourceID: mockMiResourceId}, "aro-operator": {ResourceID: mockMiResourceId}, "disk-csi-driver": {ResourceID: mockMiResourceId}, - "new": {ResourceID: mockMiResourceId}, + "extra-new-operator": {ResourceID: mockMiResourceId}, }, + UpgradeableTo: pointerutils.ToPtr(v20240812preview.UpgradeableTo(getMIWIUpgradeableToVersion().String())), } }, isPatch: true, @@ -2917,6 +3027,9 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { MasterProfile: api.MasterProfile{ EncryptionAtHost: api.EncryptionAtHostDisabled, }, + ClusterProfile: api.ClusterProfile{ + Version: defaultVersion, + }, OperatorFlags: api.OperatorFlags{}, PlatformWorkloadIdentityProfile: &api.PlatformWorkloadIdentityProfile{ PlatformWorkloadIdentities: map[string]api.PlatformWorkloadIdentity{ @@ -2986,6 +3099,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { ProvisioningState: api.ProvisioningStateUpdating, LastProvisioningState: api.ProvisioningStateSucceeded, ClusterProfile: api.ClusterProfile{ + Version: defaultVersion, FipsValidatedModules: api.FipsValidatedModulesDisabled, }, IngressProfiles: []api.IngressProfile{{Name: "default"}}, @@ -3051,10 +3165,11 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { ClientID: mockGuid, ObjectID: mockGuid, }, - "new": { + "extra-new-operator": { ResourceID: mockMiResourceId, }, }, + UpgradeableTo: pointerutils.ToPtr(api.UpgradeableTo(getMIWIUpgradeableToVersion().String())), }, }, }, @@ -3072,6 +3187,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { Properties: v20240812preview.OpenShiftClusterProperties{ ProvisioningState: v20240812preview.ProvisioningStateUpdating, ClusterProfile: v20240812preview.ClusterProfile{ + Version: defaultVersion, FipsValidatedModules: v20240812preview.FipsValidatedModulesDisabled, }, IngressProfiles: []v20240812preview.IngressProfile{{Name: "default"}}, @@ -3136,14 +3252,123 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { ClientID: mockGuid, ObjectID: mockGuid, }, - "new": { + "extra-new-operator": { ResourceID: mockMiResourceId, }, }, + UpgradeableTo: pointerutils.ToPtr(v20240812preview.UpgradeableTo(getMIWIUpgradeableToVersion().String())), }, }, }, }, + { + name: "patch a workload identity cluster - unexpected identity provided", + request: func(oc *v20240812preview.OpenShiftCluster) { + oc.Properties.PlatformWorkloadIdentityProfile = &v20240812preview.PlatformWorkloadIdentityProfile{ + PlatformWorkloadIdentities: map[string]v20240812preview.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + "unexpected-operator": {ResourceID: mockMiResourceId}, + }, + UpgradeableTo: pointerutils.ToPtr(v20240812preview.UpgradeableTo(getMIWIUpgradeableToVersion().String())), + } + }, + isPatch: true, + fixture: func(f *testdatabase.Fixture) { + f.AddSubscriptionDocuments(&api.SubscriptionDocument{ + ID: mockGuid, + Subscription: &api.Subscription{ + State: api.SubscriptionStateRegistered, + Properties: &api.SubscriptionProperties{ + TenantID: "11111111-1111-1111-1111-111111111111", + }, + }, + }) + f.AddOpenShiftClusterDocuments(&api.OpenShiftClusterDocument{ + Key: strings.ToLower(testdatabase.GetResourcePath(mockGuid, "resourceName")), + OpenShiftCluster: &api.OpenShiftCluster{ + ID: testdatabase.GetResourcePath(mockGuid, "resourceName"), + Name: "resourceName", + Type: "Microsoft.RedHatOpenShift/openShiftClusters", + Tags: map[string]string{"tag": "will-be-kept"}, + Properties: api.OpenShiftClusterProperties{ + ProvisioningState: api.ProvisioningStateSucceeded, + IngressProfiles: []api.IngressProfile{{Name: "default"}}, + WorkerProfiles: []api.WorkerProfile{ + { + Name: "default", + EncryptionAtHost: api.EncryptionAtHostDisabled, + }, + }, + NetworkProfile: api.NetworkProfile{ + SoftwareDefinedNetwork: api.SoftwareDefinedNetworkOpenShiftSDN, + OutboundType: api.OutboundTypeLoadbalancer, + }, + MasterProfile: api.MasterProfile{ + EncryptionAtHost: api.EncryptionAtHostDisabled, + }, + ClusterProfile: api.ClusterProfile{ + Version: defaultVersion, + }, + OperatorFlags: api.OperatorFlags{}, + PlatformWorkloadIdentityProfile: &api.PlatformWorkloadIdentityProfile{ + PlatformWorkloadIdentities: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": { + ResourceID: mockMiResourceId, + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "cloud-controller-manager": { + ResourceID: mockMiResourceId, + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "ingress": { + ResourceID: mockMiResourceId, + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "image-registry": { + ResourceID: mockMiResourceId, + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "machine-api": { + ResourceID: mockMiResourceId, + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "cloud-network-config": { + ResourceID: mockMiResourceId, + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "aro-operator": { + ResourceID: mockMiResourceId, + ClientID: mockGuid, + ObjectID: mockGuid, + }, + "disk-csi-driver": { + ResourceID: mockMiResourceId, + ClientID: mockGuid, + ObjectID: mockGuid, + }, + }, + }, + }, + }, + }) + }, + wantSystemDataEnriched: true, + wantStatusCode: http.StatusBadRequest, + wantError: fmt.Sprintf(`400: PlatformWorkloadIdentityMismatch: properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities: There's a mismatch between the required and expected set of platform workload identities for the requested OpenShift minor version '%s or %s'. The required platform workload identities are '[aro-operator cloud-controller-manager cloud-network-config disk-csi-driver extra-new-operator file-csi-driver image-registry ingress machine-api]'`, defaultMinorVersion, getMIWIUpgradeableToVersion().MinorVersion()), + }, { name: "patch a cluster from failed during update", request: func(oc *v20240812preview.OpenShiftCluster) { @@ -3408,7 +3633,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), wantSystemDataEnriched: true, wantAsync: true, wantStatusCode: http.StatusBadRequest, @@ -3456,7 +3681,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), wantSystemDataEnriched: true, wantAsync: true, wantStatusCode: http.StatusBadRequest, @@ -3474,30 +3699,14 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { } oc.Properties.PlatformWorkloadIdentityProfile = &v20240812preview.PlatformWorkloadIdentityProfile{ PlatformWorkloadIdentities: map[string]v20240812preview.PlatformWorkloadIdentity{ - "AzureFilesStorageOperator": { - ResourceID: mockMiResourceId, - }, - "CloudControllerManager": { - ResourceID: mockMiResourceId, - }, - "ClusterIngressOperator": { - ResourceID: mockMiResourceId, - }, - "ImageRegistryOperator": { - ResourceID: mockMiResourceId, - }, - "MachineApiOperator": { - ResourceID: mockMiResourceId, - }, - "NetworkOperator": { - ResourceID: mockMiResourceId, - }, - "ServiceOperator": { - ResourceID: mockMiResourceId, - }, - "StorageOperator": { - ResourceID: mockMiResourceId, - }, + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, }, } }, @@ -3542,7 +3751,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { }, }) }, - changeFeed: ocpVersionsChangeFeed, + ocpVersionsChangeFeed: getOCPVersionsChangeFeed(), wantSystemDataEnriched: true, wantAsync: true, wantStatusCode: http.StatusBadRequest, @@ -3592,14 +3801,18 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) { go f.Run(ctx, nil, nil) f.ocpVersionsMu.Lock() - f.enabledOcpVersions = tt.changeFeed - for key, doc := range tt.changeFeed { + f.enabledOcpVersions = tt.ocpVersionsChangeFeed + for key, doc := range tt.ocpVersionsChangeFeed { if doc.Properties.Default { f.defaultOcpVersion = key } } f.ocpVersionsMu.Unlock() + f.platformWorkloadIdentityRoleSetsMu.Lock() + f.availablePlatformWorkloadIdentityRoleSets = getPlatformWorkloadIdentityRolesChangeFeed() + f.platformWorkloadIdentityRoleSetsMu.Unlock() + oc := &v20240812preview.OpenShiftCluster{} if tt.request != nil { tt.request(oc) @@ -4172,3 +4385,63 @@ func TestValidateIdentityTenantID(t *testing.T) { }) } } + +func getMIWIUpgradeableToVersion() *version.Version { + ver := version.DefaultInstallStream.Version.V + return version.NewVersion(ver[0], ver[1]+1, ver[2]) +} + +func getOCPVersionsChangeFeed() map[string]*api.OpenShiftVersion { + return map[string]*api.OpenShiftVersion{ + defaultVersion: { + Properties: api.OpenShiftVersionProperties{ + Version: defaultVersion, + Enabled: true, + Default: true, + }, + }, + getMIWIUpgradeableToVersion().String(): { + Properties: api.OpenShiftVersionProperties{ + Version: getMIWIUpgradeableToVersion().String(), + Enabled: true, + Default: false, + }, + }, + } +} + +func getPlatformWorkloadIdentityRolesChangeFeed() map[string]*api.PlatformWorkloadIdentityRoleSet { + return map[string]*api.PlatformWorkloadIdentityRoleSet{ + defaultMinorVersion: { + Properties: api.PlatformWorkloadIdentityRoleSetProperties{ + OpenShiftVersion: defaultMinorVersion, + PlatformWorkloadIdentityRoles: []api.PlatformWorkloadIdentityRole{ + {OperatorName: "cloud-controller-manager"}, + {OperatorName: "ingress"}, + {OperatorName: "machine-api"}, + {OperatorName: "disk-csi-driver"}, + {OperatorName: "cloud-network-config"}, + {OperatorName: "image-registry"}, + {OperatorName: "file-csi-driver"}, + {OperatorName: "aro-operator"}, + }, + }, + }, + getMIWIUpgradeableToVersion().MinorVersion(): { + Properties: api.PlatformWorkloadIdentityRoleSetProperties{ + OpenShiftVersion: getMIWIUpgradeableToVersion().MinorVersion(), + PlatformWorkloadIdentityRoles: []api.PlatformWorkloadIdentityRole{ + {OperatorName: "cloud-controller-manager"}, + {OperatorName: "ingress"}, + {OperatorName: "machine-api"}, + {OperatorName: "disk-csi-driver"}, + {OperatorName: "cloud-network-config"}, + {OperatorName: "image-registry"}, + {OperatorName: "file-csi-driver"}, + {OperatorName: "aro-operator"}, + {OperatorName: "extra-new-operator"}, + }, + }, + }, + } +} diff --git a/pkg/frontend/platformworkloadidentityrolesets_list.go b/pkg/frontend/platformworkloadidentityrolesets_list.go index c03d7de4062..37198c0d67c 100644 --- a/pkg/frontend/platformworkloadidentityrolesets_list.go +++ b/pkg/frontend/platformworkloadidentityrolesets_list.go @@ -4,7 +4,6 @@ package frontend // Licensed under the Apache License 2.0. import ( - "context" "encoding/json" "net/http" @@ -25,14 +24,14 @@ func (f *frontend) listPlatformWorkloadIdentityRoleSets(w http.ResponseWriter, r return } - roleSets := f.getAvailablePlatformWorkloadIdentityRoleSets(ctx) + roleSets := f.getAvailablePlatformWorkloadIdentityRoleSets() converter := f.apis[apiVersion].PlatformWorkloadIdentityRoleSetConverter b, err := json.MarshalIndent(converter.ToExternalList(roleSets), "", " ") reply(log, w, nil, b, err) } -func (f *frontend) getAvailablePlatformWorkloadIdentityRoleSets(ctx context.Context) []*api.PlatformWorkloadIdentityRoleSet { +func (f *frontend) getAvailablePlatformWorkloadIdentityRoleSets() []*api.PlatformWorkloadIdentityRoleSet { roleSets := make([]*api.PlatformWorkloadIdentityRoleSet, 0) f.platformWorkloadIdentityRoleSetsMu.RLock() diff --git a/pkg/frontend/platformworkloadidentityrolesets_validate.go b/pkg/frontend/platformworkloadidentityrolesets_validate.go new file mode 100644 index 00000000000..d851a13f616 --- /dev/null +++ b/pkg/frontend/platformworkloadidentityrolesets_validate.go @@ -0,0 +1,27 @@ +package frontend + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +import ( + "github.com/Azure/ARO-RP/pkg/api" + "github.com/Azure/ARO-RP/pkg/util/platformworkloadidentity" +) + +// validatePlatformWorkloadIdentities validates that customer provided platform workload identities are expected +func (f *frontend) validatePlatformWorkloadIdentities(oc *api.OpenShiftCluster) error { + roleSets := f.getAvailablePlatformWorkloadIdentityRoleSets() + + platformWorkloadIdentityRolesByVersionService := platformworkloadidentity.NewPlatformWorkloadIdentityRolesByVersionService() + err := platformWorkloadIdentityRolesByVersionService.PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets(oc, roleSets) + if err != nil { + return err + } + matches := platformWorkloadIdentityRolesByVersionService.MatchesPlatformWorkloadIdentityRoles(oc) + + if !matches { + return platformworkloadidentity.GetPlatformWorkloadIdentityMismatchError(oc, platformWorkloadIdentityRolesByVersionService.GetPlatformWorkloadIdentityRolesByRoleName()) + } + + return nil +} diff --git a/pkg/frontend/platformworkloadidentityrolesets_validate_test.go b/pkg/frontend/platformworkloadidentityrolesets_validate_test.go new file mode 100644 index 00000000000..6090ef151a5 --- /dev/null +++ b/pkg/frontend/platformworkloadidentityrolesets_validate_test.go @@ -0,0 +1,190 @@ +package frontend + +// Copyright (c) Microsoft Corporation. +// Licensed under the Apache License 2.0. + +import ( + "testing" + + "github.com/Azure/ARO-RP/pkg/api" + "github.com/Azure/ARO-RP/pkg/util/pointerutils" + utilerror "github.com/Azure/ARO-RP/test/util/error" +) + +func TestValidatePlatformWorkloadIdentities(t *testing.T) { + mockMiResourceId := "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/not-a-real-group/providers/Microsoft.ManagedIdentity/userAssignedIdentities/not-a-real-mi" + + for _, tt := range []struct { + test string + pwi map[string]api.PlatformWorkloadIdentity + version string + upgradeableTo *api.UpgradeableTo + wantErr string + }{ + { + test: "Success - Valid platform workload identities provided", + version: defaultVersion, + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + }, + }, + { + test: "Success - Valid platform workload identities provided with UpgradeableTo", + version: defaultVersion, + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + "extra-new-operator": {ResourceID: mockMiResourceId}, + }, + upgradeableTo: pointerutils.ToPtr(api.UpgradeableTo(getMIWIUpgradeableToVersion().String())), + }, + { + test: "Success - Valid platform workload identities provided with UpgradeableTo smaller than current version", + version: defaultVersion, + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + }, + upgradeableTo: pointerutils.ToPtr(api.UpgradeableTo("4.10.25")), + }, + { + test: "Fail - Not a MIWI Cluster", + version: defaultVersion, + wantErr: "PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets called for a Cluster Service Principal cluster", + }, + { + test: "Fail - Invalid Cluster Version", + version: "4.1052", + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + }, + wantErr: `could not parse version "4.1052"`, + }, + { + test: "Fail - Invalid upgradeableTo version", + version: defaultVersion, + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + }, + upgradeableTo: pointerutils.ToPtr(api.UpgradeableTo("4.1052")), + wantErr: `could not parse version "4.1052"`, + }, + { + test: "Fail - No roleset exists for the older version", + version: "4.10.14", + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + }, + wantErr: "400: InvalidParameter: : No PlatformWorkloadIdentityRoleSet found for the requested or upgradeable OpenShift minor version '4.10'. Please retry with different OpenShift version, and if the issue persists, raise an Azure support ticket", + }, + { + test: "Fail - Unexpected platform workload identity provided", + version: defaultVersion, + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "unexpected-identity": {ResourceID: mockMiResourceId}, + }, + wantErr: unexpectedWorkloadIdentitiesError, + }, + { + test: "Fail - Missing platform workload identity", + version: defaultVersion, + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + }, + wantErr: unexpectedWorkloadIdentitiesError, + }, + { + test: "Fail - Extra platform workload identity provided", + version: defaultVersion, + pwi: map[string]api.PlatformWorkloadIdentity{ + "file-csi-driver": {ResourceID: mockMiResourceId}, + "cloud-controller-manager": {ResourceID: mockMiResourceId}, + "ingress": {ResourceID: mockMiResourceId}, + "image-registry": {ResourceID: mockMiResourceId}, + "machine-api": {ResourceID: mockMiResourceId}, + "cloud-network-config": {ResourceID: mockMiResourceId}, + "aro-operator": {ResourceID: mockMiResourceId}, + "disk-csi-driver": {ResourceID: mockMiResourceId}, + "extra-identity": {ResourceID: mockMiResourceId}, + }, + wantErr: unexpectedWorkloadIdentitiesError, + }, + } { + t.Run(tt.test, func(t *testing.T) { + f := frontend{ + availablePlatformWorkloadIdentityRoleSets: getPlatformWorkloadIdentityRolesChangeFeed(), + } + + oc := &api.OpenShiftCluster{ + Properties: api.OpenShiftClusterProperties{ + ClusterProfile: api.ClusterProfile{ + Version: tt.version, + }, + }, + } + + if tt.pwi != nil { + oc.Properties.PlatformWorkloadIdentityProfile = &api.PlatformWorkloadIdentityProfile{ + PlatformWorkloadIdentities: tt.pwi, + UpgradeableTo: tt.upgradeableTo, + } + } + + err := f.validatePlatformWorkloadIdentities(oc) + utilerror.AssertErrorMessage(t, err, tt.wantErr) + }) + } +} diff --git a/pkg/util/mocks/platformworkloadidentity/platformworkloadidentity.go b/pkg/util/mocks/platformworkloadidentity/platformworkloadidentity.go index c66b27d90b7..98e126856f0 100644 --- a/pkg/util/mocks/platformworkloadidentity/platformworkloadidentity.go +++ b/pkg/util/mocks/platformworkloadidentity/platformworkloadidentity.go @@ -56,6 +56,20 @@ func (mr *MockPlatformWorkloadIdentityRolesByVersionMockRecorder) GetPlatformWor return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlatformWorkloadIdentityRolesByRoleName", reflect.TypeOf((*MockPlatformWorkloadIdentityRolesByVersion)(nil).GetPlatformWorkloadIdentityRolesByRoleName)) } +// MatchesPlatformWorkloadIdentityRoles mocks base method. +func (m *MockPlatformWorkloadIdentityRolesByVersion) MatchesPlatformWorkloadIdentityRoles(arg0 *api.OpenShiftCluster) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MatchesPlatformWorkloadIdentityRoles", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// MatchesPlatformWorkloadIdentityRoles indicates an expected call of MatchesPlatformWorkloadIdentityRoles. +func (mr *MockPlatformWorkloadIdentityRolesByVersionMockRecorder) MatchesPlatformWorkloadIdentityRoles(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MatchesPlatformWorkloadIdentityRoles", reflect.TypeOf((*MockPlatformWorkloadIdentityRolesByVersion)(nil).MatchesPlatformWorkloadIdentityRoles), arg0) +} + // PopulatePlatformWorkloadIdentityRolesByVersion mocks base method. func (m *MockPlatformWorkloadIdentityRolesByVersion) PopulatePlatformWorkloadIdentityRolesByVersion(arg0 context.Context, arg1 *api.OpenShiftCluster, arg2 database.PlatformWorkloadIdentityRoleSets) error { m.ctrl.T.Helper() @@ -69,3 +83,17 @@ func (mr *MockPlatformWorkloadIdentityRolesByVersionMockRecorder) PopulatePlatfo mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PopulatePlatformWorkloadIdentityRolesByVersion", reflect.TypeOf((*MockPlatformWorkloadIdentityRolesByVersion)(nil).PopulatePlatformWorkloadIdentityRolesByVersion), arg0, arg1, arg2) } + +// PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets mocks base method. +func (m *MockPlatformWorkloadIdentityRolesByVersion) PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets(arg0 *api.OpenShiftCluster, arg1 []*api.PlatformWorkloadIdentityRoleSet) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets indicates an expected call of PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets. +func (mr *MockPlatformWorkloadIdentityRolesByVersionMockRecorder) PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets", reflect.TypeOf((*MockPlatformWorkloadIdentityRolesByVersion)(nil).PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets), arg0, arg1) +} diff --git a/pkg/util/platformworkloadidentity/platformworkloadidentityrolesbyversion.go b/pkg/util/platformworkloadidentity/platformworkloadidentityrolesbyversion.go index 302f5263f93..e9461fec251 100644 --- a/pkg/util/platformworkloadidentity/platformworkloadidentityrolesbyversion.go +++ b/pkg/util/platformworkloadidentity/platformworkloadidentityrolesbyversion.go @@ -18,6 +18,8 @@ import ( type PlatformWorkloadIdentityRolesByVersion interface { GetPlatformWorkloadIdentityRolesByRoleName() map[string]api.PlatformWorkloadIdentityRole PopulatePlatformWorkloadIdentityRolesByVersion(ctx context.Context, oc *api.OpenShiftCluster, dbPlatformWorkloadIdentityRoleSets database.PlatformWorkloadIdentityRoleSets) error + PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets(oc *api.OpenShiftCluster, platformWorkloadIdentityRoleSets []*api.PlatformWorkloadIdentityRoleSet) error + MatchesPlatformWorkloadIdentityRoles(oc *api.OpenShiftCluster) bool } // platformWorkloadIdentityRolesByVersionService is the default implementation of the PlatformWorkloadIdentityRolesByVersion interface. @@ -38,17 +40,34 @@ func (service *PlatformWorkloadIdentityRolesByVersionService) PopulatePlatformWo if !oc.UsesWorkloadIdentity() { return fmt.Errorf("PopulatePlatformWorkloadIdentityRolesByVersion called for a Cluster Service Principal cluster") } - currentOpenShiftVersion, err := version.ParseVersion(oc.Properties.ClusterProfile.Version) + + docs, err := dbPlatformWorkloadIdentityRoleSets.ListAll(ctx) if err != nil { return err } - currentMinorVersion := currentOpenShiftVersion.MinorVersion() - requiredMinorVersions := map[string]bool{currentMinorVersion: false} - docs, err := dbPlatformWorkloadIdentityRoleSets.ListAll(ctx) + platformWorkloadIdentityRoleSets := []*api.PlatformWorkloadIdentityRoleSet{} + + for _, doc := range docs.PlatformWorkloadIdentityRoleSetDocuments { + platformWorkloadIdentityRoleSets = append(platformWorkloadIdentityRoleSets, doc.PlatformWorkloadIdentityRoleSet) + } + + return service.PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets(oc, platformWorkloadIdentityRoleSets) +} + +// PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets aims to populate platformWorkloadIdentityRoles for current OpenShift minor version and also for UpgradeableTo minor version if provided and is greater than the current version +// Rather than listing platformWorkloadIdentityRoleSets from db it takes platformWorkloadIdentityRoleSets as an argument +func (service *PlatformWorkloadIdentityRolesByVersionService) PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets(oc *api.OpenShiftCluster, platformWorkloadIdentityRoleSets []*api.PlatformWorkloadIdentityRoleSet) error { + if !oc.UsesWorkloadIdentity() { + return fmt.Errorf("PopulatePlatformWorkloadIdentityRolesByVersionUsingRoleSets called for a Cluster Service Principal cluster") + } + + currentOpenShiftVersion, err := version.ParseVersion(oc.Properties.ClusterProfile.Version) if err != nil { return err } + currentMinorVersion := currentOpenShiftVersion.MinorVersion() + requiredMinorVersions := map[string]bool{currentMinorVersion: false} if oc.Properties.PlatformWorkloadIdentityProfile.UpgradeableTo != nil { upgradeableVersion, err := version.ParseVersion(string(*oc.Properties.PlatformWorkloadIdentityProfile.UpgradeableTo)) @@ -61,10 +80,10 @@ func (service *PlatformWorkloadIdentityRolesByVersionService) PopulatePlatformWo } } - for _, doc := range docs.PlatformWorkloadIdentityRoleSetDocuments { + for _, platformWorkloadIdentityRoleSet := range platformWorkloadIdentityRoleSets { for version := range requiredMinorVersions { - if version == doc.PlatformWorkloadIdentityRoleSet.Properties.OpenShiftVersion { - service.platformWorkloadIdentityRoles = append(service.platformWorkloadIdentityRoles, doc.PlatformWorkloadIdentityRoleSet.Properties.PlatformWorkloadIdentityRoles...) + if version == platformWorkloadIdentityRoleSet.Properties.OpenShiftVersion { + service.platformWorkloadIdentityRoles = append(service.platformWorkloadIdentityRoles, platformWorkloadIdentityRoleSet.Properties.PlatformWorkloadIdentityRoles...) requiredMinorVersions[version] = true } } @@ -87,6 +106,27 @@ func (service *PlatformWorkloadIdentityRolesByVersionService) GetPlatformWorkloa return platformWorkloadIdentityRolesByRoleName } +// Check if required platform identity are provided in cluster doc by assessing +// Condition 1: Platform Workload Identities and Platform Workload Identity Roles should be equal in length +// Condition 2: All the keys in Platform Workload Identities map should be present in Platform Workload Identity Roles +// These conditions also assures if Platform Workload Identities contains all the keys present in Platform Workload Identity Roles +func (service *PlatformWorkloadIdentityRolesByVersionService) MatchesPlatformWorkloadIdentityRoles(oc *api.OpenShiftCluster) bool { + platformWorkloadIdentityRolesByRoleName := service.GetPlatformWorkloadIdentityRolesByRoleName() + platformIdentities := oc.Properties.PlatformWorkloadIdentityProfile.PlatformWorkloadIdentities + if len(platformIdentities) != len(platformWorkloadIdentityRolesByRoleName) { + return false + } + + for k := range platformIdentities { + _, exists := platformWorkloadIdentityRolesByRoleName[k] + if !exists { + return false + } + } + + return true +} + func GetPlatformWorkloadIdentityMismatchError(oc *api.OpenShiftCluster, platformWorkloadIdentityRolesByRoleName map[string]api.PlatformWorkloadIdentityRole) error { if !oc.UsesWorkloadIdentity() { return fmt.Errorf("GetPlatformWorkloadIdentityMismatchError called for a Cluster Service Principal cluster")