constellation/cli/internal/cmd/iamcreategcp.go
Moritz Sanft e844ceb2b1
cli: adopt Cobra cli reference style (#997)
* adapt to Cobra CLI ref style

* linting

* change multi-line reference style

* lowercase short descriptions

* Revert "lowercase short descriptions"

This reverts commit 499dc3577a.

* use 2 newlines on long description and add dots

* mark required flags

* Update cli/internal/cmd/iamcreateaws.go

Co-authored-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>

* Update cli/internal/cmd/upgradeexecute.go

Co-authored-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>

* Update cli/internal/cmd/upgradeexecute.go

Co-authored-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>

Co-authored-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
2023-01-17 14:01:56 +01:00

216 lines
6.8 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"encoding/base64"
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
var (
zoneRegex = regexp.MustCompile(`^\w+-\w+-[abc]$`)
regionRegex = regexp.MustCompile(`^\w+-\w+[0-9]$`)
projectIDRegex = regexp.MustCompile(`^[a-z][-a-z0-9]{4,28}[a-z0-9]{1}$`)
serviceAccIDRegex = regexp.MustCompile(`^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$`)
)
// NewIAMCreateGCPCmd returns a new cobra.Command for the iam create gcp command.
func newIAMCreateGCPCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "gcp",
Short: "Create IAM configuration on GCP for your Constellation cluster",
Long: "Create IAM configuration on GCP for your Constellation cluster.",
Args: cobra.ExactArgs(0),
RunE: runIAMCreateGCP,
}
cmd.Flags().String("zone", "", "GCP zone the cluster will be deployed in (required)\n"+
"Find a list of available zones here: https://cloud.google.com/compute/docs/regions-zones#available.")
must(cobra.MarkFlagRequired(cmd.Flags(), "zone"))
cmd.Flags().String("serviceAccountID", "", "ID for the service account that will be created (required)\n"+
"Must match ^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$.")
must(cobra.MarkFlagRequired(cmd.Flags(), "serviceAccountID"))
cmd.Flags().String("projectID", "", "ID of the GCP project the configuration will be created in (required)\n"+
"Find it on the welcome screen of your project: https://console.cloud.google.com/welcome.")
must(cobra.MarkFlagRequired(cmd.Flags(), "projectID"))
cmd.Flags().Bool("yes", false, "create the IAM configuration without further confirmation")
return cmd
}
func runIAMCreateGCP(cmd *cobra.Command, args []string) error {
fileHandler := file.NewHandler(afero.NewOsFs())
spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop()
creator := cloudcmd.NewIAMCreator(spinner)
return iamCreateGCP(cmd, spinner, creator, fileHandler)
}
func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator, fileHandler file.Handler) error {
// Get input variables.
gcpFlags, err := parseGCPFlags(cmd)
if err != nil {
return err
}
// Confirmation.
if !gcpFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n\n")
cmd.Printf("Project ID:\t\t%s\n", gcpFlags.projectID)
cmd.Printf("Service Account ID:\t%s\n", gcpFlags.serviceAccountID)
cmd.Printf("Region:\t\t\t%s\n", gcpFlags.region)
cmd.Printf("Zone:\t\t\t%s\n\n", gcpFlags.zone)
if gcpFlags.generateConfig {
cmd.Printf("The configuration file %s will be automatically generated and populated with the IAM values.\n", gcpFlags.configPath)
}
ok, err := askToConfirm(cmd, "Do you want to create the configuration?")
if err != nil {
return err
}
if !ok {
cmd.Println("The creation of the configuration was aborted.")
return nil
}
}
// Creation.
spinner.Start("Creating", false)
conf := createConfig(cloudprovider.GCP)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.GCP, &cloudcmd.IAMConfig{
GCP: cloudcmd.GCPIAMConfig{
ServiceAccountID: gcpFlags.serviceAccountID,
Region: gcpFlags.region,
Zone: gcpFlags.zone,
ProjectID: gcpFlags.projectID,
},
})
spinner.Stop()
if err != nil {
return err
}
cmd.Println() // Print empty line to separate after spinner ended.
// Write back values.
tmpOut, err := parseIDFile(iamFile.GCPOutput.ServiceAccountKey)
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.GCPServiceAccountKeyFile, tmpOut, file.OptNone); err != nil {
return err
}
if gcpFlags.generateConfig {
conf.Provider.GCP.ServiceAccountKeyPath = constants.GCPServiceAccountKeyFile
if err := fileHandler.WriteYAML(gcpFlags.configPath, conf, file.OptMkdirAll); err != nil {
return err
}
cmd.Printf("Your IAM configuration was created and filled into %s successfully.\n", gcpFlags.configPath)
return nil
}
cmd.Println(fmt.Sprintf("serviceAccountKeyPath:\t%s\n", constants.GCPServiceAccountKeyFile))
cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.")
return nil
}
func parseIDFile(serviceAccountKeyBase64 string) (map[string]string, error) {
dec, err := base64.StdEncoding.DecodeString(serviceAccountKeyBase64)
if err != nil {
return nil, err
}
out := make(map[string]string)
if err = json.Unmarshal(dec, &out); err != nil {
return nil, err
}
return out, nil
}
// parseGCPFlags parses and validates the flags of the iam create gcp command.
func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
zone, err := cmd.Flags().GetString("zone")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing zone string: %w", err)
}
if !zoneRegex.MatchString(zone) {
return gcpFlags{}, fmt.Errorf("invalid zone string: %s", zone)
}
// Infer region from zone.
zoneParts := strings.Split(zone, "-")
region := fmt.Sprintf("%s-%s", zoneParts[0], zoneParts[1])
if !regionRegex.MatchString(region) {
return gcpFlags{}, fmt.Errorf("invalid region string: %s", region)
}
projectID, err := cmd.Flags().GetString("projectID")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing projectID string: %w", err)
}
// Source for regex: https://cloud.google.com/resource-manager/reference/rest/v1/projects.
if !projectIDRegex.MatchString(projectID) {
return gcpFlags{}, fmt.Errorf("invalid projectID string: %s", projectID)
}
serviceAccID, err := cmd.Flags().GetString("serviceAccountID")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing serviceAccountID string: %w", err)
}
if !serviceAccIDRegex.MatchString(serviceAccID) {
return gcpFlags{}, fmt.Errorf("invalid serviceAccountID string: %s", serviceAccID)
}
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing config string: %w", err)
}
generateConfig, err := cmd.Flags().GetBool("generate-config")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing generate-config bool: %w", err)
}
yesFlag, err := cmd.Flags().GetBool("yes")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing yes bool: %w", err)
}
return gcpFlags{
serviceAccountID: serviceAccID,
zone: zone,
region: region,
projectID: projectID,
generateConfig: generateConfig,
configPath: configPath,
yesFlag: yesFlag,
}, nil
}
// gcpFlags contains the parsed flags of the iam create gcp command.
type gcpFlags struct {
serviceAccountID string
zone string
region string
projectID string
generateConfig bool
configPath string
yesFlag bool
}