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 {
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 == "-" {
content, err := encoder.NewEncoder(conf).Encode()
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 {
return err
}
cmd.Println("Config file written to", flags.file)
cmd.Println("Please fill in your CSP-specific configuration before proceeding.")
cmd.Println("For more information refer to the documentation:")
@ -92,6 +88,19 @@ func (cg *configGenerateCmd) configGenerate(cmd *cobra.Command, fileHandler file
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) {
file, err := cmd.Flags().GetString("file")
if err != nil {

View File

@ -32,6 +32,8 @@ func newIAMCreateCmd() *cobra.Command {
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(newIAMCreateAzureCmd())
cmd.AddCommand(newIAMCreateGCPCmd())

View File

@ -11,6 +11,8 @@ import (
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
@ -28,7 +30,7 @@ func newIAMCreateAWSCmd() *cobra.Command {
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.")
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
}
@ -36,12 +38,13 @@ func newIAMCreateAWSCmd() *cobra.Command {
func runIAMCreateAWS(cmd *cobra.Command, args []string) error {
spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop()
fileHandler := file.NewHandler(afero.NewOsFs())
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.
awsFlags, err := parseAWSFlags(cmd)
if err != nil {
@ -50,9 +53,12 @@ func iamCreateAWS(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator)
// Confirmation.
if !awsFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n")
cmd.Printf("Region:\t%s\n", awsFlags.region)
cmd.Printf("Name Prefix:\t%s\n", awsFlags.prefix)
cmd.Printf("The following IAM configuration will be created:\n\n")
cmd.Printf("Region:\t\t%s\n", awsFlags.region)
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?")
if err != nil {
return err
@ -65,21 +71,39 @@ func iamCreateAWS(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator)
// Creation.
spinner.Start("Creating", false)
conf := createConfig(cloudprovider.AWS)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.AWS, &cloudcmd.IAMConfig{
AWS: cloudcmd.AWSIAMConfig{
Region: awsFlags.region,
Prefix: awsFlags.prefix,
},
})
spinner.Stop()
if err != nil {
return err
}
cmd.Println() // Print empty line to separate after spinner ended.
cmd.Printf("region:\t%s\n", awsFlags.region)
cmd.Printf("zone:\t%s\n", awsFlags.zone)
if awsFlags.generateConfig {
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("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.")
return nil
@ -106,7 +130,14 @@ func parseAWSFlags(cmd *cobra.Command) (awsFlags, error) {
} 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")
}
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")
if err != nil {
return awsFlags{}, fmt.Errorf("parsing yes bool: %w", err)
@ -115,6 +146,8 @@ func parseAWSFlags(cmd *cobra.Command) (awsFlags, error) {
zone: zone,
prefix: prefix,
region: region,
generateConfig: generateConfig,
configPath: configPath,
yesFlag: yesFlag,
}, nil
}
@ -124,5 +157,7 @@ type awsFlags struct {
prefix string
region string
zone string
generateConfig bool
configPath string
yesFlag bool
}

View File

@ -7,6 +7,7 @@ package cmd
import (
"bytes"
"strings"
"testing"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
@ -20,10 +21,16 @@ import (
)
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()
file := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider)))
fileHandler := file.NewHandler(fs)
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
}
validIAMIDFile := iamid.File{
@ -35,34 +42,91 @@ func TestIAMCreateAWS(t *testing.T) {
}
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
provider cloudprovider.Provider
zoneFlag string
prefixFlag string
yesFlag bool
generateConfigFlag bool
configFlag string
existingFiles []string
stdin string
wantAbort bool
wantErr bool
}{
"iam create aws": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
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": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
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": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
@ -70,8 +134,19 @@ func TestIAMCreateAWS(t *testing.T) {
stdin: "no\n",
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": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-west-5b",
@ -79,6 +154,17 @@ func TestIAMCreateAWS(t *testing.T) {
yesFlag: 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 {
@ -90,6 +176,10 @@ func TestIAMCreateAWS(t *testing.T) {
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
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 != "" {
require.NoError(cmd.Flags().Set("zone", tc.zoneFlag))
}
@ -99,20 +189,39 @@ func TestIAMCreateAWS(t *testing.T) {
if tc.yesFlag {
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 {
assert.Error(err)
} else {
return
}
if tc.wantAbort {
assert.False(tc.creator.createCalled)
} else {
assert.NoError(err)
return
}
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.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/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
@ -23,13 +25,13 @@ func newIAMCreateAzureCmd() *cobra.Command {
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"))
cmd.Flags().String("region", "", "Region the resources will be created in. (e.g. westus)")
must(cobra.MarkFlagRequired(cmd.Flags(), "region"))
cmd.Flags().String("servicePrincipal", "", "Name of the service principal that will be created.")
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
}
@ -37,12 +39,13 @@ func newIAMCreateAzureCmd() *cobra.Command {
func runIAMCreateAzure(cmd *cobra.Command, args []string) error {
spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop()
fileHandler := file.NewHandler(afero.NewOsFs())
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.
azureFlags, err := parseAzureFlags(cmd)
if err != nil {
@ -51,10 +54,13 @@ func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreato
// Confirmation.
if !azureFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n")
cmd.Printf("Region:\t%s\n", azureFlags.region)
cmd.Printf("Resource Group:\t%s\n", azureFlags.resourceGroup)
cmd.Printf("Service Principal:\t%s\n", azureFlags.servicePrincipal)
cmd.Printf("The following IAM configuration will be created:\n\n")
cmd.Printf("Region:\t\t\t%s\n", azureFlags.region)
cmd.Printf("Resource Group:\t\t%s\n", azureFlags.resourceGroup)
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?")
if err != nil {
return err
@ -67,6 +73,9 @@ func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreato
// Creation.
spinner.Start("Creating", false)
conf := createConfig(cloudprovider.Azure)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.Azure, &cloudcmd.IAMConfig{
Azure: cloudcmd.AzureIAMConfig{
Region: azureFlags.region,
@ -74,18 +83,35 @@ func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreato
ResourceGroup: azureFlags.resourceGroup,
},
})
spinner.Stop()
if err != nil {
return err
}
cmd.Println() // Print empty line to separate after spinner ended.
cmd.Printf("subscription:\t%s\n", iamFile.AzureOutput.SubscriptionID)
cmd.Printf("tenant:\t%s\n", iamFile.AzureOutput.TenantID)
cmd.Printf("location:\t%s\n", azureFlags.region)
cmd.Printf("resourceGroup:\t%s\n", azureFlags.resourceGroup)
if azureFlags.generateConfig {
conf.Provider.Azure.SubscriptionID = iamFile.AzureOutput.SubscriptionID
conf.Provider.Azure.TenantID = iamFile.AzureOutput.TenantID
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("appClientID:\t%s\n", iamFile.AzureOutput.ApplicationID)
cmd.Printf("appClientSecretValue:\t%s\n", iamFile.AzureOutput.ApplicationClientSecretValue)
cmd.Printf("appClientID:\t\t%s\n", iamFile.AzureOutput.ApplicationID)
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.")
return nil
@ -97,22 +123,38 @@ func parseAzureFlags(cmd *cobra.Command) (azureFlags, error) {
if err != nil {
return azureFlags{}, fmt.Errorf("parsing region string: %w", err)
}
resourceGroup, err := cmd.Flags().GetString("resourceGroup")
if err != nil {
return azureFlags{}, fmt.Errorf("parsing resourceGroup string: %w", err)
}
servicePrincipal, err := cmd.Flags().GetString("servicePrincipal")
if err != nil {
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")
if err != nil {
return azureFlags{}, fmt.Errorf("parsing yes bool: %w", err)
}
return azureFlags{
servicePrincipal: servicePrincipal,
resourceGroup: resourceGroup,
region: region,
generateConfig: generateConfig,
configPath: configPath,
yesFlag: yesFlag,
}, nil
}
@ -122,5 +164,8 @@ type azureFlags struct {
region string
resourceGroup string
servicePrincipal string
generateConfig bool
configPath string
yesFlag bool
}

View File

@ -20,10 +20,16 @@ import (
)
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()
file := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider)))
fileHandler := file.NewHandler(fs)
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
}
validIAMIDFile := iamid.File{
@ -38,19 +44,22 @@ func TestIAMCreateAzure(t *testing.T) {
}
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
provider cloudprovider.Provider
regionFlag string
servicePrincipalFlag string
resourceGroupFlag string
yesFlag bool
generateConfigFlag bool
configFlag string
existingFiles []string
stdin string
wantAbort bool
wantErr bool
}{
"iam create azure": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
@ -58,8 +67,56 @@ func TestIAMCreateAzure(t *testing.T) {
resourceGroupFlag: "constell-test-rg",
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": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
@ -67,8 +124,19 @@ func TestIAMCreateAzure(t *testing.T) {
resourceGroupFlag: "constell-test-rg",
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": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
@ -77,6 +145,29 @@ func TestIAMCreateAzure(t *testing.T) {
stdin: "no\n",
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 {
@ -88,6 +179,10 @@ func TestIAMCreateAzure(t *testing.T) {
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
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 != "" {
require.NoError(cmd.Flags().Set("region", tc.regionFlag))
}
@ -100,20 +195,42 @@ func TestIAMCreateAzure(t *testing.T) {
if tc.yesFlag {
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 {
assert.Error(err)
} else {
return
}
if tc.wantAbort {
assert.False(tc.creator.createCalled)
} else {
assert.NoError(err)
return
}
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.Equal(tc.creator.id.AzureOutput, validIAMIDFile.AzureOutput)
}
}
})
}
}

View File

@ -43,7 +43,7 @@ func newIAMCreateGCPCmd() *cobra.Command {
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")
cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation.")
return cmd
}
@ -54,10 +54,10 @@ func runIAMCreateGCP(cmd *cobra.Command, args []string) error {
defer spinner.Stop()
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.
gcpFlags, err := parseGCPFlags(cmd)
if err != nil {
@ -66,11 +66,14 @@ func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Ha
// 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("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%s\n", gcpFlags.region)
cmd.Printf("Zone:\t%s\n", gcpFlags.zone)
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
@ -83,6 +86,9 @@ func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Ha
// 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,
@ -91,10 +97,12 @@ func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Ha
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)
@ -106,7 +114,17 @@ func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Ha
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.")
return nil
@ -158,7 +176,14 @@ func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
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)
@ -169,6 +194,8 @@ func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
zone: zone,
region: region,
projectID: projectID,
generateConfig: generateConfig,
configPath: configPath,
yesFlag: yesFlag,
}, nil
}
@ -179,5 +206,7 @@ type gcpFlags struct {
zone string
region string
projectID string
generateConfig bool
configPath string
yesFlag bool
}

View File

@ -20,10 +20,16 @@ import (
)
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()
file := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider)))
fileHandler := file.NewHandler(fs)
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
}
validIAMIDFile := iamid.File{
@ -40,19 +46,22 @@ func TestIAMCreateGCP(t *testing.T) {
}
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
provider cloudprovider.Provider
zoneFlag string
serviceAccountIDFlag string
projectIDFlag string
yesFlag bool
generateConfigFlag bool
configFlag string
existingFiles []string
stdin string
wantAbort bool
wantErr bool
}{
"iam create gcp": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
@ -60,8 +69,56 @@ func TestIAMCreateGCP(t *testing.T) {
projectIDFlag: "constell-1234",
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": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "-a",
@ -69,7 +126,7 @@ func TestIAMCreateGCP(t *testing.T) {
wantErr: true,
},
"iam create gcp invalid b64": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: invalidIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
@ -79,7 +136,7 @@ func TestIAMCreateGCP(t *testing.T) {
wantErr: true,
},
"interactive": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
@ -87,8 +144,19 @@ func TestIAMCreateGCP(t *testing.T) {
projectIDFlag: "constell-1234",
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": {
setupFs: fsWithDefaultConfig,
setupFs: defaultFs,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
@ -97,6 +165,30 @@ func TestIAMCreateGCP(t *testing.T) {
stdin: "no\n",
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 {
@ -108,6 +200,10 @@ func TestIAMCreateGCP(t *testing.T) {
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
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 != "" {
require.NoError(cmd.Flags().Set("zone", tc.zoneFlag))
}
@ -120,22 +216,40 @@ func TestIAMCreateGCP(t *testing.T) {
if tc.yesFlag {
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 {
assert.Error(err)
} else {
return
}
if tc.wantAbort {
assert.False(tc.creator.createCalled)
} else {
assert.NoError(err)
return
}
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.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"])
})
}
}