From d1208392c1ba84f0457f9a76bf4c0cd4ccc7bf63 Mon Sep 17 00:00:00 2001 From: Lance Galletti Date: Wed, 15 Nov 2023 11:24:54 -0500 Subject: [PATCH] Azure workload identity for layered OLM operators --- Makefile | 4 + pkg/apis/cloudcredential/v1/types_azure.go | 18 ++ pkg/azure/actuator.go | 155 +++++++++--- pkg/azure/actuator_test.go | 235 +++++++++++++++++- pkg/azure/const.go | 15 +- .../credentialsrequest_controller.go | 2 +- test/e2e/azure/azident/actutator_e2e_test.go | 134 ++++++++++ 7 files changed, 515 insertions(+), 48 deletions(-) create mode 100644 test/e2e/azure/azident/actutator_e2e_test.go diff --git a/Makefile b/Makefile index 18c9bc5494..a83417b36d 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,10 @@ test-e2e-sts: go test -mod=vendor -race -tags e2e ./test/e2e/aws/sts/... .PHONY: test-e2e-sts +test-e2e-azident: + go test -mod=vendor -race -tags e2e ./test/e2e/azure/azident/... +.PHONY: test-e2e-azident + vet: verify-govet .PHONY: vet diff --git a/pkg/apis/cloudcredential/v1/types_azure.go b/pkg/apis/cloudcredential/v1/types_azure.go index 0901c4e39c..1722bff5ab 100644 --- a/pkg/apis/cloudcredential/v1/types_azure.go +++ b/pkg/apis/cloudcredential/v1/types_azure.go @@ -47,6 +47,24 @@ type AzureProviderSpec struct { // and RoleBindings. // +optional DataPermissions []string `json:"dataPermissions,omitempty"` + + // The following fields are only required for Azure Workload Identity. + // AzureClientID is the ID of the specific application you created in Azure + // +optional + AzureClientID string `json:"azureClientID,omitempty"` + + // AzureRegion is the geographic region of the Azure service. + // +optional + AzureRegion string `json:"azureRegion,omitempty"` + + // Each Azure subscription has an ID associated with it, as does the tenant to which a subscription belongs. + // AzureSubscriptionID is the ID of the subscription. + // +optional + AzureSubscriptionID string `json:"azureSubscriptionID,omitempty"` + + // AzureTenantID is the ID of the tenant to which the subscription belongs. + // +optional + AzureTenantID string `json:"azureTenantID,omitempty"` } // RoleBinding models part of the Azure RBAC Role Binding diff --git a/pkg/azure/actuator.go b/pkg/azure/actuator.go index 8b8b88dfae..94b2e71c25 100644 --- a/pkg/azure/actuator.go +++ b/pkg/azure/actuator.go @@ -36,6 +36,7 @@ import ( operatorv1 "github.com/openshift/api/operator/v1" minterv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1" + "github.com/openshift/cloud-credential-operator/pkg/cmd/provisioning" "github.com/openshift/cloud-credential-operator/pkg/operator/constants" actuatoriface "github.com/openshift/cloud-credential-operator/pkg/operator/credentialsrequest/actuator" "github.com/openshift/cloud-credential-operator/pkg/operator/utils" @@ -68,20 +69,6 @@ func NewFakeActuator(c, rootCredClient client.Client, } } -func (a *Actuator) IsValidMode() error { - mode, err := a.client.Mode(context.Background()) - if err != nil { - return err - } - - switch mode { - case constants.PassthroughAnnotation: - return nil - default: - return errors.New("invalid mode") - } -} - func isAzureCredentials(providerSpec *runtime.RawExtension) (bool, error) { var err error unknown := runtime.Unknown{} @@ -108,6 +95,19 @@ func (a *Actuator) needsUpdate(ctx context.Context, cr *minterv1.CredentialsRequ if !exists { return true, nil } + + // Manual mode just update + credentialsMode, _, err := utils.GetOperatorConfiguration(a.client, logger) + if err != nil { + logger.WithError(err).Error("error loading CCO configuration to determine valid mode") + return true, err + } + if credentialsMode == operatorv1.CloudCredentialsModeManual { + return true, nil + } + if credentialsMode == operatorv1.CloudCredentialsModeMint { + return false, errors.New("mint mode is invalid") + } // passthrough-specifc checks here (now the only kind of checks...) credentialsRootSecret, err := a.GetCredentialsRootSecret(ctx, cr) @@ -160,11 +160,17 @@ func (a *Actuator) Delete(ctx context.Context, cr *minterv1.CredentialsRequest) if isAzure, err := isAzureCredentials(cr.Spec.ProviderSpec); !isAzure { return err } - if err := a.IsValidMode(); err != nil { + logger := a.getLogger(cr) + credentialsMode, _, err := utils.GetOperatorConfiguration(a.client, logger) + if err != nil { + logger.WithError(err).Error("error loading CCO configuration to determine valid mode") return err } + if credentialsMode == operatorv1.CloudCredentialsModeManual { + logger.Debug("running delete in manual mode") + return nil + } - logger := a.getLogger(cr) logger.Debug("running delete") credentialsRootSecret, err := a.GetCredentialsRootSecret(ctx, cr) @@ -230,6 +236,52 @@ func (a *Actuator) sync(ctx context.Context, cr *minterv1.CredentialsRequest) er logger.Debug("credentials already up to date") return nil } + stsDetected, err := utils.IsTimedTokenCluster(a.client, ctx, logger) + if err != nil { + return err + } + if stsDetected { + logger.Debug("actuator detected Azure AD Workload Identity enabled cluster, enabling Workload Identity secret brokering for CredentialsRequests providing a Managed Identity") + azureProviderSpec, err := decodeProviderSpec(minterv1.Codec, cr) + if err != nil { + return err + } + azureFederatedTokenFile := cr.Spec.CloudTokenPath + if cr.Spec.CloudTokenPath == "" { + logger.Debugf("CredentialsRequest has no cloudTokenPath, defaulting azure_federated_token_file to %s", provisioning.OidcTokenPath) + azureFederatedTokenFile = provisioning.OidcTokenPath + } + // Check for old Manual Mode where all 4 fields are empty - defaulting to old behavior + // where CCO exists and the secret is created manually + if azureProviderSpec.AzureClientID == "" && azureProviderSpec.AzureTenantID == "" && azureProviderSpec.AzureSubscriptionID == "" && azureProviderSpec.AzureRegion == "" { + return nil + } + err = validateAzureProviderSpec(*azureProviderSpec) + if err != nil { + // At least one of the fields was set indicating that the new workload identity + // behavior of creating the secret is desired but not all fields required were + // provided. + msg := "error validating credentials request Azure AD Workload Identity fields" + return &actuatoriface.ActuatorError{ + ErrReason: minterv1.CredentialsProvisionFailure, + Message: fmt.Sprintf("%v: %v", msg, err), + } + } + desiredSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: cr.Spec.SecretRef.Name, + Namespace: cr.Spec.SecretRef.Namespace, + }, + StringData: map[string]string{ + AzureClientID: azureProviderSpec.AzureClientID, + AzureTenantID: azureProviderSpec.AzureTenantID, + AzureRegion: azureProviderSpec.AzureRegion, + AzureSubscriptionID: azureProviderSpec.AzureSubscriptionID, + AzureFederatedTokenFile: azureFederatedTokenFile, + }, + } + return a.syncCredentialSecrets(ctx, cr, desiredSecret, logger) + } credentialsRootSecret, err := a.GetCredentialsRootSecret(ctx, cr) if err != nil { @@ -259,7 +311,6 @@ func (a *Actuator) sync(ctx context.Context, cr *minterv1.CredentialsRequest) er Message: msg, } } - return nil } @@ -370,7 +421,25 @@ func (a *Actuator) cleanupAfterPassthroughPivot(ctx context.Context, cr *minterv return nil } -func (a *Actuator) syncCredentialSecrets(ctx context.Context, cr *minterv1.CredentialsRequest, cloudCredsSecret *corev1.Secret, logger log.FieldLogger) error { + +// For Azure Workload Identity, the generated Secret needs to look like this: +/* + apiVersion: v1 + stringData: + azure_client_id: 0420bfd1-ab26-4b80-a9ac-deadbeeff1f9 + azure_tenant_id: 6047c7e9-b2ad-488d-a54e-deadbeefa7ee + azure_region: centralus + azure_subscription_id: 8c20ec23-8478-4f46-96f4-deadbeeff185 + azure_federated_token_file: /var/run/secrets/openshift/serviceaccount/token + kind: Secret + metadata: + name: azure-cloud-credentials + namespace: openshift-machine-api + type: Opaque +*/ +// The first 4 fields need to come from `spec.ProviderSpec` in the CredentialsRequest with +// spec.cloudTokenPath matching up to: `azure_federated_token_file` +func (a *Actuator) syncCredentialSecrets(ctx context.Context, cr *minterv1.CredentialsRequest, desiredSecret *corev1.Secret, logger log.FieldLogger) error { sLog := logger.WithFields(log.Fields{ "targetSecret": fmt.Sprintf("%s/%s", cr.Spec.SecretRef.Namespace, cr.Spec.SecretRef.Name), "cr": fmt.Sprintf("%s/%s", cr.Namespace, cr.Name), @@ -391,16 +460,24 @@ func (a *Actuator) syncCredentialSecrets(ctx context.Context, cr *minterv1.Crede secret.Annotations = map[string]string{} } secret.Annotations[minterv1.AnnotationCredentialsRequest] = fmt.Sprintf("%s/%s", cr.Namespace, cr.Name) + if desiredSecret.Data == nil { + if secret.StringData == nil { + secret.StringData = map[string]string{} + } + secret.StringData = desiredSecret.StringData + secret.Type = corev1.SecretTypeOpaque + return nil + } if secret.Data == nil { secret.Data = map[string][]byte{} } - secret.Data[AzureClientID] = cloudCredsSecret.Data[AzureClientID] - secret.Data[AzureClientSecret] = cloudCredsSecret.Data[AzureClientSecret] - secret.Data[AzureRegion] = cloudCredsSecret.Data[AzureRegion] - secret.Data[AzureResourceGroup] = cloudCredsSecret.Data[AzureResourceGroup] - secret.Data[AzureResourcePrefix] = cloudCredsSecret.Data[AzureResourcePrefix] - secret.Data[AzureSubscriptionID] = cloudCredsSecret.Data[AzureSubscriptionID] - secret.Data[AzureTenantID] = cloudCredsSecret.Data[AzureTenantID] + secret.Data[AzureClientID] = desiredSecret.Data[AzureClientID] + secret.Data[AzureClientSecret] = desiredSecret.Data[AzureClientSecret] + secret.Data[AzureRegion] = desiredSecret.Data[AzureRegion] + secret.Data[AzureResourceGroup] = desiredSecret.Data[AzureResourceGroup] + secret.Data[AzureResourcePrefix] = desiredSecret.Data[AzureResourcePrefix] + secret.Data[AzureSubscriptionID] = desiredSecret.Data[AzureSubscriptionID] + secret.Data[AzureTenantID] = desiredSecret.Data[AzureTenantID] return nil }) sLog.WithField("operation", op).Info("processed secret") @@ -463,9 +540,6 @@ func (a *Actuator) Exists(ctx context.Context, cr *minterv1.CredentialsRequest) if isAzure, err := isAzureCredentials(cr.Spec.ProviderSpec); !isAzure { return false, err } - if err := a.IsValidMode(); err != nil { - return false, err - } existingSecret := &corev1.Secret{} err := a.client.Get(ctx, types.NamespacedName{Namespace: cr.Spec.SecretRef.Namespace, Name: cr.Spec.SecretRef.Name}, existingSecret) @@ -492,3 +566,28 @@ func (a *Actuator) getLogger(cr *minterv1.CredentialsRequest) log.FieldLogger { func (a *Actuator) Upgradeable(mode operatorv1.CloudCredentialsMode) *configv1.ClusterOperatorStatusCondition { return utils.UpgradeableCheck(a.client.RootCredClient, mode, a.GetCredentialsRootSecretLocation()) } + +func validateAzureProviderSpec(azureProviderSpec minterv1.AzureProviderSpec) error { + var errors []error + isEmptyAzureClientID := azureProviderSpec.AzureClientID == "" + isEmptyAzureTenantID := azureProviderSpec.AzureTenantID == "" + isEmptyAzureSubscriptionID := azureProviderSpec.AzureSubscriptionID == "" + isEmptyAzureRegion := azureProviderSpec.AzureRegion == "" + + if isEmptyAzureClientID { + errors = append(errors, fmt.Errorf("AzureClientID must not be empty")) + } + if isEmptyAzureTenantID { + errors = append(errors, fmt.Errorf("AzureTenantID must not be empty")) + } + if isEmptyAzureRegion { + errors = append(errors, fmt.Errorf("AzureRegion must not be empty")) + } + if isEmptyAzureSubscriptionID { + errors = append(errors, fmt.Errorf("AzureSubscriptionID must not be empty")) + } + if len(errors) > 0 { + return fmt.Errorf("AzureProviderSpec validation failed: %v", errors) + } + return nil +} diff --git a/pkg/azure/actuator_test.go b/pkg/azure/actuator_test.go index 7d1274ac7e..ab755690e5 100644 --- a/pkg/azure/actuator_test.go +++ b/pkg/azure/actuator_test.go @@ -18,11 +18,16 @@ package azure_test import ( "context" + "encoding/json" "fmt" "reflect" "testing" "time" + operatorv1 "github.com/openshift/api/operator/v1" + "github.com/openshift/cloud-credential-operator/pkg/cmd/provisioning" + schemeutils "github.com/openshift/cloud-credential-operator/pkg/util" + "github.com/golang/mock/gomock" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -76,6 +81,11 @@ const ( testTargetSecretNamespace = "my-namespace" testTargetSecretName = "my-secret" + + workloadIdentityRegion = "wi_region" + workloadIdentityClientID = "wi_client_id" + workloadIdentityTenantID = "wi_tenant_id" + workloadIdentitySubscriptionID = "wi_subscription_id" ) var ( @@ -117,6 +127,20 @@ var ( }, } + validWorkloadIdentitySecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testTargetSecretName, + Namespace: testTargetSecretNamespace, + }, + StringData: map[string]string{ + azure.AzureClientID: workloadIdentityClientID, + azure.AzureTenantID: workloadIdentityTenantID, + azure.AzureRegion: workloadIdentityRegion, + azure.AzureSubscriptionID: workloadIdentitySubscriptionID, + azure.AzureFederatedTokenFile: provisioning.OidcTokenPath, + }, + } + rootSecretNoAnnotation = corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: constants.AzureCloudCredSecretName, @@ -185,13 +209,7 @@ func getProviderStatus(t *testing.T, cr *minterv1.CredentialsRequest) minterv1.A } func TestActuator(t *testing.T) { - if err := openshiftapiv1.Install(scheme.Scheme); err != nil { - t.Fatal(err) - } - - if err := minterv1.AddToScheme(scheme.Scheme); err != nil { - t.Fatal(err) - } + schemeutils.SetupScheme(scheme.Scheme) tests := []struct { name string @@ -204,8 +222,12 @@ func TestActuator(t *testing.T) { validate func(*testing.T, client.Client, client.Client) }{ { - name: "Process a CredentialsRequest", - existing: defaultExistingObjects(), + name: "Process a CredentialsRequest", + existing: func() []runtime.Object { + objects := defaultExistingObjects() + objects = append(objects, testOperatorConfig(operatorv1.CloudCredentialsModePassthrough)) + return objects + }(), existingAdmin: []runtime.Object{&validPassthroughRootSecret}, credentialsRequest: testCredentialsRequest(t), op: func(actuator *azure.Actuator, cr *minterv1.CredentialsRequest) error { @@ -245,6 +267,7 @@ func TestActuator(t *testing.T) { } objects = append(objects, targetSecret) + objects = append(objects, testOperatorConfig(operatorv1.CloudCredentialsModePassthrough)) return objects }(), @@ -323,6 +346,7 @@ func TestActuator(t *testing.T) { } objects = append(objects, targetSecret) + objects = append(objects, testOperatorConfig(operatorv1.CloudCredentialsModePassthrough)) return objects }(), @@ -406,6 +430,7 @@ func TestActuator(t *testing.T) { } objects = append(objects, targetSecret) + objects = append(objects, testOperatorConfig(operatorv1.CloudCredentialsModePassthrough)) return objects }(), @@ -453,7 +478,28 @@ func TestActuator(t *testing.T) { name: "Missing annotation", expectedErr: fmt.Errorf("error determining whether a credentials update is needed"), existing: func() []runtime.Object { - return nil + objects := defaultExistingObjects() + // Add the existing targetSecret since we are mocking up + // a previously migrated scenario. + targetSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testTargetSecretName, + Namespace: testTargetSecretNamespace, + }, + Data: map[string][]byte{ + azure.AzureClientID: []byte(mintedClientID), + azure.AzureClientSecret: []byte(mintedClientSecret), + azure.AzureRegion: []byte(rootRegion), + azure.AzureResourceGroup: []byte(rootResourceGroup), + azure.AzureResourcePrefix: []byte(rootResourcePrefix), + azure.AzureSubscriptionID: []byte(rootSubscriptionID), + azure.AzureTenantID: []byte(rootTenantID), + }, + } + + objects = append(objects, targetSecret) + objects = append(objects, testOperatorConfig(operatorv1.CloudCredentialsModePassthrough)) + return objects }(), existingAdmin: []runtime.Object{&rootSecretNoAnnotation}, credentialsRequest: testCredentialsRequest(t), @@ -463,9 +509,31 @@ func TestActuator(t *testing.T) { }, { name: "Mint annotation", - expectedErr: fmt.Errorf("error determining whether a credentials update is needed"), + expectedErr: fmt.Errorf("unexpected value or missing cloudcredential.openshift.io/mode annotation on admin credentials Secret"), existing: func() []runtime.Object { - return nil + // Note: required now because of the isTimedToken() function + objects := defaultExistingObjects() + // Add the existing targetSecret since we are mocking up + // a previously migrated scenario. + targetSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testTargetSecretName, + Namespace: testTargetSecretNamespace, + }, + Data: map[string][]byte{ + azure.AzureClientID: []byte(mintedClientID), + azure.AzureClientSecret: []byte(mintedClientSecret), + azure.AzureRegion: []byte(rootRegion), + azure.AzureResourceGroup: []byte(rootResourceGroup), + azure.AzureResourcePrefix: []byte(rootResourcePrefix), + azure.AzureSubscriptionID: []byte(rootSubscriptionID), + azure.AzureTenantID: []byte(rootTenantID), + }, + } + + objects = append(objects, targetSecret) + objects = append(objects, testOperatorConfig(operatorv1.CloudCredentialsModePassthrough)) + return objects }(), existingAdmin: []runtime.Object{&rootSecretMintAnnotation}, credentialsRequest: testCredentialsRequest(t), @@ -479,9 +547,11 @@ func TestActuator(t *testing.T) { t.Run(test.name, func(t *testing.T) { allObjects := append(test.existing, test.credentialsRequest) fakeClient := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). WithStatusSubresource(&minterv1.CredentialsRequest{}). WithRuntimeObjects(allObjects...).Build() fakeAdminClient := fake.NewClientBuilder(). + WithScheme(scheme.Scheme). WithRuntimeObjects(test.existingAdmin...).Build() mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -521,6 +591,119 @@ func TestActuator(t *testing.T) { } } +func TestActuatorCreateOnWorkloadIdentity(t *testing.T) { + schemeutils.SetupScheme(scheme.Scheme) + + tests := []struct { + name string + existing []runtime.Object + cr *minterv1.CredentialsRequest + mockAppClient func(*gomock.Controller) *azuremock.MockAppClient + wantErr assert.ErrorAssertionFunc + validate func(*testing.T, client.Client) + }{ + { + name: "Correctly configured Azure Workload Identity fields", + existing: func() []runtime.Object { + objects := defaultExistingObjects() + objects = append(objects, testAuthentication("issuer")) + objects = append(objects, testOperatorConfig(operatorv1.CloudCredentialsModeManual)) + return objects + }(), + cr: func() *minterv1.CredentialsRequest { + cr := testCredentialsRequest(t) + + azureSpec := &minterv1.AzureProviderSpec{} + err := minterv1.Codec.DecodeProviderSpec(cr.Spec.ProviderSpec, azureSpec) + require.NoError(t, err, "error decoding provider spec") + + azureSpec.AzureClientID = workloadIdentityClientID + azureSpec.AzureTenantID = workloadIdentityTenantID + azureSpec.AzureRegion = workloadIdentityTenantID + azureSpec.AzureSubscriptionID = workloadIdentitySubscriptionID + + encodedSpec, err := json.Marshal(azureSpec) + require.NoError(t, err, "error encoding provider spec") + cr.Spec.ProviderSpec = &runtime.RawExtension{Raw: encodedSpec} + + return cr + }(), + mockAppClient: func(ctrl *gomock.Controller) *azuremock.MockAppClient { + return azuremock.NewMockAppClient(ctrl) + }, + wantErr: assert.NoError, + validate: func(t *testing.T, c client.Client) { + expectedSecret := &corev1.Secret{} + err := c.Get(context.TODO(), types.NamespacedName{Name: testTargetSecretName, Namespace: testTargetSecretNamespace}, expectedSecret) + require.NoError(t, err) + }, + }, + { + name: "Incorrectly configured Azure Workload Identity fields", + existing: func() []runtime.Object { + objects := defaultExistingObjects() + objects = append(objects, testAuthentication("issuer")) + objects = append(objects, testOperatorConfig(operatorv1.CloudCredentialsModeManual)) + return objects + }(), + cr: func() *minterv1.CredentialsRequest { + cr := testCredentialsRequest(t) + + azureSpec := &minterv1.AzureProviderSpec{} + err := minterv1.Codec.DecodeProviderSpec(cr.Spec.ProviderSpec, azureSpec) + require.NoError(t, err, "error decoding provider spec") + + azureSpec.AzureClientID = "" + azureSpec.AzureTenantID = workloadIdentityTenantID + azureSpec.AzureRegion = workloadIdentityTenantID + azureSpec.AzureSubscriptionID = "" + + encodedSpec, err := json.Marshal(azureSpec) + require.NoError(t, err, "error encoding provider spec") + cr.Spec.ProviderSpec = &runtime.RawExtension{Raw: encodedSpec} + + return cr + }(), + mockAppClient: func(ctrl *gomock.Controller) *azuremock.MockAppClient { + return azuremock.NewMockAppClient(ctrl) + }, + wantErr: assert.Error, + validate: nil, + }, + } + + for _, test := range tests { + + fakeClient := fake.NewClientBuilder(). + WithStatusSubresource(test.cr). + WithRuntimeObjects(test.existing...).Build() + + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + appClient := test.mockAppClient(ctrl) + + act := azure.NewFakeActuator( + fakeClient, + fakeClient, + func(logger log.FieldLogger, clientID, clientSecret, tenantID, subscriptionID string) (*azure.AzureCredentialsMinter, error) { + return azure.NewFakeAzureCredentialsMinter(logger, clientID, clientSecret, tenantID, subscriptionID, appClient) + }, + ) + + ctx := context.TODO() + err := act.Create(ctx, test.cr) + + test.wantErr(t, err) + + if test.validate != nil { + test.validate(t, fakeClient) + } + + ctrl.Finish() + }) + } +} + func getCredRequestTargetSecret(t *testing.T, c client.Client, cr *minterv1.CredentialsRequest) *corev1.Secret { s := &corev1.Secret{} sKey := types.NamespacedName{Namespace: cr.Spec.SecretRef.Namespace, Name: cr.Spec.SecretRef.Name} @@ -607,3 +790,31 @@ func assertSecretEquality(t *testing.T, expectedSecret, assertingSecret *corev1. assert.Equal(t, expectedSecret.Data[azure.AzureSubscriptionID], assertingSecret.Data[azure.AzureSubscriptionID]) assert.Equal(t, expectedSecret.Data[azure.AzureTenantID], assertingSecret.Data[azure.AzureTenantID]) } + +func testOperatorConfig(mode operatorv1.CloudCredentialsMode) *operatorv1.CloudCredential { + conf := &operatorv1.CloudCredential{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.CloudCredOperatorConfig, + }, + Spec: operatorv1.CloudCredentialSpec{ + CredentialsMode: mode, + }, + } + return conf +} + +func testAuthentication(issuer string) *openshiftapiv1.Authentication { + return &openshiftapiv1.Authentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cluster", + }, + Spec: openshiftapiv1.AuthenticationSpec{ + ServiceAccountIssuer: "non-empty", + }, + Status: openshiftapiv1.AuthenticationStatus{ + IntegratedOAuthMetadata: openshiftapiv1.ConfigMapNameReference{ + Name: issuer, + }, + }, + } +} diff --git a/pkg/azure/const.go b/pkg/azure/const.go index ea01dfa774..d39a4b7965 100644 --- a/pkg/azure/const.go +++ b/pkg/azure/const.go @@ -1,11 +1,12 @@ package azure const ( - AzureClientID = "azure_client_id" - AzureClientSecret = "azure_client_secret" - AzureRegion = "azure_region" - AzureResourceGroup = "azure_resourcegroup" - AzureResourcePrefix = "azure_resource_prefix" - AzureSubscriptionID = "azure_subscription_id" - AzureTenantID = "azure_tenant_id" + AzureClientID = "azure_client_id" + AzureClientSecret = "azure_client_secret" + AzureRegion = "azure_region" + AzureResourceGroup = "azure_resourcegroup" + AzureResourcePrefix = "azure_resource_prefix" + AzureSubscriptionID = "azure_subscription_id" + AzureTenantID = "azure_tenant_id" + AzureFederatedTokenFile = "azure_federated_token_file" ) diff --git a/pkg/operator/credentialsrequest/credentialsrequest_controller.go b/pkg/operator/credentialsrequest/credentialsrequest_controller.go index 55406e29b4..bcf845a1e9 100644 --- a/pkg/operator/credentialsrequest/credentialsrequest_controller.go +++ b/pkg/operator/credentialsrequest/credentialsrequest_controller.go @@ -531,7 +531,7 @@ func (r *ReconcileCredentialsRequest) Reconcile(ctx context.Context, request rec logger.Infof("operator set to disabled / manual mode") return reconcile.Result{}, err } else { - logger.Infof("operator detects STS enabled cluster") + logger.Infof("operator detects timed access token enabled cluster (STS, Workload Identity, etc.)") } } diff --git a/test/e2e/azure/azident/actutator_e2e_test.go b/test/e2e/azure/azident/actutator_e2e_test.go new file mode 100644 index 0000000000..d96ed21049 --- /dev/null +++ b/test/e2e/azure/azident/actutator_e2e_test.go @@ -0,0 +1,134 @@ +//go:build e2e +// +build e2e + +package azident + +import ( + "context" + "os" + "testing" + "time" + + minterv1 "github.com/openshift/cloud-credential-operator/pkg/apis/cloudcredential/v1" + "github.com/openshift/cloud-credential-operator/pkg/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/klog/v2" + "sigs.k8s.io/e2e-framework/klient/conf" + "sigs.k8s.io/e2e-framework/klient/k8s/resources" + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/envfuncs" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +var testenv env.Environment +var secret = &corev1.Secret{} + +const ( + name = "test-azident-creds-req" + namespace = "default" + secretName = "test-azident-secret" +) + +func TestMain(m *testing.M) { + testenv = env.New() + path := conf.ResolveKubeConfigFile() + cfg := envconf.NewWithKubeConfig(path) + testenv = env.NewWithConfig(cfg) + namespace := envconf.RandomName("sample-ns", 16) + testenv.Setup( + envfuncs.CreateNamespace(namespace), + ) + testenv.Finish( + envfuncs.DeleteNamespace(namespace), + ) + os.Exit(testenv.Run(m)) +} + +func TestSecretCreationOnCredsRequestWithAzIdentInfo(t *testing.T) { + t.Logf("About to create a CredentialsRequest for testing Secret creation") + cr := newCredentialsRequest() + credReqFeature := features.New("minterv1.CredentialsRequest"). + Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + // create a CredentialsRequest + client := cfg.Client() + util.SetupScheme(scheme.Scheme) + if err := client.Resources().Create(ctx, cr); err != nil { + t.Fatal(err) + } + // watch for the Secret and trigger action based on the event received. + client.Resources().Watch( + &corev1.SecretList{}, + resources.WithFieldSelector( + labels.FormatLabels(map[string]string{"metadata.name": secretName}))). + WithAddFunc(onAdd(t, *cfg, ctx)). + Start(ctx) + return ctx + }). + Assess("secret creation", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + time.Sleep(2 * time.Minute) + if secretName != secret.GetName() { + t.Errorf("Secret name is incorrect. Expected: `%s`", secretName) + } + return context.WithValue(ctx, "test-secret", secret) + }). + Teardown(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { + if err := cfg.Client().Resources().Delete(ctx, cr); err != nil { + t.Fatal(err) + } + return ctx + }).Feature() + testenv.Test(t, credReqFeature) +} + +func onAdd(t *testing.T, cfg envconf.Config, ctx context.Context) func(obj interface{}) { + time.Sleep(2 * time.Minute) + if err := cfg.Client().Resources().Get(ctx, secretName, namespace, secret); err != nil { + t.Fatal(err) + } + if secretName != secret.GetName() { + t.Errorf("onAdd watch: Secret name is incorrect. Expected: `%s`", secretName) + } + klog.InfoS("Found Secret with name %s", secretName) + return nil +} + +func newCredentialsRequest() *minterv1.CredentialsRequest { + var in = minterv1.AzureProviderSpec{ + RoleBindings: []minterv1.RoleBinding{ + { + Role: "Owner", + }, + }, + AzureClientID: "0420bfd1-ab26-4b80-a9ac-deadbeeff1f9", + AzureTenantID: "6047c7e9-b2ad-488d-a54e-deadbeefa7ee", + AzureRegion: "centralus", + AzureSubscriptionID: "8c20ec23-8478-4f46-96f4-deadbeeff185", + } + + var ProviderSpec, _ = minterv1.Codec.EncodeProviderSpec(in.DeepCopyObject()) + var CredentialsRequestTemplate = &minterv1.CredentialsRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "openshift-cloud-credential-operator", + }, + Spec: minterv1.CredentialsRequestSpec{ + ProviderSpec: ProviderSpec, + SecretRef: corev1.ObjectReference{ + Name: secretName, + Namespace: namespace, + }, + ServiceAccountNames: []string{ + "serviceaccountname", + }, + CloudTokenPath: "", + }, + } + + credReq := CredentialsRequestTemplate + credReq.Spec.CloudTokenPath = "/var/cloud-token" + return credReq +}