diff --git a/cmd/ocm/gcp/create-wif-config.go b/cmd/ocm/gcp/create-wif-config.go index 8c4b4a05..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,217 +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()) - } - 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..fc5998be --- /dev/null +++ b/cmd/ocm/gcp/gcp-client-shim.go @@ -0,0 +1,547 @@ +package gcp + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "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" + "github.com/openshift-online/ocm-cli/pkg/utils" +) + +const ( + maxRetries = 10 + retryDelayMs = 500 +) + +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 c.roleRequiresUpdate(permissions, existingRole.IncludedPermissions) { + 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) 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, +) error { + serviceAccountId := serviceAccount.ServiceAccountId() + roles := serviceAccount.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( + 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/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= diff --git a/pkg/gcp/client.go b/pkg/gcp/client.go index 10876f8c..dd227e44 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,28 @@ 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 - + 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) + 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) + 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 +80,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 +92,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 +102,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 +130,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 +140,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 +158,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 +179,28 @@ 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) +} + +func (c *gcpClient) GetServiceAccount( + ctx context.Context, + request *adminpb.GetServiceAccountRequest, +) (*adminpb.ServiceAccount, error) { + return c.iamClient.GetServiceAccount(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 +208,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 +217,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 -} 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()) +}