From 209d01a354a2ed4b13dbeca278bf0fad8f048497 Mon Sep 17 00:00:00 2001 From: Jakob Gray <20209054+JakobGray@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:18:57 -0400 Subject: [PATCH] Update WIF scripting logic --- cmd/ocm/gcp/create-wif-config.go | 26 +----- cmd/ocm/gcp/delete-wif-config.go | 5 ++ cmd/ocm/gcp/gcp.go | 1 - cmd/ocm/gcp/generate-wif-script.go | 72 ---------------- cmd/ocm/gcp/helpers.go | 30 +++++++ cmd/ocm/gcp/scripting.go | 128 +++++++++++++++++++++++++++-- cmd/ocm/gcp/update-wif-config.go | 43 ++++++++-- 7 files changed, 196 insertions(+), 109 deletions(-) delete mode 100644 cmd/ocm/gcp/generate-wif-script.go 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 d2f3f854..29beb83c 100644 --- a/cmd/ocm/gcp/delete-wif-config.go +++ b/cmd/ocm/gcp/delete-wif-config.go @@ -42,6 +42,11 @@ func NewDeleteWorkloadIdentityConfiguration() *cobra.Command { } func validationForDeleteWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { + var err error + DeleteWifConfigOpts.TargetDir, err = getPathFromFlag(DeleteWifConfigOpts.TargetDir) + if err != nil { + return err + } return nil } 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 f6d1ce48..00000000 --- a/cmd/ocm/gcp/generate-wif-script.go +++ /dev/null @@ -1,72 +0,0 @@ -package gcp - -import ( - "context" - "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|Name]", - Short: "Generate script based on a wif-config", - Args: cobra.ExactArgs(1), - RunE: generateCreateScriptCmd, - } - - generateScriptCmd.PersistentFlags().StringVar(&GenerateScriptOpts.TargetDir, "output-dir", "", - "Directory to place generated files (defaults to current directory)") - - return generateScriptCmd -} - -func generateCreateScriptCmd(cmd *cobra.Command, argv []string) error { - ctx := context.Background() - 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() - - gcpClient, err := gcp.NewGcpClient(ctx) - if err != nil { - errors.Wrapf(err, "failed to initiate GCP client") - } - - // Verify the WIF configuration exists - wifConfig, err := findWifConfig(connection.ClustersMgmt().V1(), key) - if err != nil { - return errors.Wrapf(err, "failed to get wif-config") - } - - 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 index 19ca216e..6298d28c 100644 --- a/cmd/ocm/gcp/helpers.go +++ b/cmd/ocm/gcp/helpers.go @@ -2,8 +2,11 @@ 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 @@ -45,3 +48,30 @@ func findWifConfig(client *cmv1.Client, key string) (*cmv1.WifConfig, error) { } 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/scripting.go b/cmd/ocm/gcp/scripting.go index 8f54f111..90ae8e64 100644 --- a/cmd/ocm/gcp/scripting.go +++ b/cmd/ocm/gcp/scripting.go @@ -9,10 +9,26 @@ import ( cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" ) -func createScript(targetDir string, wifConfig *cmv1.WifConfig, projectNum int64) error { +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 } @@ -67,7 +83,7 @@ gcloud iam workload-identity-pools delete %s --project=%s --location=global `, pool.PoolId(), wifConfig.Gcp().ProjectId()) } -func generateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { +func generateCreateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { scriptContent := "#!/bin/bash\n" // Create a script to create the workload identity pool @@ -84,6 +100,23 @@ func generateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { return scriptContent } +func generateUpdateScriptContent(wifConfig *cmv1.WifConfig, projectNum int64) string { + scriptContent := "#!/bin/bash\n" + + // 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 +158,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 +205,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 +224,33 @@ 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() + var roleResource string + if role.Predefined() { + roleResource = fmt.Sprintf("roles/%s", role.RoleId()) + } else { + roleResource = fmt.Sprintf("projects/%s/roles/%s", project, role.RoleId()) + } + permissions := strings.Join(role.Permissions(), ",") + //nolint:lll + sb.WriteString(fmt.Sprintf("gcloud iam roles update %s --project=%s--permissions=%s\n", + roleResource, 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 +265,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 +303,18 @@ 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)) + + roleResource := fmt.Sprintf("projects/%s/roles/%s", project, roleId) + //nolint:lll + sb.WriteString(fmt.Sprintf("gcloud iam roles update %s --project=%s--permissions=%s\n", + roleResource, project, permissions)) } } diff --git a/cmd/ocm/gcp/update-wif-config.go b/cmd/ocm/gcp/update-wif-config.go index 236484db..8e3945f2 100644 --- a/cmd/ocm/gcp/update-wif-config.go +++ b/cmd/ocm/gcp/update-wif-config.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strconv" "github.com/openshift-online/ocm-cli/pkg/gcp" "github.com/openshift-online/ocm-cli/pkg/ocm" @@ -11,20 +12,39 @@ import ( "github.com/spf13/cobra" ) -var UpdateWifConfigOpts struct { -} +var ( + UpdateWifConfigOpts = options{ + DryRun: false, + TargetDir: "", + } +) // NewUpdateWorkloadIdentityConfiguration provides the "gcp update wif-config" subcommand func NewUpdateWorkloadIdentityConfiguration() *cobra.Command { updateWifConfigCmd := &cobra.Command{ - Use: "wif-config [ID|Name]", - Short: "Update wif-config.", - RunE: updateWorkloadIdentityConfigurationCmd, + Use: "wif-config [ID|Name]", + Short: "Update wif-config.", + RunE: updateWorkloadIdentityConfigurationCmd, + PreRunE: validationForUpdateWorkloadIdentityConfigurationCmd, } + 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 { + var err error + UpdateWifConfigOpts.TargetDir, err = getPathFromFlag(UpdateWifConfigOpts.TargetDir) + if err != nil { + return err + } + return nil +} + func updateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) error { ctx := context.Background() log := log.Default() @@ -51,6 +71,19 @@ func updateWorkloadIdentityConfigurationCmd(cmd *cobra.Command, argv []string) e 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,