2022-12-07 11:48:54 +01:00
/ *
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" ) )
2023-01-12 11:35:26 +01:00
cmd . Flags ( ) . Bool ( "yes" , false , "Create the IAM configuration without further confirmation." )
2022-12-07 11:48:54 +01:00
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 )
2023-01-12 11:35:26 +01:00
return iamCreateGCP ( cmd , spinner , creator , fileHandler )
2022-12-07 11:48:54 +01:00
}
2023-01-12 11:35:26 +01:00
func iamCreateGCP ( cmd * cobra . Command , spinner spinnerInterf , creator iamCreator , fileHandler file . Handler ) error {
2022-12-07 11:48:54 +01:00
// Get input variables.
gcpFlags , err := parseGCPFlags ( cmd )
if err != nil {
return err
}
// Confirmation.
if ! gcpFlags . yesFlag {
2023-01-12 11:35:26 +01:00
cmd . Printf ( "The following IAM configuration will be created:\n\n" )
cmd . Printf ( "Project ID:\t\t%s\n" , gcpFlags . projectID )
2022-12-07 11:48:54 +01:00
cmd . Printf ( "Service Account ID:\t%s\n" , gcpFlags . serviceAccountID )
2023-01-12 11:35:26 +01:00
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 )
}
2022-12-07 11:48:54 +01:00
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 )
2023-01-12 11:35:26 +01:00
conf := createConfig ( cloudprovider . GCP )
2022-12-07 11:48:54 +01:00
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 ,
} ,
} )
2023-01-12 11:35:26 +01:00
2022-12-07 11:48:54 +01:00
spinner . Stop ( )
if err != nil {
return err
}
2023-01-12 11:35:26 +01:00
cmd . Println ( ) // Print empty line to separate after spinner ended.
2022-12-07 11:48:54 +01:00
// 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
}
2023-01-12 11:35:26 +01:00
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 ) )
2022-12-07 11:48:54 +01:00
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 )
}
2023-01-12 11:35:26 +01:00
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 )
}
2022-12-07 11:48:54 +01:00
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 ,
2023-01-12 11:35:26 +01:00
generateConfig : generateConfig ,
configPath : configPath ,
2022-12-07 11:48:54 +01:00
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
2023-01-12 11:35:26 +01:00
generateConfig bool
configPath string
2022-12-07 11:48:54 +01:00
yesFlag bool
}