cli: automatically add iam values to config (#782)

* AB#2706 Automatically add IAM values to config
This commit is contained in:
Moritz Sanft 2023-01-12 11:35:26 +01:00 committed by GitHub
parent c66119fe93
commit 64ec0408da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 570 additions and 110 deletions

View File

@ -60,15 +60,10 @@ func (cg *configGenerateCmd) configGenerate(cmd *cobra.Command, fileHandler file
if err != nil { if err != nil {
return err return err
} }
cg.log.Debugf("Parsed flags as %v", flags)
conf := config.Default()
conf.RemoveProviderExcept(provider)
cg.log.Debugf("Using cloud provider %s", provider.String())
// set a lower default for QEMU's state disk
if provider == cloudprovider.QEMU {
conf.StateDiskSizeGB = 10
}
cg.log.Debugf("Parsed flags as %v", flags)
cg.log.Debugf("Using cloud provider %s", provider.String())
conf := createConfig(provider)
if flags.file == "-" { if flags.file == "-" {
content, err := encoder.NewEncoder(conf).Encode() content, err := encoder.NewEncoder(conf).Encode()
if err != nil { if err != nil {
@ -84,6 +79,7 @@ func (cg *configGenerateCmd) configGenerate(cmd *cobra.Command, fileHandler file
if err := fileHandler.WriteYAML(flags.file, conf, file.OptMkdirAll); err != nil { if err := fileHandler.WriteYAML(flags.file, conf, file.OptMkdirAll); err != nil {
return err return err
} }
cmd.Println("Config file written to", flags.file) cmd.Println("Config file written to", flags.file)
cmd.Println("Please fill in your CSP-specific configuration before proceeding.") cmd.Println("Please fill in your CSP-specific configuration before proceeding.")
cmd.Println("For more information refer to the documentation:") cmd.Println("For more information refer to the documentation:")
@ -92,6 +88,19 @@ func (cg *configGenerateCmd) configGenerate(cmd *cobra.Command, fileHandler file
return nil return nil
} }
// createConfig creates a config file for the given provider.
func createConfig(provider cloudprovider.Provider) *config.Config {
conf := config.Default()
conf.RemoveProviderExcept(provider)
// set a lower default for QEMU's state disk
if provider == cloudprovider.QEMU {
conf.StateDiskSizeGB = 10
}
return conf
}
func parseGenerateFlags(cmd *cobra.Command) (generateFlags, error) { func parseGenerateFlags(cmd *cobra.Command) (generateFlags, error) {
file, err := cmd.Flags().GetString("file") file, err := cmd.Flags().GetString("file")
if err != nil { if err != nil {

View File

@ -32,6 +32,8 @@ func newIAMCreateCmd() *cobra.Command {
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
} }
cmd.PersistentFlags().Bool("generate-config", false, "Automatically generate a config file and fill in the required fields")
cmd.AddCommand(newIAMCreateAWSCmd()) cmd.AddCommand(newIAMCreateAWSCmd())
cmd.AddCommand(newIAMCreateAzureCmd()) cmd.AddCommand(newIAMCreateAzureCmd())
cmd.AddCommand(newIAMCreateGCPCmd()) cmd.AddCommand(newIAMCreateGCPCmd())

View File

@ -11,6 +11,8 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -28,7 +30,7 @@ func newIAMCreateAWSCmd() *cobra.Command {
must(cobra.MarkFlagRequired(cmd.Flags(), "prefix")) must(cobra.MarkFlagRequired(cmd.Flags(), "prefix"))
cmd.Flags().String("zone", "", "AWS availability zone the resources will be created in (e.g. us-east-2a). Find available zones here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones. Note that we do not support every zone / region. You can find a list of all supported regions in our docs.") cmd.Flags().String("zone", "", "AWS availability zone the resources will be created in (e.g. us-east-2a). Find available zones here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones. Note that we do not support every zone / region. You can find a list of all supported regions in our docs.")
must(cobra.MarkFlagRequired(cmd.Flags(), "zone")) must(cobra.MarkFlagRequired(cmd.Flags(), "zone"))
cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation") cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation.")
return cmd return cmd
} }
@ -36,12 +38,13 @@ func newIAMCreateAWSCmd() *cobra.Command {
func runIAMCreateAWS(cmd *cobra.Command, args []string) error { func runIAMCreateAWS(cmd *cobra.Command, args []string) error {
spinner := newSpinner(cmd.ErrOrStderr()) spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop() defer spinner.Stop()
fileHandler := file.NewHandler(afero.NewOsFs())
creator := cloudcmd.NewIAMCreator(spinner) creator := cloudcmd.NewIAMCreator(spinner)
return iamCreateAWS(cmd, spinner, creator) return iamCreateAWS(cmd, spinner, creator, fileHandler)
} }
func iamCreateAWS(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator) error { func iamCreateAWS(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator, fileHandler file.Handler) error {
// Get input variables. // Get input variables.
awsFlags, err := parseAWSFlags(cmd) awsFlags, err := parseAWSFlags(cmd)
if err != nil { if err != nil {
@ -50,9 +53,12 @@ func iamCreateAWS(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator)
// Confirmation. // Confirmation.
if !awsFlags.yesFlag { if !awsFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n") cmd.Printf("The following IAM configuration will be created:\n\n")
cmd.Printf("Region:\t%s\n", awsFlags.region) cmd.Printf("Region:\t\t%s\n", awsFlags.region)
cmd.Printf("Name Prefix:\t%s\n", awsFlags.prefix) cmd.Printf("Name Prefix:\t%s\n\n", awsFlags.prefix)
if awsFlags.generateConfig {
cmd.Printf("The configuration file %s will be automatically generated and populated with the IAM values.\n", awsFlags.configPath)
}
ok, err := askToConfirm(cmd, "Do you want to create the configuration?") ok, err := askToConfirm(cmd, "Do you want to create the configuration?")
if err != nil { if err != nil {
return err return err
@ -65,21 +71,39 @@ func iamCreateAWS(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator)
// Creation. // Creation.
spinner.Start("Creating", false) spinner.Start("Creating", false)
conf := createConfig(cloudprovider.AWS)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.AWS, &cloudcmd.IAMConfig{ iamFile, err := creator.Create(cmd.Context(), cloudprovider.AWS, &cloudcmd.IAMConfig{
AWS: cloudcmd.AWSIAMConfig{ AWS: cloudcmd.AWSIAMConfig{
Region: awsFlags.region, Region: awsFlags.region,
Prefix: awsFlags.prefix, Prefix: awsFlags.prefix,
}, },
}) })
spinner.Stop() spinner.Stop()
if err != nil { if err != nil {
return err return err
} }
cmd.Println() // Print empty line to separate after spinner ended.
cmd.Printf("region:\t%s\n", awsFlags.region) if awsFlags.generateConfig {
cmd.Printf("zone:\t%s\n", awsFlags.zone) conf.Provider.AWS.Region = awsFlags.region
conf.Provider.AWS.Zone = awsFlags.zone
conf.Provider.AWS.IAMProfileControlPlane = iamFile.AWSOutput.ControlPlaneInstanceProfile
conf.Provider.AWS.IAMProfileWorkerNodes = iamFile.AWSOutput.WorkerNodeInstanceProfile
if err := fileHandler.WriteYAML(awsFlags.configPath, conf, file.OptMkdirAll); err != nil {
return err
}
cmd.Printf("Your IAM configuration was created and filled into %s successfully.\n", awsFlags.configPath)
return nil
}
cmd.Printf("region:\t\t\t%s\n", awsFlags.region)
cmd.Printf("zone:\t\t\t%s\n", awsFlags.zone)
cmd.Printf("iamProfileControlPlane:\t%s\n", iamFile.AWSOutput.ControlPlaneInstanceProfile) cmd.Printf("iamProfileControlPlane:\t%s\n", iamFile.AWSOutput.ControlPlaneInstanceProfile)
cmd.Printf("iamProfileWorkerNodes:\t%s\n", iamFile.AWSOutput.WorkerNodeInstanceProfile) cmd.Printf("iamProfileWorkerNodes:\t%s\n\n", iamFile.AWSOutput.WorkerNodeInstanceProfile)
cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.") cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.")
return nil return nil
@ -106,7 +130,14 @@ func parseAWSFlags(cmd *cobra.Command) (awsFlags, error) {
} else { } else {
return awsFlags{}, fmt.Errorf("invalid AWS region, to find a correct region please refer to our docs and https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones") return awsFlags{}, fmt.Errorf("invalid AWS region, to find a correct region please refer to our docs and https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones")
} }
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return awsFlags{}, fmt.Errorf("parsing config string: %w", err)
}
generateConfig, err := cmd.Flags().GetBool("generate-config")
if err != nil {
return awsFlags{}, fmt.Errorf("parsing generate-config bool: %w", err)
}
yesFlag, err := cmd.Flags().GetBool("yes") yesFlag, err := cmd.Flags().GetBool("yes")
if err != nil { if err != nil {
return awsFlags{}, fmt.Errorf("parsing yes bool: %w", err) return awsFlags{}, fmt.Errorf("parsing yes bool: %w", err)
@ -115,6 +146,8 @@ func parseAWSFlags(cmd *cobra.Command) (awsFlags, error) {
zone: zone, zone: zone,
prefix: prefix, prefix: prefix,
region: region, region: region,
generateConfig: generateConfig,
configPath: configPath,
yesFlag: yesFlag, yesFlag: yesFlag,
}, nil }, nil
} }
@ -124,5 +157,7 @@ type awsFlags struct {
prefix string prefix string
region string region string
zone string zone string
generateConfig bool
configPath string
yesFlag bool yesFlag bool
} }

View File

@ -7,6 +7,7 @@ package cmd
import ( import (
"bytes" "bytes"
"strings"
"testing" "testing"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid" "github.com/edgelesssys/constellation/v2/cli/internal/iamid"
@ -20,10 +21,16 @@ import (
) )
func TestIAMCreateAWS(t *testing.T) { func TestIAMCreateAWS(t *testing.T) {
fsWithDefaultConfig := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs { defaultFs := func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
file := file.NewHandler(fs) fileHandler := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider))) for _, f := range existingFiles {
require.NoError(fileHandler.Write(f, []byte{1, 2, 3}, file.OptNone))
}
return fs
}
readOnlyFs := func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs {
fs := afero.NewReadOnlyFs(afero.NewMemMapFs())
return fs return fs
} }
validIAMIDFile := iamid.File{ validIAMIDFile := iamid.File{
@ -35,34 +42,91 @@ func TestIAMCreateAWS(t *testing.T) {
} }
testCases := map[string]struct { testCases := map[string]struct {
setupFs func(*require.Assertions, cloudprovider.Provider) afero.Fs setupFs func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs
creator *stubIAMCreator creator *stubIAMCreator
provider cloudprovider.Provider provider cloudprovider.Provider
zoneFlag string zoneFlag string
prefixFlag string prefixFlag string
yesFlag bool yesFlag bool
generateConfigFlag bool
configFlag string
existingFiles []string
stdin string stdin string
wantAbort bool wantAbort bool
wantErr bool wantErr bool
}{ }{
"iam create aws": { "iam create aws": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS, provider: cloudprovider.AWS,
zoneFlag: "us-east-2a", zoneFlag: "us-east-2a",
prefixFlag: "test", prefixFlag: "test",
yesFlag: true, yesFlag: true,
}, },
"iam create aws generate config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
yesFlag: true,
configFlag: constants.ConfigFilename,
generateConfigFlag: true,
},
"iam create aws generate config custom path": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
yesFlag: true,
generateConfigFlag: true,
configFlag: "custom-config.yaml",
},
"iam create aws generate config path already exists": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
yesFlag: true,
generateConfigFlag: true,
wantErr: true,
configFlag: constants.ConfigFilename,
existingFiles: []string{constants.ConfigFilename},
},
"iam create aws generate config custom path already exists": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
yesFlag: true,
generateConfigFlag: true,
wantErr: true,
configFlag: "custom-config.yaml",
existingFiles: []string{"custom-config.yaml"},
},
"interactive": { "interactive": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS, provider: cloudprovider.AWS,
zoneFlag: "us-east-2a", zoneFlag: "us-east-2a",
prefixFlag: "test", prefixFlag: "test",
stdin: "yes\n", stdin: "yes\n",
}, },
"interactive generate config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
stdin: "yes\n",
configFlag: constants.ConfigFilename,
generateConfigFlag: true,
},
"interactive abort": { "interactive abort": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS, provider: cloudprovider.AWS,
zoneFlag: "us-east-2a", zoneFlag: "us-east-2a",
@ -70,8 +134,19 @@ func TestIAMCreateAWS(t *testing.T) {
stdin: "no\n", stdin: "no\n",
wantAbort: true, wantAbort: true,
}, },
"interactive generate config abort": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
stdin: "no\n",
generateConfigFlag: true,
configFlag: constants.ConfigFilename,
wantAbort: true,
},
"invalid zone": { "invalid zone": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS, provider: cloudprovider.AWS,
zoneFlag: "us-west-5b", zoneFlag: "us-west-5b",
@ -79,6 +154,17 @@ func TestIAMCreateAWS(t *testing.T) {
yesFlag: true, yesFlag: true,
wantErr: true, wantErr: true,
}, },
"unwritable fs": {
setupFs: readOnlyFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
yesFlag: true,
generateConfigFlag: true,
wantErr: true,
configFlag: constants.ConfigFilename,
},
} }
for name, tc := range testCases { for name, tc := range testCases {
@ -90,6 +176,10 @@ func TestIAMCreateAWS(t *testing.T) {
cmd.SetOut(&bytes.Buffer{}) cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin)) cmd.SetIn(bytes.NewBufferString(tc.stdin))
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("generate-config", false, "") // register persistent flag manually
if tc.zoneFlag != "" { if tc.zoneFlag != "" {
require.NoError(cmd.Flags().Set("zone", tc.zoneFlag)) require.NoError(cmd.Flags().Set("zone", tc.zoneFlag))
} }
@ -99,20 +189,39 @@ func TestIAMCreateAWS(t *testing.T) {
if tc.yesFlag { if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true")) require.NoError(cmd.Flags().Set("yes", "true"))
} }
if tc.generateConfigFlag {
require.NoError(cmd.Flags().Set("generate-config", "true"))
}
if tc.configFlag != "" {
require.NoError(cmd.Flags().Set("config", tc.configFlag))
}
err := iamCreateAWS(cmd, &nopSpinner{}, tc.creator) fileHandler := file.NewHandler(tc.setupFs(require, tc.provider, tc.existingFiles))
err := iamCreateAWS(cmd, &nopSpinner{}, tc.creator, fileHandler)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
} else { return
}
if tc.wantAbort { if tc.wantAbort {
assert.False(tc.creator.createCalled) assert.False(tc.creator.createCalled)
} else { return
assert.NoError(err) }
if tc.generateConfigFlag {
readConfig := &config.Config{}
readErr := fileHandler.ReadYAML(tc.configFlag, readConfig)
require.NoError(readErr)
assert.Equal(tc.creator.id.AWSOutput.ControlPlaneInstanceProfile, readConfig.Provider.AWS.IAMProfileControlPlane)
assert.Equal(tc.creator.id.AWSOutput.WorkerNodeInstanceProfile, readConfig.Provider.AWS.IAMProfileWorkerNodes)
assert.Equal(tc.zoneFlag, readConfig.Provider.AWS.Zone)
assert.True(strings.HasPrefix(readConfig.Provider.AWS.Zone, readConfig.Provider.AWS.Region))
}
require.NoError(err)
assert.True(tc.creator.createCalled) assert.True(tc.creator.createCalled)
assert.Equal(tc.creator.id.AWSOutput, validIAMIDFile.AWSOutput) assert.Equal(tc.creator.id.AWSOutput, validIAMIDFile.AWSOutput)
}
}
}) })
} }
} }

