From 5787df14dd201f450603879c166b07c2665539cd Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Mon, 16 Dec 2024 11:54:42 +0100 Subject: [PATCH 1/5] Add command to import config profiles --- docs/stackit_config_profile.md | 1 + docs/stackit_config_profile_import.md | 45 +++++++ internal/cmd/config/profile/import/import.go | 106 ++++++++++++++++ .../cmd/config/profile/import/import_test.go | 118 ++++++++++++++++++ .../profile/import/template/profile.json | 33 +++++ internal/cmd/config/profile/profile.go | 2 + internal/pkg/config/profiles.go | 64 ++++++++++ internal/pkg/config/profiles_test.go | 67 ++++++++++ .../pkg/config/template/test_profile.json | 33 +++++ internal/pkg/errors/errors.go | 13 ++ 10 files changed, 482 insertions(+) create mode 100644 docs/stackit_config_profile_import.md create mode 100644 internal/cmd/config/profile/import/import.go create mode 100644 internal/cmd/config/profile/import/import_test.go create mode 100644 internal/cmd/config/profile/import/template/profile.json create mode 100644 internal/pkg/config/template/test_profile.json diff --git a/docs/stackit_config_profile.md b/docs/stackit_config_profile.md index 947f3cc9..415ac9d0 100644 --- a/docs/stackit_config_profile.md +++ b/docs/stackit_config_profile.md @@ -34,6 +34,7 @@ stackit config profile [flags] * [stackit config](./stackit_config.md) - Provides functionality for CLI configuration options * [stackit config profile create](./stackit_config_profile_create.md) - Creates a CLI configuration profile * [stackit config profile delete](./stackit_config_profile_delete.md) - Delete a CLI configuration profile +* [stackit config profile import](./stackit_config_profile_import.md) - Imports a CLI configuration profile * [stackit config profile list](./stackit_config_profile_list.md) - Lists all CLI configuration profiles * [stackit config profile set](./stackit_config_profile_set.md) - Set a CLI configuration profile * [stackit config profile unset](./stackit_config_profile_unset.md) - Unset the current active CLI configuration profile diff --git a/docs/stackit_config_profile_import.md b/docs/stackit_config_profile_import.md new file mode 100644 index 00000000..6d62ef48 --- /dev/null +++ b/docs/stackit_config_profile_import.md @@ -0,0 +1,45 @@ +## stackit config profile import + +Imports a CLI configuration profile + +### Synopsis + +Imports a CLI configuration profile. + +``` +stackit config profile import [flags] +``` + +### Examples + +``` + Import a config with name "PROFILE_NAME" from file "./config.json" + $ stackit config profile --name PROFILE_NAME --config `@./config.json` + + Import a config with name "PROFILE_NAME" from file "./config.json" and set not as active + $ stackit config profile --name PROFILE_NAME --config `@./config.json` --no-set +``` + +### Options + +``` + -c, --config string Config to be imported + -h, --help Help for "stackit config profile import" + --name string Profile name + --no-set Set the imported profile not as active +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles + diff --git a/internal/cmd/config/profile/import/import.go b/internal/cmd/config/profile/import/import.go new file mode 100644 index 00000000..8c70705a --- /dev/null +++ b/internal/cmd/config/profile/import/import.go @@ -0,0 +1,106 @@ +package _import + +import ( + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + nameFlag = "name" + configFlag = "config" + noSetFlag = "no-set" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ProfileName string + Config string + NoSet bool +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "import", + Short: "Imports a CLI configuration profile", + Long: "Imports a CLI configuration profile.", + Example: examples.Build( + examples.NewExample( + `Import a config with name "PROFILE_NAME" from file "./config.json"`, + "$ stackit config profile --name PROFILE_NAME --config `@./config.json`", + ), + examples.NewExample( + `Import a config with name "PROFILE_NAME" from file "./config.json" and set not as active`, + "$ stackit config profile --name PROFILE_NAME --config `@./config.json` --no-set", + ), + ), + Args: args.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + err = config.ImportProfile(p, model.ProfileName, model.Config, !model.NoSet) + if err != nil { + return err + } + + p.Info("Successfully imported profile %q\n", model.ProfileName) + + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(nameFlag, "", "Profile name") + cmd.Flags().VarP(flags.ReadFromFileFlag(), configFlag, "c", "Config to be imported") + cmd.Flags().Bool(noSetFlag, false, "Set the imported profile not as active") + + cobra.CheckErr(cmd.MarkFlagRequired(nameFlag)) + cobra.CheckErr(cmd.MarkFlagRequired(configFlag)) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := &inputModel{ + GlobalFlagModel: globalFlags, + ProfileName: flags.FlagToStringValue(p, cmd, nameFlag), + Config: flags.FlagToStringValue(p, cmd, configFlag), + NoSet: flags.FlagToBoolValue(p, cmd, noSetFlag), + } + + if model.Config == "" { + return nil, &errors.FlagValidationError{ + Flag: configFlag, + Details: "must not be empty", + } + } + + if model.ProfileName == "" { + return nil, &errors.FlagValidationError{ + Flag: nameFlag, + Details: "must not be empty", + } + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return model, nil +} diff --git a/internal/cmd/config/profile/import/import_test.go b/internal/cmd/config/profile/import/import_test.go new file mode 100644 index 00000000..8da3ef61 --- /dev/null +++ b/internal/cmd/config/profile/import/import_test.go @@ -0,0 +1,118 @@ +package _import + +import ( + _ "embed" + "strconv" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" +) + +const testProfile = "test-profile" +const testConfig = "@./template/profile.json" +const testNoSet = false + +//go:embed template/profile.json +var testConfigContent string + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + nameFlag: testProfile, + configFlag: testConfig, + noSetFlag: strconv.FormatBool(testNoSet), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + ProfileName: testProfile, + Config: testConfigContent, + NoSet: testNoSet, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flags", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid path", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[configFlag] = "@./template/invalid-file" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err = cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + t.Fatalf("error parsing input: %v", err) + } + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/config/profile/import/template/profile.json b/internal/cmd/config/profile/import/template/profile.json new file mode 100644 index 00000000..ab56ce66 --- /dev/null +++ b/internal/cmd/config/profile/import/template/profile.json @@ -0,0 +1,33 @@ +{ + "allowed_url_domain": "stackit.cloud", + "async": false, + "authorization_custom_endpoint": "", + "dns_custom_endpoint": "", + "iaas_custom_endpoint": "", + "identity_provider_custom_client_id": "", + "identity_provider_custom_well_known_configuration": "", + "load_balancer_custom_endpoint": "", + "logme_custom_endpoint": "", + "mariadb_custom_endpoint": "", + "mongodbflex_custom_endpoint": "", + "object_storage_custom_endpoint": "", + "observability_custom_endpoint": "", + "opensearch_custom_endpoint": "", + "output_format": "", + "postgresflex_custom_endpoint": "", + "project_id": "", + "project_name": "", + "rabbitmq_custom_endpoint": "", + "redis_custom_endpoint": "", + "resource_manager_custom_endpoint": "", + "runcommand_custom_endpoint": "", + "secrets_manager_custom_endpoint": "", + "serverbackup_custom_endpoint": "", + "service_account_custom_endpoint": "", + "service_enablement_custom_endpoint": "", + "session_time_limit": "2h", + "ske_custom_endpoint": "", + "sqlserverflex_custom_endpoint": "", + "token_custom_endpoint": "", + "verbosity": "info" +} \ No newline at end of file diff --git a/internal/cmd/config/profile/profile.go b/internal/cmd/config/profile/profile.go index 3a4233ec..c24d3a92 100644 --- a/internal/cmd/config/profile/profile.go +++ b/internal/cmd/config/profile/profile.go @@ -5,6 +5,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/create" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/delete" + importProfile "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/import" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/list" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/set" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/unset" @@ -38,4 +39,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(create.NewCmd(p)) cmd.AddCommand(list.NewCmd(p)) cmd.AddCommand(delete.NewCmd(p)) + cmd.AddCommand(importProfile.NewCmd(p)) } diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index 69a1144c..9f4b4440 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -1,6 +1,7 @@ package config import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -330,3 +331,66 @@ func DeleteProfile(p *print.Printer, profile string) error { return nil } + +func ImportProfile(p *print.Printer, profileName, config string, setAsActive bool) error { + err := ValidateProfile(profileName) + if err != nil || profileName == DefaultProfileName { + return &errors.InvalidProfileNameError{Profile: profileName} + } + + exists, err := ProfileExists(profileName) + if err != nil { + return fmt.Errorf("check if profile exists: %w", err) + } + if exists { + return &errors.ProfileAlreadyExistsError{Profile: profileName} + } + + importConfig := &map[string]interface{}{} + err = json.Unmarshal([]byte(config), importConfig) + if err != nil { + return fmt.Errorf("unmarshal config: %w", err) + } + + configFolderPath = GetProfileFolderPath(profileName) + err = os.MkdirAll(configFolderPath, 0o750) + if err != nil { + return fmt.Errorf("create config folder: %w", err) + } + + content, err := json.MarshalIndent(importConfig, "", " ") + if err != nil { + cleanupErr := os.RemoveAll(configFolderPath) + if cleanupErr != nil { + return fmt.Errorf("json marshal config: %w, cleanup directories: %w", err, cleanupErr) + } + return fmt.Errorf("marshal config file: %w", err) + } + + filePath := getConfigFilePath(configFolderPath) + err = os.WriteFile(filePath, content, 0o600) + if err != nil { + cleanupErr := os.RemoveAll(configFolderPath) + if cleanupErr != nil { + return fmt.Errorf("write config file: %w, cleanup directories: %w", err, cleanupErr) + } + return fmt.Errorf("write config file: %w", err) + } + + if p.IsVerbosityDebug() { + p.Debug(print.DebugLevel, "profile %q imported", profileName) + } + + if setAsActive { + err := SetProfile(&print.Printer{}, profileName) + if err != nil { + return fmt.Errorf("set active profile: %w", err) + } + } + + if p.IsVerbosityDebug() { + p.Debug(print.DebugLevel, "active profile %q is now active", profileName) + } + + return nil +} diff --git a/internal/pkg/config/profiles_test.go b/internal/pkg/config/profiles_test.go index bb96918c..9700ea97 100644 --- a/internal/pkg/config/profiles_test.go +++ b/internal/pkg/config/profiles_test.go @@ -1,10 +1,17 @@ package config import ( + _ "embed" + "fmt" "path/filepath" "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) +//go:embed template/test_profile.json +var templateConfig string + func TestValidateProfile(t *testing.T) { tests := []struct { description string @@ -115,3 +122,63 @@ func TestGetProfileFolderPath(t *testing.T) { }) } } + +func TestImportProfile(t *testing.T) { + tests := []struct { + description string + profile string + config string + setAsActive bool + isValid bool + }{ + { + description: "valid profile", + profile: "profile-name", + config: templateConfig, + setAsActive: false, + isValid: true, + }, + { + description: "invalid profile name", + profile: "invalid-profile-&", + config: templateConfig, + setAsActive: false, + isValid: false, + }, + { + description: "invalid config", + profile: "my-profile", + config: `{ "invalid": "json }`, + setAsActive: false, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + err := ImportProfile(p, tt.profile, tt.config, tt.setAsActive) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("profile should be valid but got error: %v\n", err) + } + + if !tt.isValid { + t.Fatalf("profile should be invalid but got no error\n") + } + }) + + t.Cleanup(func() { + p := print.NewPrinter() + err := DeleteProfile(p, tt.profile) + if err != nil { + if !tt.isValid { + return + } + fmt.Printf("could not clean up imported profile: %v\n", err) + } + }) + } +} diff --git a/internal/pkg/config/template/test_profile.json b/internal/pkg/config/template/test_profile.json new file mode 100644 index 00000000..ab56ce66 --- /dev/null +++ b/internal/pkg/config/template/test_profile.json @@ -0,0 +1,33 @@ +{ + "allowed_url_domain": "stackit.cloud", + "async": false, + "authorization_custom_endpoint": "", + "dns_custom_endpoint": "", + "iaas_custom_endpoint": "", + "identity_provider_custom_client_id": "", + "identity_provider_custom_well_known_configuration": "", + "load_balancer_custom_endpoint": "", + "logme_custom_endpoint": "", + "mariadb_custom_endpoint": "", + "mongodbflex_custom_endpoint": "", + "object_storage_custom_endpoint": "", + "observability_custom_endpoint": "", + "opensearch_custom_endpoint": "", + "output_format": "", + "postgresflex_custom_endpoint": "", + "project_id": "", + "project_name": "", + "rabbitmq_custom_endpoint": "", + "redis_custom_endpoint": "", + "resource_manager_custom_endpoint": "", + "runcommand_custom_endpoint": "", + "secrets_manager_custom_endpoint": "", + "serverbackup_custom_endpoint": "", + "service_account_custom_endpoint": "", + "service_enablement_custom_endpoint": "", + "session_time_limit": "2h", + "ske_custom_endpoint": "", + "sqlserverflex_custom_endpoint": "", + "token_custom_endpoint": "", + "verbosity": "info" +} \ No newline at end of file diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 324abbad..7f7eb56c 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -150,6 +150,11 @@ To enable it, run: IAAS_SERVER_NIC_ATTACH_MISSING_NIC_ID = `The "network-interface-id" flag must be provided if the "create" flag is not provided.` IAAS_SERVER_NIC_DETACH_MISSING_NIC_ID = `The "network-interface-id" flag must be provided if the "delete" flag is not provided.` + + PROFILE_ALREADY_EXISTS = `profile %[1]q already exists. + +To delete it, run: + $ stackit config profile delete %[1]s` ) type ServerNicAttachMissingNicIdError struct { @@ -437,3 +442,11 @@ type ServiceDisabledError struct { func (e *ServiceDisabledError) Error() string { return fmt.Sprintf(SERVICE_DISABLED, e.Service) } + +type ProfileAlreadyExistsError struct { + Profile string +} + +func (e *ProfileAlreadyExistsError) Error() string { + return fmt.Sprintf(PROFILE_ALREADY_EXISTS, e.Profile) +} From 26117cd0b6a9368b0adf8f55c71f16473271938b Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Mon, 16 Dec 2024 13:49:11 +0100 Subject: [PATCH 2/5] Add comment to ImportProfile function --- internal/pkg/config/profiles.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index 9f4b4440..f7b6c5f7 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -332,6 +332,9 @@ func DeleteProfile(p *print.Printer, profile string) error { return nil } +// ImportProfile imports a profile configuration +// It imports the profile with the name profileName and a config json. +// If setAsActive is true, it set the new profile as the active profile. func ImportProfile(p *print.Printer, profileName, config string, setAsActive bool) error { err := ValidateProfile(profileName) if err != nil || profileName == DefaultProfileName { From 200d64b178deef6fd92009c5e910d54bfa5724db Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Mon, 16 Dec 2024 14:38:09 +0100 Subject: [PATCH 3/5] Refinements in descriptions of import command --- internal/cmd/config/profile/import/import.go | 10 +++++----- internal/cmd/config/profile/import/import_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/cmd/config/profile/import/import.go b/internal/cmd/config/profile/import/import.go index 8c70705a..d57cb192 100644 --- a/internal/cmd/config/profile/import/import.go +++ b/internal/cmd/config/profile/import/import.go @@ -1,4 +1,4 @@ -package _import +package importProfile import ( "github.com/spf13/cobra" @@ -32,11 +32,11 @@ func NewCmd(p *print.Printer) *cobra.Command { Example: examples.Build( examples.NewExample( `Import a config with name "PROFILE_NAME" from file "./config.json"`, - "$ stackit config profile --name PROFILE_NAME --config `@./config.json`", + "$ stackit config profile import --name PROFILE_NAME --config `@./config.json`", ), examples.NewExample( - `Import a config with name "PROFILE_NAME" from file "./config.json" and set not as active`, - "$ stackit config profile --name PROFILE_NAME --config `@./config.json` --no-set", + `Import a config with name "PROFILE_NAME" from file "./config.json" and do not set as active`, + "$ stackit config profile import --name PROFILE_NAME --config `@./config.json` --no-set", ), ), Args: args.NoArgs, @@ -62,7 +62,7 @@ func NewCmd(p *print.Printer) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(nameFlag, "", "Profile name") - cmd.Flags().VarP(flags.ReadFromFileFlag(), configFlag, "c", "Config to be imported") + cmd.Flags().VarP(flags.ReadFromFileFlag(), configFlag, "c", "File where configuration will be imported from") cmd.Flags().Bool(noSetFlag, false, "Set the imported profile not as active") cobra.CheckErr(cmd.MarkFlagRequired(nameFlag)) diff --git a/internal/cmd/config/profile/import/import_test.go b/internal/cmd/config/profile/import/import_test.go index 8da3ef61..7e028ab5 100644 --- a/internal/cmd/config/profile/import/import_test.go +++ b/internal/cmd/config/profile/import/import_test.go @@ -1,4 +1,4 @@ -package _import +package importProfile import ( _ "embed" From 950ec883cf74f0f039d28569fc7e85b445971ef9 Mon Sep 17 00:00:00 2001 From: Marcel <72880145+marceljk@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:52:55 +0100 Subject: [PATCH 4/5] Update errors.go Fix whitespace in PROFILE_ALREADY_EXISTS error message --- internal/pkg/errors/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 7f7eb56c..d61a8d9d 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -154,7 +154,7 @@ To enable it, run: PROFILE_ALREADY_EXISTS = `profile %[1]q already exists. To delete it, run: - $ stackit config profile delete %[1]s` + $ stackit config profile delete %[1]s` ) type ServerNicAttachMissingNicIdError struct { From f0afbafbddb78b9ccd4434ecbc3949b204e50832 Mon Sep 17 00:00:00 2001 From: Marcel Jacek Date: Mon, 16 Dec 2024 16:01:51 +0100 Subject: [PATCH 5/5] Update docs --- docs/stackit_config_profile_import.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/stackit_config_profile_import.md b/docs/stackit_config_profile_import.md index 6d62ef48..d2cbd126 100644 --- a/docs/stackit_config_profile_import.md +++ b/docs/stackit_config_profile_import.md @@ -14,16 +14,16 @@ stackit config profile import [flags] ``` Import a config with name "PROFILE_NAME" from file "./config.json" - $ stackit config profile --name PROFILE_NAME --config `@./config.json` + $ stackit config profile import --name PROFILE_NAME --config `@./config.json` - Import a config with name "PROFILE_NAME" from file "./config.json" and set not as active - $ stackit config profile --name PROFILE_NAME --config `@./config.json` --no-set + Import a config with name "PROFILE_NAME" from file "./config.json" and do not set as active + $ stackit config profile import --name PROFILE_NAME --config `@./config.json` --no-set ``` ### Options ``` - -c, --config string Config to be imported + -c, --config string File where configuration will be imported from -h, --help Help for "stackit config profile import" --name string Profile name --no-set Set the imported profile not as active