mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-29 09:26:17 -05:00
184 lines
5.8 KiB
Go
184 lines
5.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. 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. 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. 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, fileHandler, creator)
|
||
|
}
|
||
|
|
||
|
func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Handler, creator iamCreator) 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")
|
||
|
cmd.Printf("Project ID:\t%s\n", gcpFlags.projectID)
|
||
|
cmd.Printf("Service Account ID:\t%s\n", gcpFlags.serviceAccountID)
|
||
|
cmd.Printf("Region:\t%s\n", gcpFlags.region)
|
||
|
cmd.Printf("Zone:\t%s\n", gcpFlags.zone)
|
||
|
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)
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
cmd.Println(fmt.Sprintf("serviceAccountKeyPath:\t%s", 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)
|
||
|
}
|
||
|
|
||
|
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,
|
||
|
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
|
||
|
yesFlag bool
|
||
|
}
|