View File

@ -10,6 +10,8 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -23,13 +25,13 @@ func newIAMCreateAzureCmd() *cobra.Command {
RunE: runIAMCreateAzure, RunE: runIAMCreateAzure,
} }
cmd.Flags().String("resourceGroup", "", "Name of the resource group your IAM resources will be created in.") cmd.Flags().String("resourceGroup", "", "Name prefix of the two resource groups your cluster / IAM resources will be created in.")
must(cobra.MarkFlagRequired(cmd.Flags(), "resourceGroup")) must(cobra.MarkFlagRequired(cmd.Flags(), "resourceGroup"))
cmd.Flags().String("region", "", "Region the resources will be created in. (e.g. westus)") cmd.Flags().String("region", "", "Region the resources will be created in. (e.g. westus)")
must(cobra.MarkFlagRequired(cmd.Flags(), "region")) must(cobra.MarkFlagRequired(cmd.Flags(), "region"))
cmd.Flags().String("servicePrincipal", "", "Name of the service principal that will be created.") cmd.Flags().String("servicePrincipal", "", "Name of the service principal that will be created.")
must(cobra.MarkFlagRequired(cmd.Flags(), "servicePrincipal")) must(cobra.MarkFlagRequired(cmd.Flags(), "servicePrincipal"))
cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation") cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation.")
return cmd return cmd
} }
@ -37,12 +39,13 @@ func newIAMCreateAzureCmd() *cobra.Command {
func runIAMCreateAzure(cmd *cobra.Command, args []string) error { func runIAMCreateAzure(cmd *cobra.Command, args []string) error {
spinner := newSpinner(cmd.ErrOrStderr()) spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop() defer spinner.Stop()
fileHandler := file.NewHandler(afero.NewOsFs())
creator := cloudcmd.NewIAMCreator(spinner) creator := cloudcmd.NewIAMCreator(spinner)
return iamCreateAzure(cmd, spinner, creator) return iamCreateAzure(cmd, spinner, creator, fileHandler)
} }
func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator) error { func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator, fileHandler file.Handler) error {
// Get input variables. // Get input variables.
azureFlags, err := parseAzureFlags(cmd) azureFlags, err := parseAzureFlags(cmd)
if err != nil { if err != nil {
@ -51,10 +54,13 @@ func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreato
// Confirmation. // Confirmation.
if !azureFlags.yesFlag { if !azureFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n") cmd.Printf("The following IAM configuration will be created:\n\n")
cmd.Printf("Region:\t%s\n", azureFlags.region) cmd.Printf("Region:\t\t\t%s\n", azureFlags.region)
cmd.Printf("Resource Group:\t%s\n", azureFlags.resourceGroup) cmd.Printf("Resource Group:\t\t%s\n", azureFlags.resourceGroup)
cmd.Printf("Service Principal:\t%s\n", azureFlags.servicePrincipal) cmd.Printf("Service Principal:\t%s\n\n", azureFlags.servicePrincipal)
if azureFlags.generateConfig {
cmd.Printf("The configuration file %s will be automatically generated and populated with the IAM values.\n", azureFlags.configPath)
}
ok, err := askToConfirm(cmd, "Do you want to create the configuration?") ok, err := askToConfirm(cmd, "Do you want to create the configuration?")
if err != nil { if err != nil {
return err return err
@ -67,6 +73,9 @@ func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreato
// Creation. // Creation.
spinner.Start("Creating", false) spinner.Start("Creating", false)
conf := createConfig(cloudprovider.Azure)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.Azure, &cloudcmd.IAMConfig{ iamFile, err := creator.Create(cmd.Context(), cloudprovider.Azure, &cloudcmd.IAMConfig{
Azure: cloudcmd.AzureIAMConfig{ Azure: cloudcmd.AzureIAMConfig{
Region: azureFlags.region, Region: azureFlags.region,
@ -74,18 +83,35 @@ func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreato
ResourceGroup: azureFlags.resourceGroup, ResourceGroup: azureFlags.resourceGroup,
}, },
}) })
spinner.Stop() spinner.Stop()
if err != nil { if err != nil {
return err return err
} }
cmd.Println() // Print empty line to separate after spinner ended.
cmd.Printf("subscription:\t%s\n", iamFile.AzureOutput.SubscriptionID) if azureFlags.generateConfig {
cmd.Printf("tenant:\t%s\n", iamFile.AzureOutput.TenantID) conf.Provider.Azure.SubscriptionID = iamFile.AzureOutput.SubscriptionID
cmd.Printf("location:\t%s\n", azureFlags.region) conf.Provider.Azure.TenantID = iamFile.AzureOutput.TenantID
cmd.Printf("resourceGroup:\t%s\n", azureFlags.resourceGroup) conf.Provider.Azure.Location = azureFlags.region
conf.Provider.Azure.ResourceGroup = azureFlags.resourceGroup
conf.Provider.Azure.UserAssignedIdentity = iamFile.AzureOutput.UAMIID
conf.Provider.Azure.AppClientID = iamFile.AzureOutput.ApplicationID
conf.Provider.Azure.ClientSecretValue = iamFile.AzureOutput.ApplicationClientSecretValue
if err := fileHandler.WriteYAML(azureFlags.configPath, conf, file.OptMkdirAll); err != nil {
return err
}
cmd.Printf("Your IAM configuration was created and filled into %s successfully.\n", azureFlags.configPath)
return nil
}
cmd.Printf("subscription:\t\t%s\n", iamFile.AzureOutput.SubscriptionID)
cmd.Printf("tenant:\t\t\t%s\n", iamFile.AzureOutput.TenantID)
cmd.Printf("location:\t\t%s\n", azureFlags.region)
cmd.Printf("resourceGroup:\t\t%s\n", azureFlags.resourceGroup)
cmd.Printf("userAssignedIdentity:\t%s\n", iamFile.AzureOutput.UAMIID) cmd.Printf("userAssignedIdentity:\t%s\n", iamFile.AzureOutput.UAMIID)
cmd.Printf("appClientID:\t%s\n", iamFile.AzureOutput.ApplicationID) cmd.Printf("appClientID:\t\t%s\n", iamFile.AzureOutput.ApplicationID)
cmd.Printf("appClientSecretValue:\t%s\n", iamFile.AzureOutput.ApplicationClientSecretValue) cmd.Printf("appClientSecretValue:\t%s\n\n", iamFile.AzureOutput.ApplicationClientSecretValue)
cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.") cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.")
return nil return nil
@ -97,22 +123,38 @@ func parseAzureFlags(cmd *cobra.Command) (azureFlags, error) {
if err != nil { if err != nil {
return azureFlags{}, fmt.Errorf("parsing region string: %w", err) return azureFlags{}, fmt.Errorf("parsing region string: %w", err)
} }
resourceGroup, err := cmd.Flags().GetString("resourceGroup") resourceGroup, err := cmd.Flags().GetString("resourceGroup")
if err != nil { if err != nil {
return azureFlags{}, fmt.Errorf("parsing resourceGroup string: %w", err) return azureFlags{}, fmt.Errorf("parsing resourceGroup string: %w", err)
} }
servicePrincipal, err := cmd.Flags().GetString("servicePrincipal") servicePrincipal, err := cmd.Flags().GetString("servicePrincipal")
if err != nil { if err != nil {
return azureFlags{}, fmt.Errorf("parsing servicePrincipal string: %w", err) return azureFlags{}, fmt.Errorf("parsing servicePrincipal string: %w", err)
} }
configPath, err := cmd.Flags().GetString("config")
if err != nil {
return azureFlags{}, fmt.Errorf("parsing config string: %w", err)
}
generateConfig, err := cmd.Flags().GetBool("generate-config")
if err != nil {
return azureFlags{}, fmt.Errorf("parsing generate-config bool: %w", err)
}
yesFlag, err := cmd.Flags().GetBool("yes") yesFlag, err := cmd.Flags().GetBool("yes")
if err != nil { if err != nil {
return azureFlags{}, fmt.Errorf("parsing yes bool: %w", err) return azureFlags{}, fmt.Errorf("parsing yes bool: %w", err)
} }
return azureFlags{ return azureFlags{
servicePrincipal: servicePrincipal, servicePrincipal: servicePrincipal,
resourceGroup: resourceGroup, resourceGroup: resourceGroup,
region: region, region: region,
generateConfig: generateConfig,
configPath: configPath,
yesFlag: yesFlag, yesFlag: yesFlag,
}, nil }, nil
} }
@ -122,5 +164,8 @@ type azureFlags struct {
region string region string
resourceGroup string resourceGroup string
servicePrincipal string servicePrincipal string
generateConfig bool
configPath string
yesFlag bool yesFlag bool
} }

View File

@ -20,10 +20,16 @@ import (
) )
func TestIAMCreateAzure(t *testing.T) { func TestIAMCreateAzure(t *testing.T) {
fsWithDefaultConfig := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs { defaultFs := func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
file := file.NewHandler(fs) fileHandler := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider))) for _, f := range existingFiles {
require.NoError(fileHandler.Write(f, []byte{1, 2, 3}, file.OptNone))
}
return fs
}
readOnlyFs := func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs {
fs := afero.NewReadOnlyFs(afero.NewMemMapFs())
return fs return fs
} }
validIAMIDFile := iamid.File{ validIAMIDFile := iamid.File{
@ -38,19 +44,22 @@ func TestIAMCreateAzure(t *testing.T) {
} }
testCases := map[string]struct { testCases := map[string]struct {
setupFs func(*require.Assertions, cloudprovider.Provider) afero.Fs setupFs func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs
creator *stubIAMCreator creator *stubIAMCreator
provider cloudprovider.Provider provider cloudprovider.Provider
regionFlag string regionFlag string
servicePrincipalFlag string servicePrincipalFlag string
resourceGroupFlag string resourceGroupFlag string
yesFlag bool yesFlag bool
generateConfigFlag bool
configFlag string
existingFiles []string
stdin string stdin string
wantAbort bool wantAbort bool
wantErr bool wantErr bool
}{ }{
"iam create azure": { "iam create azure": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure, provider: cloudprovider.Azure,
regionFlag: "westus", regionFlag: "westus",
@ -58,8 +67,56 @@ func TestIAMCreateAzure(t *testing.T) {
resourceGroupFlag: "constell-test-rg", resourceGroupFlag: "constell-test-rg",
yesFlag: true, yesFlag: true,
}, },
"iam create azure generate config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
generateConfigFlag: true,
configFlag: constants.ConfigFilename,
yesFlag: true,
},
"iam create azure generate config custom path": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
generateConfigFlag: true,
configFlag: "custom-config.yaml",
yesFlag: true,
},
"iam create azure generate config custom path already exists": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
generateConfigFlag: true,
yesFlag: true,
wantErr: true,
configFlag: "custom-config.yaml",
existingFiles: []string{"custom-config.yaml"},
},
"iam create generate config path already exists": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
generateConfigFlag: true,
configFlag: constants.ConfigFilename,
existingFiles: []string{constants.ConfigFilename},
yesFlag: true,
wantErr: true,
},
"interactive": { "interactive": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure, provider: cloudprovider.Azure,
regionFlag: "westus", regionFlag: "westus",
@ -67,8 +124,19 @@ func TestIAMCreateAzure(t *testing.T) {
resourceGroupFlag: "constell-test-rg", resourceGroupFlag: "constell-test-rg",
stdin: "yes\n", stdin: "yes\n",
}, },
"interactive generate config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
stdin: "yes\n",
generateConfigFlag: true,
configFlag: constants.ConfigFilename,
},
"interactive abort": { "interactive abort": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure, provider: cloudprovider.Azure,
regionFlag: "westus", regionFlag: "westus",
@ -77,6 +145,29 @@ func TestIAMCreateAzure(t *testing.T) {
stdin: "no\n", stdin: "no\n",
wantAbort: true, wantAbort: true,
}, },
"interactive generate config abort": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
stdin: "no\n",
generateConfigFlag: true,
wantAbort: true,
},
"unwritable fs": {
setupFs: readOnlyFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
yesFlag: true,
generateConfigFlag: true,
configFlag: constants.ConfigFilename,
wantErr: true,
},
} }
for name, tc := range testCases { for name, tc := range testCases {
@ -88,6 +179,10 @@ func TestIAMCreateAzure(t *testing.T) {
cmd.SetOut(&bytes.Buffer{}) cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin)) cmd.SetIn(bytes.NewBufferString(tc.stdin))
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("generate-config", false, "") // register persistent flag manually
if tc.regionFlag != "" { if tc.regionFlag != "" {
require.NoError(cmd.Flags().Set("region", tc.regionFlag)) require.NoError(cmd.Flags().Set("region", tc.regionFlag))
} }
@ -100,20 +195,42 @@ func TestIAMCreateAzure(t *testing.T) {
if tc.yesFlag { if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true")) require.NoError(cmd.Flags().Set("yes", "true"))
} }
if tc.generateConfigFlag {
require.NoError(cmd.Flags().Set("generate-config", "true"))
}
if tc.configFlag != "" {
require.NoError(cmd.Flags().Set("config", tc.configFlag))
}
err := iamCreateAzure(cmd, &nopSpinner{}, tc.creator) fileHandler := file.NewHandler(tc.setupFs(require, tc.provider, tc.existingFiles))
err := iamCreateAzure(cmd, &nopSpinner{}, tc.creator, fileHandler)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
} else { return
}
if tc.wantAbort { if tc.wantAbort {
assert.False(tc.creator.createCalled) assert.False(tc.creator.createCalled)
} else { return
assert.NoError(err) }
if tc.generateConfigFlag {
readConfig := &config.Config{}
readErr := fileHandler.ReadYAML(tc.configFlag, readConfig)
require.NoError(readErr)
assert.Equal(tc.creator.id.AzureOutput.SubscriptionID, readConfig.Provider.Azure.SubscriptionID)
assert.Equal(tc.creator.id.AzureOutput.TenantID, readConfig.Provider.Azure.TenantID)
assert.Equal(tc.creator.id.AzureOutput.ApplicationID, readConfig.Provider.Azure.AppClientID)
assert.Equal(tc.creator.id.AzureOutput.ApplicationClientSecretValue, readConfig.Provider.Azure.ClientSecretValue)
assert.Equal(tc.creator.id.AzureOutput.UAMIID, readConfig.Provider.Azure.UserAssignedIdentity)
assert.Equal(tc.regionFlag, readConfig.Provider.Azure.Location)
assert.Equal(tc.resourceGroupFlag, readConfig.Provider.Azure.ResourceGroup)
}
require.NoError(err)
assert.True(tc.creator.createCalled) assert.True(tc.creator.createCalled)
assert.Equal(tc.creator.id.AzureOutput, validIAMIDFile.AzureOutput) assert.Equal(tc.creator.id.AzureOutput, validIAMIDFile.AzureOutput)
}
}
}) })
} }
} }

