diff --git a/cmd/ocm/create/cluster/cmd.go b/cmd/ocm/create/cluster/cmd.go index a424ebdd..965b791a 100644 --- a/cmd/ocm/create/cluster/cmd.go +++ b/cmd/ocm/create/cluster/cmd.go @@ -477,16 +477,21 @@ func GetDefaultClusterFlavors(connection *sdk.Connection, flavour string) (dMach } func getVersionOptions(connection *sdk.Connection) ([]arguments.Option, error) { - options, _, err := getVersionOptionsWithDefault(connection, "", "") + options, _, err := getVersionOptionsWithDefault(connection, "", "", "") return options, err } -func getVersionOptionsWithDefault(connection *sdk.Connection, channelGroup string, gcpMarketplaceEnabled string) ( +func getVersionOptionsWithDefault( + connection *sdk.Connection, + channelGroup string, + gcpMarketplaceEnabled string, + additionalFilters string, +) ( options []arguments.Option, defaultVersion string, err error, ) { // Check and set the cluster version versionList, defaultVersion, err := c.GetEnabledVersions( - connection.ClustersMgmt().V1(), channelGroup, gcpMarketplaceEnabled) + connection.ClustersMgmt().V1(), channelGroup, gcpMarketplaceEnabled, additionalFilters) if err != nil { return } @@ -674,8 +679,9 @@ func preRun(cmd *cobra.Command, argv []string) error { if isGcpMarketplace { gcpMarketplaceEnabled = strconv.FormatBool(isGcpMarketplace) } + additionalFilters := getVersionFilters() versions, defaultVersion, err := getVersionOptionsWithDefault(connection, args.channelGroup, - gcpMarketplaceEnabled) + gcpMarketplaceEnabled, additionalFilters) if err != nil { return err } @@ -1573,3 +1579,13 @@ func promptAutoscaling(fs *pflag.FlagSet) error { } return nil } + +// getVersionFilters returns a version filter based on the current args +func getVersionFilters() string { + filter := "" + // WIF filter + if args.gcpAuthentication.Type == c.AuthenticationWif { + filter = fmt.Sprintf("%s AND wif_enabled = 'true'", filter) + } + return filter +} diff --git a/cmd/ocm/gcp/create-wif-config.go b/cmd/ocm/gcp/create-wif-config.go index 2f59858c..67cb7808 100644 --- a/cmd/ocm/gcp/create-wif-config.go +++ b/cmd/ocm/gcp/create-wif-config.go @@ -4,8 +4,6 @@ import ( "context" "fmt" "log" - "os" - "path/filepath" "strconv" "github.com/openshift-online/ocm-cli/pkg/gcp" @@ -65,26 +63,10 @@ func validationForCreateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, arg 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) + var err error + CreateWifConfigOpts.TargetDir, err = getPathFromFlag(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 err } return nil } @@ -116,7 +98,7 @@ func createWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e if err != nil { return errors.Wrapf(err, "failed to get project number from id") } - err = createScript(CreateWifConfigOpts.TargetDir, wifConfig, projectNum) + err = createCreateScript(CreateWifConfigOpts.TargetDir, wifConfig, projectNum) if err != nil { return errors.Wrapf(err, "Failed to create script files") } diff --git a/cmd/ocm/gcp/delete-wif-config.go b/cmd/ocm/gcp/delete-wif-config.go index 576c3f14..29beb83c 100644 --- a/cmd/ocm/gcp/delete-wif-config.go +++ b/cmd/ocm/gcp/delete-wif-config.go @@ -27,7 +27,7 @@ var ( // NewDeleteWorkloadIdentityConfiguration provides the "gcp delete wif-config" subcommand func NewDeleteWorkloadIdentityConfiguration() *cobra.Command { deleteWifConfigCmd := &cobra.Command{ - Use: "wif-config [ID]", + Use: "wif-config [ID|Name]", Short: "Delete workload identity configuration", RunE: deleteWorkloadIdentityConfigurationCmd, PreRunE: validationForDeleteWorkloadIdentityConfigurationCmd, @@ -42,21 +42,19 @@ func NewDeleteWorkloadIdentityConfiguration() *cobra.Command { } func validationForDeleteWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { - if len(argv) != 1 { - return fmt.Errorf( - "expected exactly one command line parameters containing the id " + - "of the WIF config", - ) + var err error + DeleteWifConfigOpts.TargetDir, err = getPathFromFlag(DeleteWifConfigOpts.TargetDir) + if err != nil { + return err } return nil } func deleteWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { ctx := context.Background() - - wifConfigId := argv[0] - if wifConfigId == "" { - return fmt.Errorf("WIF config ID is required") + key, err := wifKeyFromArgs(argv) + if err != nil { + return err } // Create the client for the OCM API: @@ -66,11 +64,11 @@ func deleteWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e } defer connection.Close() - response, err := connection.ClustersMgmt().V1().GCP().WifConfigs().WifConfig(wifConfigId).Get().Send() + // Verify the WIF configuration exists + wifConfig, err := findWifConfig(connection.ClustersMgmt().V1(), key) if err != nil { return errors.Wrapf(err, "failed to get wif-config") } - wifConfig := response.Body() if DeleteWifConfigOpts.DryRun { log.Printf("Writing script files to %s", DeleteWifConfigOpts.TargetDir) @@ -96,11 +94,11 @@ func deleteWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e } _, err = connection.ClustersMgmt().V1().GCP().WifConfigs(). - WifConfig(wifConfigId). + WifConfig(wifConfig.ID()). Delete(). Send() if err != nil { - return errors.Wrapf(err, "failed to delete wif config %q", wifConfigId) + return errors.Wrapf(err, "failed to delete wif config %q", wifConfig.ID()) } return nil } diff --git a/cmd/ocm/gcp/describe-wif-config.go b/cmd/ocm/gcp/describe-wif-config.go index bb5cd326..26a7a9db 100644 --- a/cmd/ocm/gcp/describe-wif-config.go +++ b/cmd/ocm/gcp/describe-wif-config.go @@ -6,7 +6,6 @@ import ( "text/tabwriter" "github.com/openshift-online/ocm-cli/pkg/ocm" - "github.com/openshift-online/ocm-cli/pkg/urls" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -14,19 +13,18 @@ import ( // NewDescribeWorkloadIdentityConfiguration provides the "gcp describe wif-config" subcommand func NewDescribeWorkloadIdentityConfiguration() *cobra.Command { describeWorkloadIdentityPoolCmd := &cobra.Command{ - Use: "wif-config [ID]", - Short: "Show details of a wif-config.", - RunE: describeWorkloadIdentityConfigurationCmd, - PreRunE: validationForDescribeWorkloadIdentityConfigurationCmd, + Use: "wif-config [ID|Name]", + Short: "Show details of a wif-config.", + RunE: describeWorkloadIdentityConfigurationCmd, } return describeWorkloadIdentityPoolCmd } func describeWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { - id, err := urls.Expand(argv) + key, err := wifKeyFromArgs(argv) if err != nil { - return errors.Wrapf(err, "could not create URI") + return err } // Create the client for the OCM API: @@ -36,11 +34,11 @@ func describeWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) } defer connection.Close() - response, err := connection.ClustersMgmt().V1().GCP().WifConfigs().WifConfig(id).Get().Send() + // Verify the WIF configuration exists + wifConfig, err := findWifConfig(connection.ClustersMgmt().V1(), key) if err != nil { return errors.Wrapf(err, "failed to get wif-config") } - wifConfig := response.Body() // Print output w := tabwriter.NewWriter(os.Stdout, 8, 0, 2, ' ', 0) @@ -52,10 +50,3 @@ func describeWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) return w.Flush() } - -func validationForDescribeWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { - if len(argv) != 1 { - return fmt.Errorf("Expected exactly one command line parameters containing the id of the WIF config") - } - return nil -} diff --git a/cmd/ocm/gcp/gcp-client-shim.go b/cmd/ocm/gcp/gcp-client-shim.go index fc5998be..2a2fc86e 100644 --- a/cmd/ocm/gcp/gcp-client-shim.go +++ b/cmd/ocm/gcp/gcp-client-shim.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "sort" "strings" "time" @@ -86,8 +87,6 @@ func (c *shim) CreateWorkloadIdentityPool( } 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 @@ -136,8 +135,6 @@ func (c *shim) CreateWorkloadIdentityProvider( 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 @@ -175,7 +172,6 @@ func (c *shim) GrantSupportAccess( if err := c.bindRolesToGroup(ctx, support.Principal(), support.Roles()); err != nil { return err } - log.Printf("support access granted to %s", support.Principal()) return nil } @@ -255,9 +251,11 @@ func (c *shim) createOrUpdateRoles( log.Printf("Role %q undeleted", roleID) } - // Update role if permissions have changed - if c.roleRequiresUpdate(permissions, existingRole.IncludedPermissions) { - existingRole.IncludedPermissions = permissions + if addedPermissions, needsUpdate := c.missingPermissions(permissions, existingRole.IncludedPermissions); needsUpdate { + // Add missing permissions + existingRole.IncludedPermissions = append(existingRole.IncludedPermissions, addedPermissions...) + sort.Strings(existingRole.IncludedPermissions) + _, err := c.updateRole(ctx, existingRole, c.fmtRoleResourceId(role)) if err != nil { return errors.Wrap(err, fmt.Sprintf("Failed to update %s", roleID)) @@ -268,23 +266,27 @@ func (c *shim) createOrUpdateRoles( return nil } -func (c *shim) roleRequiresUpdate( +// missingPermissions returns true if there are new permissions that are not in the existing permissions +// and returns the list of missing permissions +func (c *shim) missingPermissions( newPermissions []string, existingPermissions []string, -) bool { +) ([]string, bool) { + missing := []string{} 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 + missing = append(missing, permission) } } - return false + if len(missing) > 0 { + return missing, true + } else { + return missing, false + } } func (c *shim) bindRolesToServiceAccount( diff --git a/cmd/ocm/gcp/gcp.go b/cmd/ocm/gcp/gcp.go index 9cbbc68b..7486dc6e 100644 --- a/cmd/ocm/gcp/gcp.go +++ b/cmd/ocm/gcp/gcp.go @@ -30,7 +30,6 @@ func NewGcpCmd() *cobra.Command { gcpCmd.AddCommand(NewGetCmd()) gcpCmd.AddCommand(NewListCmd()) gcpCmd.AddCommand(NewDescribeCmd()) - gcpCmd.AddCommand(NewGenerateCommand()) return gcpCmd } diff --git a/cmd/ocm/gcp/generate-wif-script.go b/cmd/ocm/gcp/generate-wif-script.go deleted file mode 100644 index 5a6ca45e..00000000 --- a/cmd/ocm/gcp/generate-wif-script.go +++ /dev/null @@ -1,84 +0,0 @@ -package gcp - -import ( - "context" - "fmt" - "log" - - "github.com/openshift-online/ocm-cli/pkg/gcp" - "github.com/openshift-online/ocm-cli/pkg/ocm" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -var ( - // CreateWorkloadIdentityPoolOpts captures the options that affect creation of the workload identity pool - GenerateScriptOpts = options{ - TargetDir: "", - } -) - -func NewGenerateCommand() *cobra.Command { - generateScriptCmd := &cobra.Command{ - Use: "generate [wif-config ID]", - Short: "Generate script based on a wif-config", - Args: cobra.ExactArgs(1), - RunE: generateCreateScriptCmd, - PreRunE: validationForGenerateCreateScriptCmd, - } - - generateScriptCmd.PersistentFlags().StringVar(&GenerateScriptOpts.TargetDir, "output-dir", "", - "Directory to place generated files (defaults to current directory)") - - return generateScriptCmd -} - -func validationForGenerateCreateScriptCmd(cmd *cobra.Command, argv []string) error { - if len(argv) != 1 { - return fmt.Errorf( - "Expected exactly one command line parameters containing the id " + - "of the WIF config.", - ) - } - return nil -} - -func generateCreateScriptCmd(cmd *cobra.Command, argv []string) error { - ctx := context.Background() - - gcpClient, err := gcp.NewGcpClient(ctx) - if err != nil { - errors.Wrapf(err, "failed to initiate GCP client") - } - - connection, err := ocm.NewConnection().Build() - if err != nil { - return errors.Wrapf(err, "Failed to create OCM connection") - } - defer connection.Close() - - wifConfigId := argv[0] - if wifConfigId == "" { - return fmt.Errorf("WIF config ID is required") - } - - response, err := connection.ClustersMgmt().V1().GCP().WifConfigs().WifConfig(wifConfigId).Get().Send() - if err != nil { - return errors.Wrapf(err, "failed to get wif-config") - } - wifConfig := response.Body() - - projectNum, err := gcpClient.ProjectNumberFromId(ctx, wifConfig.Gcp().ProjectId()) - if err != nil { - return errors.Wrapf(err, "failed to get project number from id") - } - - log.Printf("Writing script files to %s", GenerateScriptOpts.TargetDir) - if err := createScript(GenerateScriptOpts.TargetDir, wifConfig, projectNum); err != nil { - return errors.Wrapf(err, "failed to generate create script") - } - if err := createDeleteScript(GenerateScriptOpts.TargetDir, wifConfig); err != nil { - return errors.Wrapf(err, "failed to generate delete script") - } - return nil -} diff --git a/cmd/ocm/gcp/helpers.go b/cmd/ocm/gcp/helpers.go new file mode 100644 index 00000000..6298d28c --- /dev/null +++ b/cmd/ocm/gcp/helpers.go @@ -0,0 +1,77 @@ +package gcp + +import ( + "fmt" + "os" + "path/filepath" + + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + "github.com/pkg/errors" +) + +// Checks for WIF config name or id in input +func wifKeyArgCheck(args []string) error { + if len(args) != 1 || args[0] == "" { + return fmt.Errorf("expected exactly one command line parameters containing the name " + + "or ID of the WIF config") + } + return nil +} + +// Extracts WIF config name or id from input +func wifKeyFromArgs(args []string) (string, error) { + if err := wifKeyArgCheck(args); err != nil { + return "", err + } + return args[0], nil +} + +// findWifConfig finds the WIF configuration by ID or name +func findWifConfig(client *cmv1.Client, key string) (*cmv1.WifConfig, error) { + collection := client.GCP().WifConfigs() + page := 1 + size := 1 + query := fmt.Sprintf( + "id = '%s' or display_name = '%s'", + key, key, + ) + + response, err := collection.List().Search(query).Page(page).Size(size).Send() + if err != nil { + return nil, err + } + if response.Total() == 0 { + return nil, fmt.Errorf("WIF configuration with identifier or name '%s' not found", key) + } + if response.Total() > 1 { + return nil, fmt.Errorf("there are %d WIF configurations found with identifier or name '%s'", response.Total(), key) + } + return response.Items().Slice()[0], nil +} + +// getPathFromFlag validates the filepath +func getPathFromFlag(targetDir string) (string, error) { + if targetDir == "" { + pwd, err := os.Getwd() + if err != nil { + return "", errors.Wrapf(err, "failed to get current directory") + } + + return pwd, nil + } + + fPath, err := filepath.Abs(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 targetDir, nil +} diff --git a/cmd/ocm/gcp/list-wif-config.go b/cmd/ocm/gcp/list-wif-config.go index 98f88363..025380fa 100644 --- a/cmd/ocm/gcp/list-wif-config.go +++ b/cmd/ocm/gcp/list-wif-config.go @@ -24,7 +24,6 @@ func NewListWorkloadIdentityConfiguration() *cobra.Command { Aliases: []string{"wif-configs"}, Short: "List wif-configs.", RunE: listWorkloadIdentityConfigurationCmd, - PreRunE: validationForListWorkloadIdentityConfigurationCmd, } fs := listWorkloadIdentityPoolCmd.Flags() @@ -44,11 +43,6 @@ func NewListWorkloadIdentityConfiguration() *cobra.Command { return listWorkloadIdentityPoolCmd } -func validationForListWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { - // No validation needed - return nil -} - func listWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { // Create a context: ctx := context.Background() diff --git a/cmd/ocm/gcp/scripting.go b/cmd/ocm/gcp/scripting.go index 8f54f111..c528d38f 100644 --- a/cmd/ocm/gcp/scripting.go +++ b/cmd/ocm/gcp/scripting.go @@ -9,10 +9,28 @@ import ( cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" ) -func createScript(targetDir string, wifConfig *cmv1.WifConfig, projectNum int64) error { +const bashShebang = "#!/bin/bash\n" + +func createCreateScript(targetDir string, wifConfig *cmv1.WifConfig, projectNum int64) error { // Write the script content to the path - scriptContent := generateScriptContent(wifConfig, projectNum) - err := os.WriteFile(filepath.Join(targetDir, "script.sh"), []byte(scriptContent), 0600) + scriptContent := generateCreateScriptContent(wifConfig, projectNum) + err := os.WriteFile(filepath.Join(targetDir, "create.sh"), []byte(scriptContent), 0600) + if err != nil { + return err + } + // Write jwk json file to the path + jwkPath := filepath.Join(targetDir, "jwk.json") + err = os.WriteFile(jwkPath, []byte(wifConfig.Gcp().WorkloadIdentityPool().IdentityProvider().Jwks()), 0600) + if err != nil { + return err + } + return nil +} + +func createUpdateScript(targetDir string, wifConfig *cmv1.WifConfig, projectNum int64) error { + // Write the script content to the path + scriptContent := generateUpdateScriptContent(wifConfig, projectNum) + err := os.WriteFile(filepath.Join(targetDir, "apply.sh"), []byte(scriptContent), 0600) if err != nil { return err } @@ -36,7 +54,7 @@ func createDeleteScript(targetDir string, wifConfig *cmv1.WifConfig) error { } func generateDeleteScriptContent(wifConfig *cmv1.WifConfig) string { - scriptContent := "#!/bin/bash\n" + scriptContent := bashShebang // Append the script to delete the service accounts scriptContent += deleteServiceAccountScriptContent(wifConfig) @@ -67,8 +85,8 @@ gcloud iam workload-identity-pools delete %s --project=%s --location=global `, pool.PoolId(), wifConfig.Gcp().ProjectId()) } -func generateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { - scriptContent := "#!/bin/bash\n" +func generateCreateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { + scriptContent := bashShebang // Create a script to create the workload identity pool scriptContent += createIdentityPoolScriptContent(wifConfig) @@ -84,6 +102,23 @@ func generateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { return scriptContent } +func generateUpdateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { + scriptContent := bashShebang + + // Create a script to create the workload identity pool + scriptContent += createIdentityPoolScriptContent(wifConfig) + + // Append the script to create the identity provider + scriptContent += createIdentityProviderScriptContent(wifConfig) + + // Append the script to create/update the service accounts + scriptContent += updateServiceAccountScriptContent(wifConfig, projectNum) + + scriptContent += grantSupportAccessScriptContent(wifConfig) + + return scriptContent +} + func createIdentityPoolScriptContent(wifConfig *cmv1.WifConfig) string { name := wifConfig.Gcp().WorkloadIdentityPool().PoolId() project := wifConfig.Gcp().ProjectId() @@ -125,6 +160,44 @@ func createServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int var sb strings.Builder sb.WriteString("\n# Create service accounts:\n") + sb.WriteString(createServiceAccountScript(wifConfig)) + + sb.WriteString("\n# Create custom roles for service accounts:\n") + sb.WriteString(createCustomRoleScript(wifConfig)) + + sb.WriteString("\n# Bind roles to service accounts:\n") + sb.WriteString(addRoleBindingsScript(wifConfig)) + + sb.WriteString("\n# Grant access to service accounts:\n") + sb.WriteString(grantServiceAccountAccessScript(wifConfig, projectNum)) + + return sb.String() +} + +func updateServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { + // For each service account, create a service account and bind it to the workload identity pool + var sb strings.Builder + + sb.WriteString("\n# Create service accounts:\n") + sb.WriteString(createServiceAccountScript(wifConfig)) + + sb.WriteString("\n# Create custom roles for service accounts:\n") + sb.WriteString(createCustomRoleScript(wifConfig)) + + sb.WriteString("\n# Update custom roles for service accounts:\n") + sb.WriteString(updateCustomRolesScript(wifConfig)) + + sb.WriteString("\n# Bind roles to service accounts:\n") + sb.WriteString(addRoleBindingsScript(wifConfig)) + + sb.WriteString("\n# Grant access to service accounts:\n") + sb.WriteString(grantServiceAccountAccessScript(wifConfig, projectNum)) + + return sb.String() +} + +func createServiceAccountScript(wifConfig *cmv1.WifConfig) string { + var sb strings.Builder for _, sa := range wifConfig.Gcp().ServiceAccounts() { project := wifConfig.Gcp().ProjectId() serviceAccountID := sa.ServiceAccountId() @@ -134,11 +207,15 @@ 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 custom roles for service accounts:\n") + return sb.String() +} + +func createCustomRoleScript(wifConfig *cmv1.WifConfig) string { + var sb strings.Builder for _, sa := range wifConfig.Gcp().ServiceAccounts() { for _, role := range sa.Roles() { if !role.Predefined() { - roleId := strings.ReplaceAll(role.RoleId(), "-", "_") + roleId := role.RoleId() project := wifConfig.Gcp().ProjectId() permissions := strings.Join(role.Permissions(), ",") roleName := roleId @@ -149,7 +226,27 @@ func createServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int } } } - sb.WriteString("\n# Bind roles to service accounts:\n") + return sb.String() +} + +func updateCustomRolesScript(wifConfig *cmv1.WifConfig) string { + var sb strings.Builder + for _, sa := range wifConfig.Gcp().ServiceAccounts() { + for _, role := range sa.Roles() { + if !role.Predefined() { + project := wifConfig.Gcp().ProjectId() + permissions := strings.Join(role.Permissions(), ",") + //nolint:lll + sb.WriteString(fmt.Sprintf("gcloud iam roles update %s --project=%s --permissions=%s\n", + role.RoleId(), project, permissions)) + } + } + } + return sb.String() +} + +func addRoleBindingsScript(wifConfig *cmv1.WifConfig) string { + var sb strings.Builder for _, sa := range wifConfig.Gcp().ServiceAccounts() { for _, role := range sa.Roles() { project := wifConfig.Gcp().ProjectId() @@ -164,7 +261,11 @@ func createServiceAccountScriptContent(wifConfig *cmv1.WifConfig, projectNum int project, member, roleResource)) } } - sb.WriteString("\n# Grant access to service accounts:\n") + return sb.String() +} + +func grantServiceAccountAccessScript(wifConfig *cmv1.WifConfig, projectNum int64) string { + var sb strings.Builder for _, sa := range wifConfig.Gcp().ServiceAccounts() { if sa.AccessMethod() == "wif" { project := wifConfig.Gcp().ProjectId() @@ -198,13 +299,16 @@ func grantSupportAccessScriptContent(wifConfig *cmv1.WifConfig) string { sb.WriteString("\n# Create custom roles for support:\n") for _, role := range roles { if !role.Predefined() { - roleId := strings.ReplaceAll(role.RoleId(), "-", "_") + roleId := 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)) + //nolint:lll + sb.WriteString(fmt.Sprintf("gcloud iam roles update %s --project=%s --permissions=%s\n", + roleId, project, permissions)) } } diff --git a/cmd/ocm/gcp/update-wif-config.go b/cmd/ocm/gcp/update-wif-config.go index 31273157..8e3945f2 100644 --- a/cmd/ocm/gcp/update-wif-config.go +++ b/cmd/ocm/gcp/update-wif-config.go @@ -1,39 +1,110 @@ package gcp import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/openshift-online/ocm-cli/pkg/gcp" + "github.com/openshift-online/ocm-cli/pkg/ocm" + "github.com/pkg/errors" "github.com/spf13/cobra" ) -var UpdateWifConfigOpts struct { - wifId string - templateId string -} +var ( + UpdateWifConfigOpts = options{ + DryRun: false, + TargetDir: "", + } +) // NewUpdateWorkloadIdentityConfiguration provides the "gcp update wif-config" subcommand func NewUpdateWorkloadIdentityConfiguration() *cobra.Command { updateWifConfigCmd := &cobra.Command{ - Use: "wif-config", + Use: "wif-config [ID|Name]", Short: "Update wif-config.", RunE: updateWorkloadIdentityConfigurationCmd, PreRunE: validationForUpdateWorkloadIdentityConfigurationCmd, } - updateWifConfigCmd.PersistentFlags().StringVar(&UpdateWifConfigOpts.wifId, "wif-id", "", - "Workload Identity Federation ID") - updateWifConfigCmd.MarkPersistentFlagRequired("wif-id") - updateWifConfigCmd.PersistentFlags().StringVar(&UpdateWifConfigOpts.templateId, "template-id", "", - "Template ID") - updateWifConfigCmd.MarkPersistentFlagRequired("template-id") + updateWifConfigCmd.PersistentFlags().BoolVar(&UpdateWifConfigOpts.DryRun, "dry-run", false, + "Skip creating objects, and just save what would have been created into files") + updateWifConfigCmd.PersistentFlags().StringVar(&UpdateWifConfigOpts.TargetDir, "output-dir", "", + "Directory to place generated files (defaults to current directory)") return updateWifConfigCmd } func validationForUpdateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { - // No validation needed + var err error + UpdateWifConfigOpts.TargetDir, err = getPathFromFlag(UpdateWifConfigOpts.TargetDir) + if err != nil { + return err + } return nil } func updateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { - // No implementation yet + ctx := context.Background() + log := log.Default() + key, err := wifKeyFromArgs(argv) + if err != nil { + return err + } + + // Create the client for the OCM API: + connection, err := ocm.NewConnection().Build() + if err != nil { + return errors.Wrapf(err, "Failed to create OCM connection") + } + defer connection.Close() + + // Verify the WIF configuration exists + wifConfig, err := findWifConfig(connection.ClustersMgmt().V1(), key) + if err != nil { + return errors.Wrapf(err, "failed to get wif-config") + } + + gcpClient, err := gcp.NewGcpClient(ctx) + if err != nil { + return errors.Wrapf(err, "failed to initiate GCP client") + } + + if UpdateWifConfigOpts.DryRun { + log.Printf("Writing script files to %s", UpdateWifConfigOpts.TargetDir) + projectNumInt64, err := strconv.ParseInt(wifConfig.Gcp().ProjectNumber(), 10, 64) + if err != nil { + return errors.Wrapf(err, "failed to parse project number from WifConfig") + } + + if err := createUpdateScript(UpdateWifConfigOpts.TargetDir, wifConfig, projectNumInt64); err != nil { + return errors.Wrapf(err, "failed to generate script files") + } + return nil + } + + // Re-apply WIF resources + gcpClientWifConfigShim := NewGcpClientWifConfigShim(GcpClientWifConfigShimSpec{ + GcpClient: gcpClient, + WifConfig: wifConfig, + }) + + if err := gcpClientWifConfigShim.GrantSupportAccess(ctx, log); err != nil { + return fmt.Errorf("Failed to grant support access to project: %s", err) + } + + if err := gcpClientWifConfigShim.CreateWorkloadIdentityPool(ctx, log); err != nil { + return fmt.Errorf("Failed to update workload identity pool: %s", err) + } + + if err = gcpClientWifConfigShim.CreateWorkloadIdentityProvider(ctx, log); err != nil { + return fmt.Errorf("Failed to update workload identity provider: %s", err) + } + + if err = gcpClientWifConfigShim.CreateServiceAccounts(ctx, log); err != nil { + return fmt.Errorf("Failed to update IAM service accounts: %s", err) + } + return nil } diff --git a/cmd/ocm/list/version/cmd.go b/cmd/ocm/list/version/cmd.go index 0fc67062..ab0b74df 100644 --- a/cmd/ocm/list/version/cmd.go +++ b/cmd/ocm/list/version/cmd.go @@ -74,7 +74,7 @@ func run(cmd *cobra.Command, argv []string) error { defer connection.Close() client := connection.ClustersMgmt().V1() - versions, defaultVersion, err := cluster.GetEnabledVersions(client, args.channelGroup, args.marketplaceGcp) + versions, defaultVersion, err := cluster.GetEnabledVersions(client, args.channelGroup, args.marketplaceGcp, "") if err != nil { return fmt.Errorf("Can't retrieve versions: %v", err) } diff --git a/pkg/cluster/versions.go b/pkg/cluster/versions.go index fd292662..1f67554a 100644 --- a/pkg/cluster/versions.go +++ b/pkg/cluster/versions.go @@ -42,7 +42,11 @@ func EnsureOpenshiftVPrefix(v string) string { // GetEnabledVersions returns the versions with enabled=true, and the one that has default=true. // The returned strings are the IDs without "openshift-v" prefix (e.g. "4.6.0-rc.4-candidate") // sorted in approximate SemVer order (handling of text parts is somewhat arbitrary). -func GetEnabledVersions(client *cmv1.Client, channelGroup string, gcpMarketplaceEnabled string) ( +func GetEnabledVersions(client *cmv1.Client, + channelGroup string, + gcpMarketplaceEnabled string, + additionalFilters string, +) ( versions []string, defaultVersion string, err error) { collection := client.Versions() page := 1 @@ -54,6 +58,9 @@ func GetEnabledVersions(client *cmv1.Client, channelGroup string, gcpMarketplace if channelGroup != "" { filter = fmt.Sprintf("%s AND channel_group = '%s'", filter, channelGroup) } + if additionalFilters != "" { + filter = fmt.Sprintf("%s %s", filter, additionalFilters) + } for { response, err := collection.List(). Search(filter).