From 1cbbd851b7efd87b42be4a97b05c74b7e435b51f Mon Sep 17 00:00:00 2001 From: Renan Campos Date: Thu, 5 Sep 2024 17:18:55 -0400 Subject: [PATCH 1/5] chore: bump ocm-sdk-go v0.1.439 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b13b626b..33e25fd5 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/nwidger/jsoncolor v0.3.2 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 - github.com/openshift-online/ocm-sdk-go v0.1.437 + github.com/openshift-online/ocm-sdk-go v0.1.439 github.com/openshift/rosa v1.2.24 github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 392cf96b..91084163 100644 --- a/go.sum +++ b/go.sum @@ -361,8 +361,8 @@ github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= -github.com/openshift-online/ocm-sdk-go v0.1.437 h1:2xFFOu3lvrrA0wfz4zUmX7UxcWQvqfbgBWelLLB5T2I= -github.com/openshift-online/ocm-sdk-go v0.1.437/go.mod h1:CiAu2jwl3ITKOxkeV0Qnhzv4gs35AmpIzVABQLtcI2Y= +github.com/openshift-online/ocm-sdk-go v0.1.439 h1:ELrJjmYgtzhdUY1cOJ0chtbhBEGz682EiTvojt5/xVM= +github.com/openshift-online/ocm-sdk-go v0.1.439/go.mod h1:CiAu2jwl3ITKOxkeV0Qnhzv4gs35AmpIzVABQLtcI2Y= github.com/openshift/rosa v1.2.24 h1:vv0yYnWHx6CCPEAau/0rS54P2ksaf+uWXb1TQPWxiYE= github.com/openshift/rosa v1.2.24/go.mod h1:MVXB27O3PF8WoOic23I03mmq6/9kVxpFx6FKyLMCyrQ= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= From c80019fd207256cf5071c72c96601115f57f2ce8 Mon Sep 17 00:00:00 2001 From: Renan Campos Date: Thu, 5 Sep 2024 17:24:06 -0400 Subject: [PATCH 2/5] support "vm" access method for wif configs --- cmd/ocm/gcp/create-wif-config.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/ocm/gcp/create-wif-config.go b/cmd/ocm/gcp/create-wif-config.go index 8c4b4a05..273febf2 100644 --- a/cmd/ocm/gcp/create-wif-config.go +++ b/cmd/ocm/gcp/create-wif-config.go @@ -367,6 +367,9 @@ func createServiceAccounts(ctx context.Context, gcpClient gcp.GcpClient, wifConf return errors.Wrapf(err, "Failed to attach workload identity pool to service account %s", serviceAccount.ServiceAccountId()) } + case cmv1.WifAccessMethodVm: + // Service accounts with the "vm" access method require no external access + continue default: log.Printf("Warning: %s is not a supported access type\n", serviceAccount.AccessMethod()) } From 46bb9dc78a40a444066c70bdb751c4d4422a263c Mon Sep 17 00:00:00 2001 From: Renan Campos Date: Thu, 5 Sep 2024 23:56:54 -0400 Subject: [PATCH 3/5] OCM-10387 | Grant support group access --- cmd/ocm/gcp/create-wif-config.go | 327 ++++-------------- cmd/ocm/gcp/delete-wif-config.go | 2 +- cmd/ocm/gcp/gcp-client-shim.go | 516 +++++++++++++++++++++++++++++ cmd/ocm/gcp/generate-wif-script.go | 2 +- cmd/ocm/gcp/iam.go | 147 -------- cmd/ocm/gcp/scripting.go | 46 ++- pkg/gcp/client.go | 261 ++++----------- pkg/gcp/error_handlers.go | 31 -- 8 files changed, 692 insertions(+), 640 deletions(-) create mode 100644 cmd/ocm/gcp/gcp-client-shim.go delete mode 100644 cmd/ocm/gcp/iam.go diff --git a/cmd/ocm/gcp/create-wif-config.go b/cmd/ocm/gcp/create-wif-config.go index 273febf2..2f59858c 100644 --- a/cmd/ocm/gcp/create-wif-config.go +++ b/cmd/ocm/gcp/create-wif-config.go @@ -6,22 +6,14 @@ import ( "log" "os" "path/filepath" - "reflect" "strconv" - "strings" - "github.com/googleapis/gax-go/v2/apierror" - - "cloud.google.com/go/iam/admin/apiv1/adminpb" "github.com/openshift-online/ocm-cli/pkg/gcp" "github.com/openshift-online/ocm-cli/pkg/ocm" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" "github.com/pkg/errors" "github.com/spf13/cobra" - "google.golang.org/api/googleapi" - iamv1 "google.golang.org/api/iam/v1" - "google.golang.org/grpc/codes" ) var ( @@ -45,8 +37,8 @@ func NewCreateWorkloadIdentityConfiguration() *cobra.Command { createWifConfigCmd := &cobra.Command{ Use: "wif-config", Short: "Create workload identity configuration", - RunE: createWorkloadIdentityConfigurationCmd, PreRunE: validationForCreateWorkloadIdentityConfigurationCmd, + RunE: createWorkloadIdentityConfigurationCmd, } createWifConfigCmd.PersistentFlags().StringVar(&CreateWifConfigOpts.Name, "name", "", @@ -65,8 +57,41 @@ func NewCreateWorkloadIdentityConfiguration() *cobra.Command { return createWifConfigCmd } +func validationForCreateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { + if CreateWifConfigOpts.Name == "" { + return fmt.Errorf("Name is required") + } + if CreateWifConfigOpts.Project == "" { + return fmt.Errorf("Project is required") + } + + if CreateWifConfigOpts.TargetDir == "" { + pwd, err := os.Getwd() + if err != nil { + return errors.Wrapf(err, "failed to get current directory") + } + + CreateWifConfigOpts.TargetDir = pwd + } + + fPath, err := filepath.Abs(CreateWifConfigOpts.TargetDir) + if err != nil { + return errors.Wrapf(err, "failed to resolve full path") + } + + sResult, err := os.Stat(fPath) + if os.IsNotExist(err) { + return fmt.Errorf("directory %s does not exist", fPath) + } + if !sResult.IsDir() { + return fmt.Errorf("file %s exists and is not a directory", fPath) + } + return nil +} + func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { ctx := context.Background() + log := log.Default() gcpClient, err := gcp.NewGcpClient(ctx) if err != nil { @@ -74,7 +99,12 @@ func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e } log.Println("Creating workload identity configuration...") - wifConfig, err := createWorkloadIdentityConfiguration(gcpClient, CreateWifConfigOpts.Name, CreateWifConfigOpts.Project) + wifConfig, err := createWorkloadIdentityConfiguration( + ctx, + gcpClient, + CreateWifConfigOpts.Name, + CreateWifConfigOpts.Project, + ) if err != nil { return errors.Wrapf(err, "failed to create WIF config") } @@ -82,7 +112,7 @@ func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e if CreateWifConfigOpts.DryRun { log.Printf("Writing script files to %s", CreateWifConfigOpts.TargetDir) - projectNum, err := gcpClient.ProjectNumberFromId(wifConfig.Gcp().ProjectId()) + projectNum, err := gcpClient.ProjectNumberFromId(ctx, wifConfig.Gcp().ProjectId()) if err != nil { return errors.Wrapf(err, "failed to get project number from id") } @@ -93,58 +123,40 @@ func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e return nil } - if err = createWorkloadIdentityPool(ctx, gcpClient, wifConfig); err != nil { + gcpClientWifConfigShim := NewGcpClientWifConfigShim(GcpClientWifConfigShimSpec{ + GcpClient: gcpClient, + WifConfig: wifConfig, + }) + + if err := gcpClientWifConfigShim.GrantSupportAccess(ctx, log); err != nil { + log.Printf("Failed to grant support access to project: %s", err) + return fmt.Errorf("To clean up, run the following command: ocm gcp delete wif-config %s", wifConfig.ID()) + } + + if err := gcpClientWifConfigShim.CreateWorkloadIdentityPool(ctx, log); err != nil { log.Printf("Failed to create workload identity pool: %s", err) return fmt.Errorf("To clean up, run the following command: ocm gcp delete wif-config %s", wifConfig.ID()) } - if err = createWorkloadIdentityProvider(ctx, gcpClient, wifConfig); err != nil { + if err = gcpClientWifConfigShim.CreateWorkloadIdentityProvider(ctx, log); err != nil { log.Printf("Failed to create workload identity provider: %s", err) return fmt.Errorf("To clean up, run the following command: ocm gcp delete wif-config %s", wifConfig.ID()) } - if err = createServiceAccounts(ctx, gcpClient, wifConfig); err != nil { + if err = gcpClientWifConfigShim.CreateServiceAccounts(ctx, log); err != nil { log.Printf("Failed to create IAM service accounts: %s", err) return fmt.Errorf("To clean up, run the following command: ocm gcp delete wif-config %s", wifConfig.ID()) } return nil } -func validationForCreateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { - if CreateWifConfigOpts.Name == "" { - return fmt.Errorf("Name is required") - } - if CreateWifConfigOpts.Project == "" { - return fmt.Errorf("Project is required") - } - - if CreateWifConfigOpts.TargetDir == "" { - pwd, err := os.Getwd() - if err != nil { - return errors.Wrapf(err, "failed to get current directory") - } - - CreateWifConfigOpts.TargetDir = pwd - } - - fPath, err := filepath.Abs(CreateWifConfigOpts.TargetDir) - if err != nil { - return errors.Wrapf(err, "failed to resolve full path") - } - - sResult, err := os.Stat(fPath) - if os.IsNotExist(err) { - return fmt.Errorf("directory %s does not exist", fPath) - } - if !sResult.IsDir() { - return fmt.Errorf("file %s exists and is not a directory", fPath) - } - return nil -} - -func createWorkloadIdentityConfiguration(client gcp.GcpClient, displayName, projectId string) (*cmv1.WifConfig, error) { - - projectNum, err := client.ProjectNumberFromId(projectId) +func createWorkloadIdentityConfiguration( + ctx context.Context, + client gcp.GcpClient, + displayName string, + projectId string, +) (*cmv1.WifConfig, error) { + projectNum, err := client.ProjectNumberFromId(ctx, projectId) if err != nil { return nil, errors.Wrapf(err, "failed to get GCP project number from project id") } @@ -182,220 +194,3 @@ func createWorkloadIdentityConfiguration(client gcp.GcpClient, displayName, proj return response.Body(), nil } - -func createWorkloadIdentityPool(ctx context.Context, client gcp.GcpClient, - wifConfig *cmv1.WifConfig) error { - poolId := wifConfig.Gcp().WorkloadIdentityPool().PoolId() - project := wifConfig.Gcp().ProjectId() - - parentResourceForPool := fmt.Sprintf("projects/%s/locations/global", project) - poolResource := fmt.Sprintf("%s/workloadIdentityPools/%s", parentResourceForPool, poolId) - resp, err := client.GetWorkloadIdentityPool(ctx, poolResource) - if resp != nil && resp.State == "DELETED" { - log.Printf("Workload identity pool %s was deleted", poolId) - _, err := client.UndeleteWorkloadIdentityPool(ctx, poolResource, &iamv1.UndeleteWorkloadIdentityPoolRequest{}) - if err != nil { - return errors.Wrapf(err, "failed to undelete workload identity pool %s", poolId) - } - } else if err != nil { - if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && - strings.Contains(gerr.Message, "Requested entity was not found") { - pool := &iamv1.WorkloadIdentityPool{ - Name: poolId, - DisplayName: poolId, - Description: poolDescription, - State: "ACTIVE", - Disabled: false, - } - - _, err := client.CreateWorkloadIdentityPool(ctx, parentResourceForPool, poolId, pool) - if err != nil { - return errors.Wrapf(err, "failed to create workload identity pool %s", poolId) - } - log.Printf("Workload identity pool created with name %s", poolId) - } else { - return errors.Wrapf(err, "failed to check if there is existing workload identity pool %s", poolId) - } - } else { - log.Printf("Workload identity pool %s already exists", poolId) - } - - return nil -} - -func createWorkloadIdentityProvider(ctx context.Context, client gcp.GcpClient, - wifConfig *cmv1.WifConfig) error { - projectId := wifConfig.Gcp().ProjectId() - poolId := wifConfig.Gcp().WorkloadIdentityPool().PoolId() - jwks := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().Jwks() - audiences := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().AllowedAudiences() - issuerUrl := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IssuerUrl() - providerId := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IdentityProviderId() - - parent := fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", projectId, poolId) - providerResource := fmt.Sprintf("%s/providers/%s", parent, providerId) - - _, err := client.GetWorkloadIdentityProvider(ctx, providerResource) - if err != nil { - if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && - strings.Contains(gerr.Message, "Requested entity was not found") { - provider := &iamv1.WorkloadIdentityPoolProvider{ - Name: providerId, - DisplayName: providerId, - Description: poolDescription, - State: "ACTIVE", - Disabled: false, - Oidc: &iamv1.Oidc{ - AllowedAudiences: audiences, - IssuerUri: issuerUrl, - JwksJson: jwks, - }, - AttributeMapping: map[string]string{ - "google.subject": "assertion.sub", - }, - } - - _, err := client.CreateWorkloadIdentityProvider(ctx, parent, providerId, provider) - if err != nil { - return errors.Wrapf(err, "failed to create workload identity provider %s", providerId) - } - log.Printf("workload identity provider created with name %s", providerId) - } else { - return errors.Wrapf(err, "failed to check if there is existing workload identity provider %s in pool %s", - providerId, poolId) - } - } else { - return errors.Errorf("workload identity provider %s already exists in pool %s", providerId, poolId) - } - - return nil -} - -func createServiceAccounts(ctx context.Context, gcpClient gcp.GcpClient, wifConfig *cmv1.WifConfig) error { - projectId := wifConfig.Gcp().ProjectId() - fmtRoleResourceId := func(role *cmv1.WifRole) string { - if role.Predefined() { - return fmt.Sprintf("roles/%s", role.RoleId()) - } else { - return fmt.Sprintf("projects/%s/roles/%s", projectId, role.RoleId()) - } - } - - // Create service accounts - for _, serviceAccount := range wifConfig.Gcp().ServiceAccounts() { - serviceAccountID := serviceAccount.ServiceAccountId() - serviceAccountName := wifConfig.DisplayName() + "-" + serviceAccountID - serviceAccountDesc := poolDescription + " for WIF config " + wifConfig.DisplayName() - - _, err := createServiceAccount(gcpClient, serviceAccountID, serviceAccountName, serviceAccountDesc, projectId, true) - if err != nil { - return errors.Wrap(err, "Failed to create IAM service account") - } - log.Printf("IAM service account %s created", serviceAccountID) - } - - // Create roles that aren't predefined - for _, serviceAccount := range wifConfig.Gcp().ServiceAccounts() { - for _, role := range serviceAccount.Roles() { - if role.Predefined() { - continue - } - roleID := role.RoleId() - roleTitle := role.RoleId() - permissions := role.Permissions() - existingRole, err := GetRole(gcpClient, fmtRoleResourceId(role)) - if err != nil { - if gerr, ok := err.(*apierror.APIError); ok && gerr.GRPCStatus().Code() == codes.NotFound { - _, err = CreateRole(gcpClient, permissions, roleTitle, - roleID, roleDescription, projectId) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("Failed to create %s", roleID)) - } - log.Printf("Role %q created", roleID) - continue - } else { - return errors.Wrap(err, "Failed to check if role exists") - } - } - - // Undelete role if it was deleted - if existingRole.Deleted { - _, err = UndeleteRole(gcpClient, fmtRoleResourceId(role)) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("Failed to undelete custom role %q", roleID)) - } - existingRole.Deleted = false - log.Printf("Role %q undeleted", roleID) - } - - // Update role if permissions have changed - if !reflect.DeepEqual(existingRole.IncludedPermissions, permissions) { - existingRole.IncludedPermissions = permissions - _, err := UpdateRole(gcpClient, existingRole, fmtRoleResourceId(role)) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("Failed to update %s", roleID)) - } - log.Printf("Role %q updated", roleID) - } - } - } - - // Bind roles and grant access - for _, serviceAccount := range wifConfig.Gcp().ServiceAccounts() { - serviceAccountID := serviceAccount.ServiceAccountId() - - roles := make([]string, 0, len(serviceAccount.Roles())) - for _, role := range serviceAccount.Roles() { - roles = append(roles, fmtRoleResourceId(role)) - } - member := fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccountID, projectId) - err := EnsurePolicyBindingsForProject(gcpClient, roles, member, projectId) - if err != nil { - return errors.Errorf("Failed to bind roles to service account %s: %s", serviceAccountID, err) - } - - switch serviceAccount.AccessMethod() { - case cmv1.WifAccessMethodImpersonate: - if err := gcpClient.AttachImpersonator(serviceAccount.ServiceAccountId(), projectId, - wifConfig.Gcp().ImpersonatorEmail()); err != nil { - return errors.Wrapf(err, "Failed to attach impersonator to service account %s", - serviceAccount.ServiceAccountId()) - } - case cmv1.WifAccessMethodWif: - if err := gcpClient.AttachWorkloadIdentityPool(serviceAccount, - wifConfig.Gcp().WorkloadIdentityPool().PoolId(), projectId); err != nil { - return errors.Wrapf(err, "Failed to attach workload identity pool to service account %s", - serviceAccount.ServiceAccountId()) - } - case cmv1.WifAccessMethodVm: - // Service accounts with the "vm" access method require no external access - continue - default: - log.Printf("Warning: %s is not a supported access type\n", serviceAccount.AccessMethod()) - } - } - - return nil -} - -func createServiceAccount(gcpClient gcp.GcpClient, svcAcctID, svcAcctName, svcAcctDescription, - projectName string, allowExisting bool) (*adminpb.ServiceAccount, error) { - request := &adminpb.CreateServiceAccountRequest{ - Name: fmt.Sprintf("projects/%s", projectName), - AccountId: svcAcctID, - ServiceAccount: &adminpb.ServiceAccount{ - DisplayName: svcAcctName, - Description: svcAcctDescription, - }, - } - svcAcct, err := gcpClient.CreateServiceAccount(context.TODO(), request) - if err != nil { - pApiError, ok := err.(*apierror.APIError) - if ok { - if pApiError.GRPCStatus().Code() == codes.AlreadyExists && allowExisting { - return svcAcct, nil - } - } - } - return svcAcct, err -} diff --git a/cmd/ocm/gcp/delete-wif-config.go b/cmd/ocm/gcp/delete-wif-config.go index 6dd6b020..576c3f14 100644 --- a/cmd/ocm/gcp/delete-wif-config.go +++ b/cmd/ocm/gcp/delete-wif-config.go @@ -113,7 +113,7 @@ func deleteServiceAccounts(ctx context.Context, gcpClient gcp.GcpClient, for _, serviceAccount := range wifConfig.Gcp().ServiceAccounts() { serviceAccountID := serviceAccount.ServiceAccountId() log.Println("Deleting service account", serviceAccountID) - err := gcpClient.DeleteServiceAccount(serviceAccountID, projectId, allowMissing) + err := gcpClient.DeleteServiceAccount(ctx, serviceAccountID, projectId, allowMissing) if err != nil { return errors.Wrapf(err, "Failed to delete service account %q", serviceAccountID) } diff --git a/cmd/ocm/gcp/gcp-client-shim.go b/cmd/ocm/gcp/gcp-client-shim.go new file mode 100644 index 00000000..c89c6543 --- /dev/null +++ b/cmd/ocm/gcp/gcp-client-shim.go @@ -0,0 +1,516 @@ +package gcp + +import ( + "context" + "fmt" + "log" + "reflect" + "strings" + + "cloud.google.com/go/iam/admin/apiv1/adminpb" + "github.com/googleapis/gax-go/v2/apierror" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + "github.com/pkg/errors" + cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1" + "google.golang.org/api/googleapi" + iamv1 "google.golang.org/api/iam/v1" + "google.golang.org/grpc/codes" + + "github.com/openshift-online/ocm-cli/pkg/gcp" +) + +type GcpClientWifConfigShim interface { + CreateServiceAccounts(ctx context.Context, log *log.Logger) error + CreateWorkloadIdentityPool(ctx context.Context, log *log.Logger) error + CreateWorkloadIdentityProvider(ctx context.Context, log *log.Logger) error + GrantSupportAccess(ctx context.Context, log *log.Logger) error +} + +type shim struct { + wifConfig *cmv1.WifConfig + gcpClient gcp.GcpClient +} + +type GcpClientWifConfigShimSpec struct { + WifConfig *cmv1.WifConfig + GcpClient gcp.GcpClient +} + +func NewGcpClientWifConfigShim(spec GcpClientWifConfigShimSpec) GcpClientWifConfigShim { + return &shim{ + wifConfig: spec.WifConfig, + gcpClient: spec.GcpClient, + } +} + +func (c *shim) CreateWorkloadIdentityPool( + ctx context.Context, + log *log.Logger, +) error { + poolId := c.wifConfig.Gcp().WorkloadIdentityPool().PoolId() + project := c.wifConfig.Gcp().ProjectId() + + parentResourceForPool := fmt.Sprintf("projects/%s/locations/global", project) + poolResource := fmt.Sprintf("%s/workloadIdentityPools/%s", parentResourceForPool, poolId) + resp, err := c.gcpClient.GetWorkloadIdentityPool(ctx, poolResource) + if resp != nil && resp.State == "DELETED" { + log.Printf("Workload identity pool %s was deleted", poolId) + _, err := c.gcpClient.UndeleteWorkloadIdentityPool( + ctx, poolResource, &iamv1.UndeleteWorkloadIdentityPoolRequest{}, + ) + if err != nil { + return errors.Wrapf(err, "failed to undelete workload identity pool %s", poolId) + } + } else if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && + strings.Contains(gerr.Message, "Requested entity was not found") { + pool := &iamv1.WorkloadIdentityPool{ + Name: poolId, + DisplayName: poolId, + Description: poolDescription, + State: "ACTIVE", + Disabled: false, + } + + _, err := c.gcpClient.CreateWorkloadIdentityPool(ctx, parentResourceForPool, poolId, pool) + if err != nil { + return errors.Wrapf(err, "failed to create workload identity pool %s", poolId) + } + log.Printf("Workload identity pool created with name %s", poolId) + } else { + return errors.Wrapf(err, "failed to check if there is existing workload identity pool %s", poolId) + } + } else { + log.Printf("Workload identity pool %s already exists", poolId) + } + + return nil +} + +func (c *shim) CreateWorkloadIdentityProvider( + ctx context.Context, + log *log.Logger, +) error { + projectId := c.wifConfig.Gcp().ProjectId() + poolId := c.wifConfig.Gcp().WorkloadIdentityPool().PoolId() + jwks := c.wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().Jwks() + audiences := c.wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().AllowedAudiences() + issuerUrl := c.wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IssuerUrl() + providerId := c.wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IdentityProviderId() + + parent := fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", projectId, poolId) + providerResource := fmt.Sprintf("%s/providers/%s", parent, providerId) + + _, err := c.gcpClient.GetWorkloadIdentityProvider(ctx, providerResource) + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 && + strings.Contains(gerr.Message, "Requested entity was not found") { + provider := &iamv1.WorkloadIdentityPoolProvider{ + Name: providerId, + DisplayName: providerId, + Description: poolDescription, + State: "ACTIVE", + Disabled: false, + Oidc: &iamv1.Oidc{ + AllowedAudiences: audiences, + IssuerUri: issuerUrl, + JwksJson: jwks, + }, + AttributeMapping: map[string]string{ + "google.subject": "assertion.sub", + }, + } + + _, err := c.gcpClient.CreateWorkloadIdentityProvider(ctx, parent, providerId, provider) + if err != nil { + return errors.Wrapf(err, "failed to create workload identity provider %s", providerId) + } + log.Printf("Workload identity provider created with name %s", providerId) + } else { + return errors.Wrapf(err, "failed to check if there is existing workload identity provider %s in pool %s", + providerId, poolId) + } + } else { + return errors.Errorf("workload identity provider %s already exists in pool %s", providerId, poolId) + } + + return nil +} + +func (c *shim) CreateServiceAccounts( + ctx context.Context, + log *log.Logger, +) error { + for _, serviceAccount := range c.wifConfig.Gcp().ServiceAccounts() { + if err := c.createServiceAccount(ctx, log, serviceAccount); err != nil { + return err + } + if err := c.createOrUpdateRoles(ctx, log, serviceAccount.Roles()); err != nil { + return err + } + if err := c.bindRolesToServiceAccount(ctx, serviceAccount); err != nil { + return err + } + if err := c.grantAccessToServiceAccount(ctx, serviceAccount); err != nil { + return err + } + } + return nil +} + +func (c *shim) GrantSupportAccess( + ctx context.Context, + log *log.Logger, +) error { + support := c.wifConfig.Gcp().Support() + if err := c.createOrUpdateRoles(ctx, log, support.Roles()); err != nil { + return err + } + if err := c.bindRolesToGroup(ctx, support.Principal(), support.Roles()); err != nil { + return err + } + log.Printf("support access granted to %s", support.Principal()) + return nil +} + +func (c *shim) createServiceAccount( + ctx context.Context, + log *log.Logger, + serviceAccount *cmv1.WifServiceAccount, +) error { + serviceAccountId := serviceAccount.ServiceAccountId() + serviceAccountName := c.wifConfig.DisplayName() + "-" + serviceAccountId + serviceAccountDesc := poolDescription + " for WIF config " + c.wifConfig.DisplayName() + + request := &adminpb.CreateServiceAccountRequest{ + Name: fmt.Sprintf("projects/%s", c.wifConfig.Gcp().ProjectId()), + AccountId: serviceAccountId, + ServiceAccount: &adminpb.ServiceAccount{ + DisplayName: serviceAccountName, + Description: serviceAccountDesc, + }, + } + _, err := c.gcpClient.CreateServiceAccount(ctx, request) + if err != nil { + pApiError, ok := err.(*apierror.APIError) + if ok { + if pApiError.GRPCStatus().Code() == codes.AlreadyExists { + return nil + } + } + } + if err != nil { + return errors.Wrap(err, "Failed to create IAM service account") + } + log.Printf("IAM service account %s created", serviceAccountId) + return nil +} + +func (c *shim) createOrUpdateRoles( + ctx context.Context, + log *log.Logger, + roles []*cmv1.WifRole, +) error { + for _, role := range roles { + if role.Predefined() { + continue + } + roleID := role.RoleId() + roleTitle := role.RoleId() + permissions := role.Permissions() + existingRole, err := c.getRole(ctx, c.fmtRoleResourceId(role)) + if err != nil { + if gerr, ok := err.(*apierror.APIError); ok && gerr.GRPCStatus().Code() == codes.NotFound { + _, err = c.createRole( + ctx, + permissions, + roleTitle, + roleID, + roleDescription, + c.wifConfig.Gcp().ProjectId(), + ) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to create %s", roleID)) + } + log.Printf("Role %q created", roleID) + continue + } else { + return errors.Wrap(err, "Failed to check if role exists") + } + } + + // Undelete role if it was deleted + if existingRole.Deleted { + _, err = c.undeleteRole(ctx, c.fmtRoleResourceId(role)) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to undelete custom role %q", roleID)) + } + existingRole.Deleted = false + log.Printf("Role %q undeleted", roleID) + } + + // Update role if permissions have changed + if !reflect.DeepEqual(existingRole.IncludedPermissions, permissions) { + existingRole.IncludedPermissions = permissions + _, err := c.updateRole(ctx, existingRole, c.fmtRoleResourceId(role)) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Failed to update %s", roleID)) + } + log.Printf("Role %q updated", roleID) + } + } + return nil +} + +func (c *shim) bindRolesToServiceAccount( + ctx context.Context, + serviceAccount *cmv1.WifServiceAccount, +) error { + serviceAccountId := serviceAccount.ServiceAccountId() + roles := serviceAccount.Roles() + + return c.bindRolesToPrincipal( + ctx, + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccountId, c.wifConfig.Gcp().ProjectId()), + roles, + ) +} + +func (c *shim) bindRolesToGroup( + ctx context.Context, + groupEmail string, + roles []*cmv1.WifRole, +) error { + return c.bindRolesToPrincipal( + ctx, + fmt.Sprintf("group:%s", groupEmail), + roles, + ) +} + +func (c *shim) bindRolesToPrincipal( + ctx context.Context, + principal string, + roles []*cmv1.WifRole, +) error { + formattedRoles := make([]string, 0, len(roles)) + for _, role := range roles { + formattedRoles = append(formattedRoles, c.fmtRoleResourceId(role)) + } + err := c.ensurePolicyBindingsForProject( + ctx, + formattedRoles, + principal, + c.wifConfig.Gcp().ProjectId(), + ) + if err != nil { + return errors.Errorf("Failed to bind roles to principal %s: %s", principal, err) + } + return nil +} + +func (c *shim) grantAccessToServiceAccount( + ctx context.Context, + serviceAccount *cmv1.WifServiceAccount, +) error { + switch serviceAccount.AccessMethod() { + case cmv1.WifAccessMethodImpersonate: + if err := c.gcpClient.AttachImpersonator( + ctx, + serviceAccount.ServiceAccountId(), + c.wifConfig.Gcp().ProjectId(), + c.wifConfig.Gcp().ImpersonatorEmail(), + ); err != nil { + return errors.Wrapf(err, "Failed to attach impersonator to service account %s", + serviceAccount.ServiceAccountId()) + } + case cmv1.WifAccessMethodWif: + if err := c.gcpClient.AttachWorkloadIdentityPool( + ctx, + serviceAccount, + c.wifConfig.Gcp().WorkloadIdentityPool().PoolId(), + c.wifConfig.Gcp().ProjectId(), + ); err != nil { + return errors.Wrapf(err, "Failed to attach workload identity pool to service account %s", + serviceAccount.ServiceAccountId()) + } + case cmv1.WifAccessMethodVm: + // Service accounts with the "vm" access method require no external access + return nil + default: + log.Printf("Warning: %s is not a supported access type\n", serviceAccount.AccessMethod()) + } + return nil +} + +func (c *shim) fmtRoleResourceId( + role *cmv1.WifRole, +) string { + if role.Predefined() { + return fmt.Sprintf("roles/%s", role.RoleId()) + } else { + return fmt.Sprintf("projects/%s/roles/%s", c.wifConfig.Gcp().ProjectId(), role.RoleId()) + } +} + +// GetRole fetches the role created to satisfy a credentials request. +// Custom roles should follow the format projects/{project}/roles/{role_id}. +func (c *shim) getRole( + ctx context.Context, + roleName string, +) (*adminpb.Role, error) { + role, err := c.gcpClient.GetRole(ctx, &adminpb.GetRoleRequest{ + Name: roleName, + }) + return role, err +} + +// CreateRole creates a new role given permissions +func (c *shim) createRole( + ctx context.Context, + permissions []string, + roleTitle string, + roleId string, + roleDescription string, + projectName string, +) (*adminpb.Role, error) { + role, err := c.gcpClient.CreateRole(ctx, &adminpb.CreateRoleRequest{ + Role: &adminpb.Role{ + Title: roleTitle, + Description: roleDescription, + IncludedPermissions: permissions, + Stage: adminpb.Role_GA, + }, + Parent: fmt.Sprintf("projects/%s", projectName), + RoleId: roleId, + }) + if err != nil { + return nil, err + } + return role, nil +} + +// UpdateRole updates an existing role given permissions. +// Custom roles should follow the format projects/{project}/roles/{role_id}. +func (c *shim) updateRole( + ctx context.Context, + role *adminpb.Role, + roleName string, +) (*adminpb.Role, error) { + updated, err := c.gcpClient.UpdateRole(ctx, &adminpb.UpdateRoleRequest{ + Name: roleName, + Role: role, + }) + if err != nil { + return nil, err + } + return updated, nil +} + +// UndeleteRole undeletes a previously deleted role that has not yet been pruned +func (c *shim) undeleteRole( + ctx context.Context, + roleName string, +) (*adminpb.Role, error) { + role, err := c.gcpClient.UndeleteRole(ctx, &adminpb.UndeleteRoleRequest{ + Name: roleName, + }) + return role, err +} + +// EnsurePolicyBindingsForProject ensures that given roles and member, appropriate binding is added to project. +// Roles should be in the format projects/{project}/roles/{role_id} for custom roles and roles/{role_id} +// for predefined roles. +func (c *shim) ensurePolicyBindingsForProject( + ctx context.Context, + roles []string, + member string, + projectName string, +) error { + needPolicyUpdate := false + + policy, err := c.gcpClient.GetProjectIamPolicy(ctx, projectName, &cloudresourcemanager.GetIamPolicyRequest{}) + + if err != nil { + return fmt.Errorf("error fetching policy for project: %v", err) + } + + // Validate that each role exists, and add the policy binding as needed + for _, definedRole := range roles { + // Earlier we've verified that the requested roles already exist. + + // Add policy binding + modified := c.addPolicyBindingForProject(policy, definedRole, member) + if modified { + needPolicyUpdate = true + } + + } + + if needPolicyUpdate { + return c.setProjectIamPolicy(ctx, policy) + } + + // If we made it this far there were no updates needed + return nil +} + +func (c *shim) setProjectIamPolicy( + ctx context.Context, + policy *cloudresourcemanager.Policy, +) error { + _, err := c.gcpClient.SetProjectIamPolicy( + ctx, + c.wifConfig.Gcp().ProjectId(), + &cloudresourcemanager.SetIamPolicyRequest{ + Policy: policy, + }) + if err != nil { + return fmt.Errorf("error setting project policy: %v", err) + } + return nil +} + +func (c *shim) addPolicyBindingForProject( + policy *cloudresourcemanager.Policy, + roleName string, + memberName string, +) bool { + for i, binding := range policy.Bindings { + if binding.Role == roleName { + return c.addMemberToBindingForProject(memberName, policy.Bindings[i]) + } + } + + // if we didn't find an existing binding entry, then make one + c.createMemberRoleBindingForProject(policy, roleName, memberName) + + return true +} + +// adds member to existing binding. returns bool indicating if an entry was made +func (c *shim) addMemberToBindingForProject( + memberName string, + binding *cloudresourcemanager.Binding, +) bool { + for _, member := range binding.Members { + if member == memberName { + // already present + return false + } + } + + binding.Members = append(binding.Members, memberName) + return true +} + +func (c *shim) createMemberRoleBindingForProject( + policy *cloudresourcemanager.Policy, + roleName string, + memberName string, +) { + policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{ + Members: []string{memberName}, + Role: roleName, + }) +} diff --git a/cmd/ocm/gcp/generate-wif-script.go b/cmd/ocm/gcp/generate-wif-script.go index 1b4bf166..5a6ca45e 100644 --- a/cmd/ocm/gcp/generate-wif-script.go +++ b/cmd/ocm/gcp/generate-wif-script.go @@ -68,7 +68,7 @@ func generateCreateScriptCmd(cmd *cobra.Command, argv []string) error { } wifConfig := response.Body() - projectNum, err := gcpClient.ProjectNumberFromId(wifConfig.Gcp().ProjectId()) + projectNum, err := gcpClient.ProjectNumberFromId(ctx, wifConfig.Gcp().ProjectId()) if err != nil { return errors.Wrapf(err, "failed to get project number from id") } diff --git a/cmd/ocm/gcp/iam.go b/cmd/ocm/gcp/iam.go deleted file mode 100644 index d308a71b..00000000 --- a/cmd/ocm/gcp/iam.go +++ /dev/null @@ -1,147 +0,0 @@ -package gcp - -import ( - "context" - "fmt" - - "cloud.google.com/go/iam/admin/apiv1/adminpb" - "github.com/openshift-online/ocm-cli/pkg/gcp" - - cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1" -) - -// EnsurePolicyBindingsForProject ensures that given roles and member, appropriate binding is added to project. -// Roles should be in the format projects/{project}/roles/{role_id} for custom roles and roles/{role_id} -// for predefined roles. -func EnsurePolicyBindingsForProject(gcpClient gcp.GcpClient, roles []string, member string, projectName string) error { - needPolicyUpdate := false - - policy, err := gcpClient.GetProjectIamPolicy(projectName, &cloudresourcemanager.GetIamPolicyRequest{}) - - if err != nil { - return fmt.Errorf("error fetching policy for project: %v", err) - } - - // Validate that each role exists, and add the policy binding as needed - for _, definedRole := range roles { - // Earlier we've verified that the requested roles already exist. - - // Add policy binding - modified := addPolicyBindingForProject(policy, definedRole, member) - if modified { - needPolicyUpdate = true - } - - } - - if needPolicyUpdate { - return setProjectIamPolicy(gcpClient, policy, projectName) - } - - // If we made it this far there were no updates needed - return nil -} - -func addPolicyBindingForProject(policy *cloudresourcemanager.Policy, roleName, memberName string) bool { - for i, binding := range policy.Bindings { - if binding.Role == roleName { - return addMemberToBindingForProject(memberName, policy.Bindings[i]) - } - } - - // if we didn't find an existing binding entry, then make one - createMemberRoleBindingForProject(policy, roleName, memberName) - - return true -} - -func createMemberRoleBindingForProject(policy *cloudresourcemanager.Policy, roleName, memberName string) { - policy.Bindings = append(policy.Bindings, &cloudresourcemanager.Binding{ - Members: []string{memberName}, - Role: roleName, - }) -} - -// adds member to existing binding. returns bool indicating if an entry was made -func addMemberToBindingForProject(memberName string, binding *cloudresourcemanager.Binding) bool { - for _, member := range binding.Members { - if member == memberName { - // already present - return false - } - } - - binding.Members = append(binding.Members, memberName) - return true -} - -func setProjectIamPolicy(gcpClient gcp.GcpClient, policy *cloudresourcemanager.Policy, projectName string) error { - policyRequest := &cloudresourcemanager.SetIamPolicyRequest{ - Policy: policy, - } - - _, err := gcpClient.SetProjectIamPolicy(projectName, policyRequest) - if err != nil { - return fmt.Errorf("error setting project policy: %v", err) - } - return nil -} - -/* Custom Role Creation */ - -// GetRole fetches the role created to satisfy a credentials request. -// Custom roles should follow the format projects/{project}/roles/{role_id}. -func GetRole(gcpClient gcp.GcpClient, roleName string) (*adminpb.Role, error) { - role, err := gcpClient.GetRole(context.TODO(), &adminpb.GetRoleRequest{ - Name: roleName, - }) - return role, err -} - -// CreateRole creates a new role given permissions -func CreateRole(gcpClient gcp.GcpClient, permissions []string, roleTitle, roleId, roleDescription, - projectName string) (*adminpb.Role, error) { - role, err := gcpClient.CreateRole(context.TODO(), &adminpb.CreateRoleRequest{ - Role: &adminpb.Role{ - Title: roleTitle, - Description: roleDescription, - IncludedPermissions: permissions, - Stage: adminpb.Role_GA, - }, - Parent: fmt.Sprintf("projects/%s", projectName), - RoleId: roleId, - }) - if err != nil { - return nil, err - } - return role, nil -} - -// UpdateRole updates an existing role given permissions. -// Custom roles should follow the format projects/{project}/roles/{role_id}. -func UpdateRole(gcpClient gcp.GcpClient, role *adminpb.Role, roleName string) (*adminpb.Role, error) { - updated, err := gcpClient.UpdateRole(context.TODO(), &adminpb.UpdateRoleRequest{ - Name: roleName, - Role: role, - }) - if err != nil { - return nil, err - } - return updated, nil -} - -// DeleteRole deletes the role created to satisfy a credentials request -func DeleteRole(gcpClient gcp.GcpClient, roleName string) (*adminpb.Role, error) { - role, err := gcpClient.DeleteRole(context.TODO(), &adminpb.DeleteRoleRequest{ - Name: roleName, - }) - return role, err -} - -// UndeleteRole undeletes a previously deleted role that has not yet been pruned -func UndeleteRole(gcpClient gcp.GcpClient, roleName string) (*adminpb.Role, error) { - role, err := gcpClient.UndeleteRole(context.TODO(), &adminpb.UndeleteRoleRequest{ - Name: roleName, - }) - return role, err -} diff --git a/cmd/ocm/gcp/scripting.go b/cmd/ocm/gcp/scripting.go index cf0f0ab4..8f54f111 100644 --- a/cmd/ocm/gcp/scripting.go +++ b/cmd/ocm/gcp/scripting.go @@ -79,6 +79,8 @@ func generateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { // Append the script to create the service accounts scriptContent += createServiceAccountScriptContent(wifConfig, projectNum) + scriptContent += grantSupportAccessScriptContent(wifConfig) + return scriptContent } @@ -87,7 +89,7 @@ func createIdentityPoolScriptContent(wifConfig *cmv1.WifConfig) string { project := wifConfig.Gcp().ProjectId() return fmt.Sprintf(` -# Create a workload identity pool +# Create workload identity pool: gcloud iam workload-identity-pools create %s \ --project=%s \ --location=global \ @@ -103,7 +105,7 @@ func createIdentityProviderScriptContent(wifConfig *cmv1.WifConfig) string { providerId := wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().IdentityProviderId() return fmt.Sprintf(` -# Create a workload identity provider +# Create workload identity provider: gcloud iam workload-identity-pools providers create-oidc %s \ --display-name="%s" \ --description="%s" \ @@ -132,7 +134,7 @@ func createServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int sb.WriteString(fmt.Sprintf("gcloud iam service-accounts create %s --display-name=%s --description=\"%s\" --project=%s\n", serviceAccountID, serviceAccountName, serviceAccountDesc, project)) } - sb.WriteString("\n# Create roles:\n") + sb.WriteString("\n# Create custom roles for service accounts:\n") for _, sa := range wifConfig.Gcp().ServiceAccounts() { for _, role := range sa.Roles() { if !role.Predefined() { @@ -147,7 +149,7 @@ func createServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int } } } - sb.WriteString("\n# Bind service account roles:\n") + sb.WriteString("\n# Bind roles to service accounts:\n") for _, sa := range wifConfig.Gcp().ServiceAccounts() { for _, role := range sa.Roles() { project := wifConfig.Gcp().ProjectId() @@ -162,7 +164,7 @@ func createServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int project, member, roleResource)) } } - sb.WriteString("\n# Grant access:\n") + sb.WriteString("\n# Grant access to service accounts:\n") for _, sa := range wifConfig.Gcp().ServiceAccounts() { if sa.AccessMethod() == "wif" { project := wifConfig.Gcp().ProjectId() @@ -186,6 +188,40 @@ func createServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int return sb.String() } +func grantSupportAccessScriptContent(wifConfig *cmv1.WifConfig) string { + var sb strings.Builder + + roles := wifConfig.Gcp().Support().Roles() + project := wifConfig.Gcp().ProjectId() + principal := wifConfig.Gcp().Support().Principal() + + sb.WriteString("\n# Create custom roles for support:\n") + for _, role := range roles { + if !role.Predefined() { + roleId := strings.ReplaceAll(role.RoleId(), "-", "_") + permissions := strings.Join(role.Permissions(), ",") + roleName := roleId + roleDesc := roleDescription + " for WIF config " + wifConfig.DisplayName() + //nolint:lll + sb.WriteString(fmt.Sprintf("gcloud iam roles create %s --project=%s --title=%s --description=\"%s\" --stage=GA --permissions=%s\n", + roleId, project, roleName, roleDesc, permissions)) + } + } + + sb.WriteString("\n# Bind roles to support principal:\n") + for _, role := range roles { + var roleResource string + if role.Predefined() { + roleResource = fmt.Sprintf("roles/%s", role.RoleId()) + } else { + roleResource = fmt.Sprintf("projects/%s/roles/%s", project, role.RoleId()) + } + sb.WriteString(fmt.Sprintf("gcloud projects add-iam-policy-binding %s --member=%s --role=%s\n", + project, principal, roleResource)) + } + return sb.String() +} + func fmtMembers(sa *cmv1.WifServiceAccount, projectNum int64, poolId string) []string { members := []string{} for _, saName := range sa.CredentialRequest().ServiceAccountNames() { diff --git a/pkg/gcp/client.go b/pkg/gcp/client.go index 10876f8c..371734e5 100644 --- a/pkg/gcp/client.go +++ b/pkg/gcp/client.go @@ -2,7 +2,6 @@ package gcp import ( "context" - "encoding/base64" "fmt" "cloud.google.com/go/iam" @@ -14,42 +13,32 @@ import ( cloudresourcemanager "google.golang.org/api/cloudresourcemanager/v1" iamv1 "google.golang.org/api/iam/v1" - "google.golang.org/api/iterator" secretmanager "google.golang.org/api/secretmanager/v1" ) type GcpClient interface { - ListServiceAccounts(project string, filter func(s string) bool) ([]string, error) //nolint:lll - - CreateServiceAccount(ctx context.Context, request *adminpb.CreateServiceAccountRequest) (*adminpb.ServiceAccount, error) //nolint:lll - - CreateWorkloadIdentityPool(ctx context.Context, parent, poolID string, pool *iamv1.WorkloadIdentityPool) (*iamv1.Operation, error) //nolint:lll - GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) //nolint:lll - DeleteWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.Operation, error) //nolint:lll - UndeleteWorkloadIdentityPool(ctx context.Context, resource string, request *iamv1.UndeleteWorkloadIdentityPoolRequest) (*iamv1.Operation, error) //nolint:lll - + /* + ListServiceAccounts(ctx context.Context, project string, filter func(s string) bool) ([]string, error) //nolint:lll + DeleteRole(context.Context, *adminpb.DeleteRoleRequest) (*adminpb.Role, error) + ListRoles(context.Context, *adminpb.ListRolesRequest) (*adminpb.ListRolesResponse, error) + */ + AttachImpersonator(ctx context.Context, saId, projectId, impersonatorResourceId string) error + AttachWorkloadIdentityPool(ctx context.Context, sa *cmv1.WifServiceAccount, poolId, projectId string) error + CreateRole(context.Context, *adminpb.CreateRoleRequest) (*adminpb.Role, error) + CreateServiceAccount(ctx context.Context, request *adminpb.CreateServiceAccountRequest) (*adminpb.ServiceAccount, error) //nolint:lll + CreateWorkloadIdentityPool(ctx context.Context, parent, poolID string, pool *iamv1.WorkloadIdentityPool) (*iamv1.Operation, error) //nolint:lll CreateWorkloadIdentityProvider(ctx context.Context, parent, providerID string, provider *iamv1.WorkloadIdentityPoolProvider) (*iamv1.Operation, error) //nolint:lll - GetWorkloadIdentityProvider(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPoolProvider, error) //nolint:lll - - DeleteServiceAccount(saName string, project string, allowMissing bool) error - - GetProjectIamPolicy(projectName string, request *cloudresourcemanager.GetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll - SetProjectIamPolicy(svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll - - AttachImpersonator(saId, projectId, impersonatorResourceId string) error - AttachWorkloadIdentityPool(sa *cmv1.WifServiceAccount, poolId, projectId string) error - - SaveSecret(secretId, projectId string, secretData []byte) error - RetreiveSecret(secretId string, projectId string) ([]byte, error) - - ProjectNumberFromId(projectId string) (int64, error) - + DeleteServiceAccount(ctx context.Context, saName string, project string, allowMissing bool) error + DeleteWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.Operation, error) //nolint:lll + GetProjectIamPolicy(ctx context.Context, projectName string, request *cloudresourcemanager.GetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll GetRole(context.Context, *adminpb.GetRoleRequest) (*adminpb.Role, error) - CreateRole(context.Context, *adminpb.CreateRoleRequest) (*adminpb.Role, error) - UpdateRole(context.Context, *adminpb.UpdateRoleRequest) (*adminpb.Role, error) - DeleteRole(context.Context, *adminpb.DeleteRoleRequest) (*adminpb.Role, error) + GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) //nolint:lll + GetWorkloadIdentityProvider(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPoolProvider, error) //nolint:lll + ProjectNumberFromId(ctx context.Context, projectId string) (int64, error) + SetProjectIamPolicy(ctx context.Context, svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll UndeleteRole(context.Context, *adminpb.UndeleteRoleRequest) (*adminpb.Role, error) - ListRoles(context.Context, *adminpb.ListRolesRequest) (*adminpb.ListRolesResponse, error) + UndeleteWorkloadIdentityPool(ctx context.Context, resource string, request *iamv1.UndeleteWorkloadIdentityPoolRequest) (*iamv1.Operation, error) //nolint:lll + UpdateRole(context.Context, *adminpb.UpdateRoleRequest) (*adminpb.Role, error) } type gcpClient struct { @@ -95,50 +84,10 @@ func NewGcpClient(ctx context.Context) (GcpClient, error) { }, nil } -func (c *gcpClient) CreateServiceAccount(ctx context.Context, - request *adminpb.CreateServiceAccountRequest) (*adminpb.ServiceAccount, error) { - svcAcct, err := c.iamClient.CreateServiceAccount(ctx, request) - return svcAcct, err -} - -func (c *gcpClient) DeleteServiceAccount(saName string, project string, allowMissing bool) error { - name := fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", project, saName, project) - err := c.iamClient.DeleteServiceAccount(context.Background(), &adminpb.DeleteServiceAccountRequest{ - Name: name, - }) - if err != nil { - return c.handleDeleteServiceAccountError(err, allowMissing) - } - return nil -} - -func (c *gcpClient) ListServiceAccounts(project string, filter func(string) bool) ([]string, error) { - out := []string{} - // Listing objects follow the iterator pattern specified here: - // https://github.com/googleapis/google-cloud-go/wiki/Iterator-Guidelines - saIterator := c.iamClient.ListServiceAccounts(context.Background(), &adminpb.ListServiceAccountsRequest{ - Name: fmt.Sprintf("projects/%s", project), - // The pagesize can be adjusted for optimized network load. - // PageSize: 5, - }) - for sa, err := saIterator.Next(); err != iterator.Done; sa, err = saIterator.Next() { - if err != nil { - return nil, c.handleListServiceAccountError(err) - } - // Example: - // To list all service accounts: - // filter = func(s string) bool { return true } - if filter(sa.Name) { - out = append(out, sa.Name) - } - } - return out, nil -} - -func (c *gcpClient) AttachImpersonator(saId, projectId string, impersonatorEmail string) error { +func (c *gcpClient) AttachImpersonator(ctx context.Context, saId, projectId string, impersonatorEmail string) error { saResourceId := fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", projectId, saId, projectId) - policy, err := c.iamClient.GetIamPolicy(context.Background(), &iampb.GetIamPolicyRequest{ + policy, err := c.iamClient.GetIamPolicy(ctx, &iampb.GetIamPolicyRequest{ Resource: saResourceId, }) if err != nil { @@ -147,7 +96,7 @@ func (c *gcpClient) AttachImpersonator(saId, projectId string, impersonatorEmail policy.Add( fmt.Sprintf("serviceAccount:%s", impersonatorEmail), iam.RoleName("roles/iam.serviceAccountTokenCreator")) - _, err = c.iamClient.SetIamPolicy(context.Background(), &iamadmin.SetIamPolicyRequest{ + _, err = c.iamClient.SetIamPolicy(ctx, &iamadmin.SetIamPolicyRequest{ Resource: saResourceId, Policy: policy, }) @@ -157,15 +106,20 @@ func (c *gcpClient) AttachImpersonator(saId, projectId string, impersonatorEmail return nil } -func (c *gcpClient) AttachWorkloadIdentityPool(sa *cmv1.WifServiceAccount, poolId, projectId string) error { +func (c *gcpClient) AttachWorkloadIdentityPool( + ctx context.Context, + sa *cmv1.WifServiceAccount, + poolId string, + projectId string, +) error { saResourceId := c.fmtSaResourceId(sa.ServiceAccountId(), projectId) - projectNum, err := c.ProjectNumberFromId(projectId) + projectNum, err := c.ProjectNumberFromId(ctx, projectId) if err != nil { return c.handleAttachWorkloadIdentityPoolError(err) } - policy, err := c.iamClient.GetIamPolicy(context.Background(), &iampb.GetIamPolicyRequest{ + policy, err := c.iamClient.GetIamPolicy(ctx, &iampb.GetIamPolicyRequest{ Resource: saResourceId, }) if err != nil { @@ -180,7 +134,7 @@ func (c *gcpClient) AttachWorkloadIdentityPool(sa *cmv1.WifServiceAccount, poolI ), iam.RoleName("roles/iam.workloadIdentityUser")) } - _, err = c.iamClient.SetIamPolicy(context.Background(), &iamadmin.SetIamPolicyRequest{ + _, err = c.iamClient.SetIamPolicy(ctx, &iamadmin.SetIamPolicyRequest{ Resource: saResourceId, Policy: policy, }) @@ -190,89 +144,16 @@ func (c *gcpClient) AttachWorkloadIdentityPool(sa *cmv1.WifServiceAccount, poolI return nil } -// - secretResource: The resource name of the secret is in the format -// `projects/*/secrets/*` -// - secretData: Can be anything. -func (c *gcpClient) SaveSecret(secretName, secretProject string, secretData []byte) error { - _, err := c.secretManager.Projects.Secrets.Create("projects/"+secretProject, &secretmanager.Secret{ - // This is an undocumented required field. - // https://github.com/hashicorp/terraform-provider-google/issues/11395 - Replication: &secretmanager.Replication{Automatic: &secretmanager.Automatic{}}, - }).SecretId(secretName).Do() - if err != nil { - err = c.handleSaveSecretError(err) - if err != nil { - return err - } - } - _, err = c.secretManager.Projects.Locations.Secrets.AddVersion( - fmt.Sprintf("projects/%s/secrets/%s", secretProject, secretName), - &secretmanager.AddSecretVersionRequest{ - Payload: &secretmanager.SecretPayload{ - Data: base64.StdEncoding.EncodeToString(secretData), - }, - }).Do() - if err != nil { - return c.handleSaveSecretError(err) - } - return nil -} - -// - name: The resource name of the secret is in the format -// `projects/*/secrets/*/versions/*` or -// `projects/*/locations/*/secrets/*/versions/*`. -// `projects/*/secrets/*/versions/latest` or -// `projects/*/locations/*/secrets/*/versions/latest` is an alias to -// the most recently created SecretVersion. -func (c *gcpClient) RetreiveSecret(secretId string, projectId string) ([]byte, error) { - secretResource := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", projectId, secretId) - resp, err := c.secretManager.Projects.Secrets.Versions.Access(secretResource).Do() - if err != nil { - c.handleRetrieveSecretError(err) - } - return base64.StdEncoding.DecodeString(resp.Payload.Data) -} - -type WorkloadIdentityPoolSpec struct { - Audience []string - IssuerUrl string - PoolName string - ProjectId string - Jwks string - PoolIdentityProviderId string +func (c *gcpClient) CreateRole(ctx context.Context, request *adminpb.CreateRoleRequest) (*adminpb.Role, error) { + return c.iamClient.CreateRole(ctx, request) } -func (c *gcpClient) CreateWorkloadIdentityPool2(spec WorkloadIdentityPoolSpec) error { - // Note: The parent parameter should be in the following format: - // projects/*/locations/* - // https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools/create - if _, err := c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Create( - fmt.Sprintf("projects/%s/locations/global", spec.ProjectId), &iamv1.WorkloadIdentityPool{ - DisplayName: spec.PoolName, - Description: "Workload Identity pool created by prototype", - }).WorkloadIdentityPoolId(spec.PoolName).Do(); err != nil { - if err != nil { - return err - } - } - if _, err := c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Providers.Create( - fmt.Sprintf("projects/%s/locations/global/workloadIdentityPools/%s", spec.ProjectId, spec.PoolName), - &iamv1.WorkloadIdentityPoolProvider{ - AttributeMapping: map[string]string{ - "google.subject": "assertion.sub", - }, - Description: "Identity Provider created by prototype", - Oidc: &iamv1.Oidc{ - AllowedAudiences: []string{ - "openshift", - }, - IssuerUri: spec.IssuerUrl, - JwksJson: spec.Jwks, - }, - }).WorkloadIdentityPoolProviderId(spec.PoolIdentityProviderId).Do(); err != nil { - return err - } - return nil +func (c *gcpClient) CreateServiceAccount( + ctx context.Context, + request *adminpb.CreateServiceAccountRequest, +) (*adminpb.ServiceAccount, error) { + svcAcct, err := c.iamClient.CreateServiceAccount(ctx, request) + return svcAcct, err } //nolint:lll @@ -281,8 +162,19 @@ func (c *gcpClient) CreateWorkloadIdentityPool(ctx context.Context, parent, pool } //nolint:lll -func (c *gcpClient) GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) { - return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Get(resource).Context(ctx).Do() +func (c *gcpClient) CreateWorkloadIdentityProvider(ctx context.Context, parent, providerID string, provider *iamv1.WorkloadIdentityPoolProvider) (*iamv1.Operation, error) { + return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Providers.Create(parent, provider).WorkloadIdentityPoolProviderId(providerID).Context(ctx).Do() +} + +func (c *gcpClient) DeleteServiceAccount(ctx context.Context, saName string, project string, allowMissing bool) error { + name := fmt.Sprintf("projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com", project, saName, project) + err := c.iamClient.DeleteServiceAccount(ctx, &adminpb.DeleteServiceAccountRequest{ + Name: name, + }) + if err != nil { + return c.handleDeleteServiceAccountError(err, allowMissing) + } + return nil } //nolint:lll @@ -291,13 +183,21 @@ func (c *gcpClient) DeleteWorkloadIdentityPool(ctx context.Context, resource str } //nolint:lll -func (c *gcpClient) UndeleteWorkloadIdentityPool(ctx context.Context, resource string, request *iamv1.UndeleteWorkloadIdentityPoolRequest) (*iamv1.Operation, error) { - return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Undelete(resource, request).Context(ctx).Do() +func (c *gcpClient) GetProjectIamPolicy( + ctx context.Context, + projectName string, + request *cloudresourcemanager.GetIamPolicyRequest, +) (*cloudresourcemanager.Policy, error) { + return c.cloudResourceManager.Projects.GetIamPolicy(projectName, request).Context(context.Background()).Do() +} + +func (c *gcpClient) GetRole(ctx context.Context, request *adminpb.GetRoleRequest) (*adminpb.Role, error) { + return c.iamClient.GetRole(ctx, request) } //nolint:lll -func (c *gcpClient) CreateWorkloadIdentityProvider(ctx context.Context, parent, providerID string, provider *iamv1.WorkloadIdentityPoolProvider) (*iamv1.Operation, error) { - return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Providers.Create(parent, provider).WorkloadIdentityPoolProviderId(providerID).Context(ctx).Do() +func (c *gcpClient) GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) { + return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Get(resource).Context(ctx).Do() } //nolint:lll @@ -305,7 +205,7 @@ func (c *gcpClient) GetWorkloadIdentityProvider(ctx context.Context, resource st return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Providers.Get(resource).Context(ctx).Do() } -func (c *gcpClient) ProjectNumberFromId(projectId string) (int64, error) { +func (c *gcpClient) ProjectNumberFromId(ctx context.Context, projectId string) (int64, error) { project, err := c.cloudResourceManager.Projects.Get(projectId).Do() if err != nil { return 0, err @@ -314,36 +214,19 @@ func (c *gcpClient) ProjectNumberFromId(projectId string) (int64, error) { } //nolint:lll -func (c *gcpClient) GetProjectIamPolicy(projectName string, request *cloudresourcemanager.GetIamPolicyRequest) (*cloudresourcemanager.Policy, error) { - return c.cloudResourceManager.Projects.GetIamPolicy(projectName, request).Context(context.Background()).Do() -} - -//nolint:lll -func (c *gcpClient) SetProjectIamPolicy(svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) { - return c.cloudResourceManager.Projects.SetIamPolicy(svcAcctResource, request).Context(context.Background()).Do() +func (c *gcpClient) SetProjectIamPolicy(ctx context.Context, svcAcctResource string, request *cloudresourcemanager.SetIamPolicyRequest) (*cloudresourcemanager.Policy, error) { + return c.cloudResourceManager.Projects.SetIamPolicy(svcAcctResource, request).Context(ctx).Do() } -func (c *gcpClient) GetRole(ctx context.Context, request *adminpb.GetRoleRequest) (*adminpb.Role, error) { - return c.iamClient.GetRole(ctx, request) +func (c *gcpClient) UndeleteRole(ctx context.Context, request *adminpb.UndeleteRoleRequest) (*adminpb.Role, error) { + return c.iamClient.UndeleteRole(ctx, request) } -func (c *gcpClient) CreateRole(ctx context.Context, request *adminpb.CreateRoleRequest) (*adminpb.Role, error) { - return c.iamClient.CreateRole(ctx, request) +//nolint:lll +func (c *gcpClient) UndeleteWorkloadIdentityPool(ctx context.Context, resource string, request *iamv1.UndeleteWorkloadIdentityPoolRequest) (*iamv1.Operation, error) { + return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Undelete(resource, request).Context(ctx).Do() } func (c *gcpClient) UpdateRole(ctx context.Context, request *adminpb.UpdateRoleRequest) (*adminpb.Role, error) { return c.iamClient.UpdateRole(ctx, request) } - -func (c *gcpClient) DeleteRole(ctx context.Context, request *adminpb.DeleteRoleRequest) (*adminpb.Role, error) { - return c.iamClient.DeleteRole(ctx, request) -} - -func (c *gcpClient) UndeleteRole(ctx context.Context, request *adminpb.UndeleteRoleRequest) (*adminpb.Role, error) { - return c.iamClient.UndeleteRole(ctx, request) -} - -//nolint:lll -func (c *gcpClient) ListRoles(ctx context.Context, request *adminpb.ListRolesRequest) (*adminpb.ListRolesResponse, error) { - return c.iamClient.ListRoles(ctx, request) -} diff --git a/pkg/gcp/error_handlers.go b/pkg/gcp/error_handlers.go index 8d8dc423..a8d6bd2d 100644 --- a/pkg/gcp/error_handlers.go +++ b/pkg/gcp/error_handlers.go @@ -2,10 +2,8 @@ package gcp import ( "fmt" - "net/http" "github.com/googleapis/gax-go/v2/apierror" - "google.golang.org/api/googleapi" "google.golang.org/grpc/codes" ) @@ -26,14 +24,6 @@ func (c *gcpClient) handleAttachWorkloadIdentityPoolError(err error) error { return fmt.Errorf(pApiError.Error()) } -func (c *gcpClient) handleListServiceAccountError(err error) error { - pApiError, ok := err.(*apierror.APIError) - if !ok { - return fmt.Errorf("Unexpected error") - } - return fmt.Errorf(pApiError.Details().String()) -} - func (c *gcpClient) handleDeleteServiceAccountError(err error, allowMissing bool) error { pApiError, ok := err.(*apierror.APIError) if !ok { @@ -44,24 +34,3 @@ func (c *gcpClient) handleDeleteServiceAccountError(err error, allowMissing bool } return fmt.Errorf(pApiError.Details().String()) } - -func (c *gcpClient) handleRetrieveSecretError(err error) ([]byte, error) { - gApiError, ok := err.(*googleapi.Error) - if !ok { - return []byte{}, fmt.Errorf("Unexpected error") - } - return []byte{}, gApiError -} - -// Errors that can't be converted to *googleapi.Error are unexpected -// If the secret already exists, this is not considered an error -func (c *gcpClient) handleSaveSecretError(err error) error { - gApiError, ok := err.(*googleapi.Error) - if !ok { - return fmt.Errorf("Unexpected error") - } - if gApiError.Code == http.StatusConflict { - return nil - } - return gApiError -} From 17958de0169f9fed3c4cdcc8268e5f6eaed444c6 Mon Sep 17 00:00:00 2001 From: Renan Campos Date: Mon, 16 Sep 2024 15:43:44 -0400 Subject: [PATCH 4/5] updating wif logic for determining role updates The prior check was lead to custom roles being updated during every wif creation call if the permission set provided was not in the exact order that is returned by GCP- emperically found to be alphabetical. With this change, this assumption is no longer necassary. --- cmd/ocm/gcp/gcp-client-shim.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/cmd/ocm/gcp/gcp-client-shim.go b/cmd/ocm/gcp/gcp-client-shim.go index c89c6543..2bee4652 100644 --- a/cmd/ocm/gcp/gcp-client-shim.go +++ b/cmd/ocm/gcp/gcp-client-shim.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "reflect" "strings" "cloud.google.com/go/iam/admin/apiv1/adminpb" @@ -250,7 +249,7 @@ func (c *shim) createOrUpdateRoles( } // Update role if permissions have changed - if !reflect.DeepEqual(existingRole.IncludedPermissions, permissions) { + if c.roleRequiresUpdate(permissions, existingRole.IncludedPermissions) { existingRole.IncludedPermissions = permissions _, err := c.updateRole(ctx, existingRole, c.fmtRoleResourceId(role)) if err != nil { @@ -262,6 +261,25 @@ func (c *shim) createOrUpdateRoles( return nil } +func (c *shim) roleRequiresUpdate( + newPermissions []string, + existingPermissions []string, +) bool { + permissionMap := map[string]bool{} + for _, permission := range existingPermissions { + permissionMap[permission] = true + } + if len(permissionMap) != len(newPermissions) { + return true + } + for _, permission := range newPermissions { + if !permissionMap[permission] { + return true + } + } + return false +} + func (c *shim) bindRolesToServiceAccount( ctx context.Context, serviceAccount *cmv1.WifServiceAccount, From 0961981158472b5764a9ba43f008ee38223b53b3 Mon Sep 17 00:00:00 2001 From: Renan Campos Date: Fri, 13 Sep 2024 14:36:41 -0400 Subject: [PATCH 5/5] fix: tolerate required delay between service account creation and configuration It was discovered through testing that service accounts created on GCP need a duration of time between creation and being referenced, otherwise a BadRequest error occurs. A delayed retry logic is introduced to ensure the service account is available before making additional configuration calls. --- cmd/ocm/gcp/gcp-client-shim.go | 23 ++++++++++++++++++----- pkg/gcp/client.go | 13 ++++++++----- pkg/utils/helper.go | 13 +++++++++++++ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/cmd/ocm/gcp/gcp-client-shim.go b/cmd/ocm/gcp/gcp-client-shim.go index 2bee4652..fc5998be 100644 --- a/cmd/ocm/gcp/gcp-client-shim.go +++ b/cmd/ocm/gcp/gcp-client-shim.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "time" "cloud.google.com/go/iam/admin/apiv1/adminpb" "github.com/googleapis/gax-go/v2/apierror" @@ -16,6 +17,12 @@ import ( "google.golang.org/grpc/codes" "github.com/openshift-online/ocm-cli/pkg/gcp" + "github.com/openshift-online/ocm-cli/pkg/utils" +) + +const ( + maxRetries = 10 + retryDelayMs = 500 ) type GcpClientWifConfigShim interface { @@ -287,11 +294,17 @@ func (c *shim) bindRolesToServiceAccount( serviceAccountId := serviceAccount.ServiceAccountId() roles := serviceAccount.Roles() - return c.bindRolesToPrincipal( - ctx, - fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccountId, c.wifConfig.Gcp().ProjectId()), - roles, - ) + // It was found that there is a window of time between when a service + // account creation call is made that the service account is not available + // in adjacent API calls. The call is therefore wrapped in retry logic to + // be robust to these types of synchronization issues. + return utils.DelayedRetry(func() error { + return c.bindRolesToPrincipal( + ctx, + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", serviceAccountId, c.wifConfig.Gcp().ProjectId()), + roles, + ) + }, maxRetries, retryDelayMs*time.Millisecond) } func (c *shim) bindRolesToGroup( diff --git a/pkg/gcp/client.go b/pkg/gcp/client.go index 371734e5..dd227e44 100644 --- a/pkg/gcp/client.go +++ b/pkg/gcp/client.go @@ -17,11 +17,6 @@ import ( ) type GcpClient interface { - /* - ListServiceAccounts(ctx context.Context, project string, filter func(s string) bool) ([]string, error) //nolint:lll - DeleteRole(context.Context, *adminpb.DeleteRoleRequest) (*adminpb.Role, error) - ListRoles(context.Context, *adminpb.ListRolesRequest) (*adminpb.ListRolesResponse, error) - */ AttachImpersonator(ctx context.Context, saId, projectId, impersonatorResourceId string) error AttachWorkloadIdentityPool(ctx context.Context, sa *cmv1.WifServiceAccount, poolId, projectId string) error CreateRole(context.Context, *adminpb.CreateRoleRequest) (*adminpb.Role, error) @@ -32,6 +27,7 @@ type GcpClient interface { DeleteWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.Operation, error) //nolint:lll GetProjectIamPolicy(ctx context.Context, projectName string, request *cloudresourcemanager.GetIamPolicyRequest) (*cloudresourcemanager.Policy, error) //nolint:lll GetRole(context.Context, *adminpb.GetRoleRequest) (*adminpb.Role, error) + GetServiceAccount(ctx context.Context, request *adminpb.GetServiceAccountRequest) (*adminpb.ServiceAccount, error) GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) //nolint:lll GetWorkloadIdentityProvider(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPoolProvider, error) //nolint:lll ProjectNumberFromId(ctx context.Context, projectId string) (int64, error) @@ -195,6 +191,13 @@ func (c *gcpClient) GetRole(ctx context.Context, request *adminpb.GetRoleRequest return c.iamClient.GetRole(ctx, request) } +func (c *gcpClient) GetServiceAccount( + ctx context.Context, + request *adminpb.GetServiceAccountRequest, +) (*adminpb.ServiceAccount, error) { + return c.iamClient.GetServiceAccount(ctx, request) +} + //nolint:lll func (c *gcpClient) GetWorkloadIdentityPool(ctx context.Context, resource string) (*iamv1.WorkloadIdentityPool, error) { return c.oldIamClient.Projects.Locations.WorkloadIdentityPools.Get(resource).Context(ctx).Do() diff --git a/pkg/utils/helper.go b/pkg/utils/helper.go index 77a42ba1..070f975e 100644 --- a/pkg/utils/helper.go +++ b/pkg/utils/helper.go @@ -6,6 +6,7 @@ import ( "net/url" "os" "regexp" + "time" ) // the following regex defines four different patterns: @@ -90,3 +91,15 @@ func HasDuplicates(valSlice []string) (string, bool) { } return "", false } + +func DelayedRetry(f func() error, maxRetries int, delay time.Duration) error { + var err error + for i := 0; i < maxRetries; i++ { + err = f() + if err == nil { + return nil + } + time.Sleep(delay) + } + return fmt.Errorf("Reached max retries. Last error: %s", err.Error()) +}