View File

@ -43,7 +43,7 @@ func newIAMCreateGCPCmd() *cobra.Command {
must(cobra.MarkFlagRequired(cmd.Flags(), "serviceAccountID")) 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") 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")) must(cobra.MarkFlagRequired(cmd.Flags(), "projectID"))
cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation") cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation.")
return cmd return cmd
} }
@ -54,10 +54,10 @@ func runIAMCreateGCP(cmd *cobra.Command, args []string) error {
defer spinner.Stop() defer spinner.Stop()
creator := cloudcmd.NewIAMCreator(spinner) creator := cloudcmd.NewIAMCreator(spinner)
return iamCreateGCP(cmd, spinner, fileHandler, creator) return iamCreateGCP(cmd, spinner, creator, fileHandler)
} }
func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Handler, creator iamCreator) error { func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator, fileHandler file.Handler) error {
// Get input variables. // Get input variables.
gcpFlags, err := parseGCPFlags(cmd) gcpFlags, err := parseGCPFlags(cmd)
if err != nil { if err != nil {
@ -66,11 +66,14 @@ func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Ha
// Confirmation. // Confirmation.
if !gcpFlags.yesFlag { if !gcpFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n") cmd.Printf("The following IAM configuration will be created:\n\n")
cmd.Printf("Project ID:\t%s\n", gcpFlags.projectID) cmd.Printf("Project ID:\t\t%s\n", gcpFlags.projectID)
cmd.Printf("Service Account ID:\t%s\n", gcpFlags.serviceAccountID) cmd.Printf("Service Account ID:\t%s\n", gcpFlags.serviceAccountID)
cmd.Printf("Region:\t%s\n", gcpFlags.region) cmd.Printf("Region:\t\t\t%s\n", gcpFlags.region)
cmd.Printf("Zone:\t%s\n", gcpFlags.zone) 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?") ok, err := askToConfirm(cmd, "Do you want to create the configuration?")
if err != nil { if err != nil {
return err return err
@ -83,6 +86,9 @@ func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Ha
// Creation. // Creation.
spinner.Start("Creating", false) spinner.Start("Creating", false)
conf := createConfig(cloudprovider.GCP)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.GCP, &cloudcmd.IAMConfig{ iamFile, err := creator.Create(cmd.Context(), cloudprovider.GCP, &cloudcmd.IAMConfig{
GCP: cloudcmd.GCPIAMConfig{ GCP: cloudcmd.GCPIAMConfig{
ServiceAccountID: gcpFlags.serviceAccountID, ServiceAccountID: gcpFlags.serviceAccountID,
@ -91,10 +97,12 @@ func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Ha
ProjectID: gcpFlags.projectID, ProjectID: gcpFlags.projectID,
}, },
}) })
spinner.Stop() spinner.Stop()
if err != nil { if err != nil {
return err return err
} }
cmd.Println() // Print empty line to separate after spinner ended.
// Write back values. // Write back values.
tmpOut, err := parseIDFile(iamFile.GCPOutput.ServiceAccountKey) tmpOut, err := parseIDFile(iamFile.GCPOutput.ServiceAccountKey)
@ -106,7 +114,17 @@ func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Ha
return err return err
} }
cmd.Println(fmt.Sprintf("serviceAccountKeyPath:\t%s", constants.GCPServiceAccountKeyFile)) 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.") cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.")
return nil return nil
@ -158,7 +176,14 @@ func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
if !serviceAccIDRegex.MatchString(serviceAccID) { if !serviceAccIDRegex.MatchString(serviceAccID) {
return gcpFlags{}, fmt.Errorf("invalid serviceAccountID string: %s", 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") yesFlag, err := cmd.Flags().GetBool("yes")
if err != nil { if err != nil {
return gcpFlags{}, fmt.Errorf("parsing yes bool: %w", err) return gcpFlags{}, fmt.Errorf("parsing yes bool: %w", err)
@ -169,6 +194,8 @@ func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
zone: zone, zone: zone,
region: region, region: region,
projectID: projectID, projectID: projectID,
generateConfig: generateConfig,
configPath: configPath,
yesFlag: yesFlag, yesFlag: yesFlag,
}, nil }, nil
} }
@ -179,5 +206,7 @@ type gcpFlags struct {
zone string zone string
region string region string
projectID string projectID string
generateConfig bool
configPath string
yesFlag bool yesFlag bool
} }

View File

@ -20,10 +20,16 @@ import (
) )
func TestIAMCreateGCP(t *testing.T) { func TestIAMCreateGCP(t *testing.T) {
fsWithDefaultConfig := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs { defaultFs := func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs {
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
file := file.NewHandler(fs) fileHandler := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider))) for _, f := range existingFiles {
require.NoError(fileHandler.Write(f, []byte{1, 2, 3}, file.OptNone))
}
return fs
}
readOnlyFs := func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs {
fs := afero.NewReadOnlyFs(afero.NewMemMapFs())
return fs return fs
} }
validIAMIDFile := iamid.File{ validIAMIDFile := iamid.File{
@ -40,19 +46,22 @@ func TestIAMCreateGCP(t *testing.T) {
} }
testCases := map[string]struct { testCases := map[string]struct {
setupFs func(*require.Assertions, cloudprovider.Provider) afero.Fs setupFs func(require *require.Assertions, provider cloudprovider.Provider, existingFiles []string) afero.Fs
creator *stubIAMCreator creator *stubIAMCreator
provider cloudprovider.Provider provider cloudprovider.Provider
zoneFlag string zoneFlag string
serviceAccountIDFlag string serviceAccountIDFlag string
projectIDFlag string projectIDFlag string
yesFlag bool yesFlag bool
generateConfigFlag bool
configFlag string
existingFiles []string
stdin string stdin string
wantAbort bool wantAbort bool
wantErr bool wantErr bool
}{ }{
"iam create gcp": { "iam create gcp": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP, provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a", zoneFlag: "europe-west1-a",
@ -60,8 +69,56 @@ func TestIAMCreateGCP(t *testing.T) {
projectIDFlag: "constell-1234", projectIDFlag: "constell-1234",
yesFlag: true, yesFlag: true,
}, },
"iam create gcp generate config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
generateConfigFlag: true,
configFlag: constants.ConfigFilename,
yesFlag: true,
},
"iam create gcp generate config custom path": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
generateConfigFlag: true,
configFlag: "custom-config.yaml",
yesFlag: true,
},
"iam create gcp generate config path already exists": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
generateConfigFlag: true,
configFlag: constants.ConfigFilename,
existingFiles: []string{constants.ConfigFilename},
yesFlag: true,
wantErr: true,
},
"iam create gcp generate config custom path already exists": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
generateConfigFlag: true,
configFlag: "custom-config.yaml",
existingFiles: []string{"custom-config.yaml"},
yesFlag: true,
wantErr: true,
},
"iam create gcp invalid flags": { "iam create gcp invalid flags": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP, provider: cloudprovider.GCP,
zoneFlag: "-a", zoneFlag: "-a",
@ -69,7 +126,7 @@ func TestIAMCreateGCP(t *testing.T) {
wantErr: true, wantErr: true,
}, },
"iam create gcp invalid b64": { "iam create gcp invalid b64": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: invalidIAMIDFile}, creator: &stubIAMCreator{id: invalidIAMIDFile},
provider: cloudprovider.GCP, provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a", zoneFlag: "europe-west1-a",
@ -79,7 +136,7 @@ func TestIAMCreateGCP(t *testing.T) {
wantErr: true, wantErr: true,
}, },
"interactive": { "interactive": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP, provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a", zoneFlag: "europe-west1-a",
@ -87,8 +144,19 @@ func TestIAMCreateGCP(t *testing.T) {
projectIDFlag: "constell-1234", projectIDFlag: "constell-1234",
stdin: "yes\n", stdin: "yes\n",
}, },
"interactive generate config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
stdin: "yes\n",
configFlag: constants.ConfigFilename,
generateConfigFlag: true,
},
"interactive abort": { "interactive abort": {
setupFs: fsWithDefaultConfig, setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile}, creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP, provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a", zoneFlag: "europe-west1-a",
@ -97,6 +165,30 @@ func TestIAMCreateGCP(t *testing.T) {
stdin: "no\n", stdin: "no\n",
wantAbort: true, wantAbort: true,
}, },
"interactive abort generate config": {
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
stdin: "no\n",
wantAbort: true,
configFlag: constants.ConfigFilename,
generateConfigFlag: true,
},
"unwritable fs": {
setupFs: readOnlyFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
yesFlag: true,
generateConfigFlag: true,
configFlag: constants.ConfigFilename,
wantErr: true,
},
} }
for name, tc := range testCases { for name, tc := range testCases {
@ -108,6 +200,10 @@ func TestIAMCreateGCP(t *testing.T) {
cmd.SetOut(&bytes.Buffer{}) cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin)) cmd.SetIn(bytes.NewBufferString(tc.stdin))
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
cmd.Flags().Bool("generate-config", false, "") // register persistent flag manually
if tc.zoneFlag != "" { if tc.zoneFlag != "" {
require.NoError(cmd.Flags().Set("zone", tc.zoneFlag)) require.NoError(cmd.Flags().Set("zone", tc.zoneFlag))
} }
@ -120,22 +216,40 @@ func TestIAMCreateGCP(t *testing.T) {
if tc.yesFlag { if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true")) require.NoError(cmd.Flags().Set("yes", "true"))
} }
if tc.generateConfigFlag {
require.NoError(cmd.Flags().Set("generate-config", "true"))
}
if tc.configFlag != "" {
require.NoError(cmd.Flags().Set("config", tc.configFlag))
}
fileHandler := file.NewHandler(tc.setupFs(require, tc.provider)) fileHandler := file.NewHandler(tc.setupFs(require, tc.provider, tc.existingFiles))
err := iamCreateGCP(cmd, &nopSpinner{}, fileHandler, tc.creator) err := iamCreateGCP(cmd, &nopSpinner{}, tc.creator, fileHandler)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
} else { return
}
if tc.wantAbort { if tc.wantAbort {
assert.False(tc.creator.createCalled) assert.False(tc.creator.createCalled)
} else { return
assert.NoError(err) }
if tc.generateConfigFlag {
readConfig := &config.Config{}
readErr := fileHandler.ReadYAML(tc.configFlag, readConfig)
require.NoError(readErr)
assert.Equal(constants.GCPServiceAccountKeyFile, readConfig.Provider.GCP.ServiceAccountKeyPath)
}
require.NoError(err)
assert.True(tc.creator.createCalled) assert.True(tc.creator.createCalled)
assert.Equal(tc.creator.id.GCPOutput, validIAMIDFile.GCPOutput) assert.Equal(tc.creator.id.GCPOutput, validIAMIDFile.GCPOutput)
} readServiceAccountKey := &map[string]string{}
} readErr := fileHandler.ReadJSON(constants.GCPServiceAccountKeyFile, readServiceAccountKey)
require.NoError(readErr)
assert.Equal("not_a_secret", (*readServiceAccountKey)["private_key_id"])
}) })
} }
} }