Skip to content

Add stackit profile create command and refactor set command #351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/stackit_config_profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ stackit config profile [flags]
```
-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"]
-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](./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 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

48 changes: 48 additions & 0 deletions docs/stackit_config_profile_create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## stackit config profile create

Creates a CLI configuration profile

### Synopsis

Creates a CLI configuration profile based on the currently active profile and sets it as active.
The profile name can be provided via the STACKIT_CLI_PROFILE environment variable or as an argument in this command.
The environment variable takes precedence over the argument.
If you do not want to set the profile as active, use the --no-set flag.
If you want to create the new profile with the initial default configurations, use the --empty flag.

```
stackit config profile create PROFILE [flags]
```

### Examples

```
Create a new configuration profile "my-profile" with the current configuration, setting it as the active profile
$ stackit config profile create my-profile

Create a new configuration profile "my-profile" with a default initial configuration and don't set it as the active profile
$ stackit config profile create my-profile --empty --no-set
```

### Options

```
--empty Create the profile with the initial default configurations
-h, --help Help for "stackit config profile create"
--no-set Do not set the profile as the active profile
```

### 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

3 changes: 1 addition & 2 deletions docs/stackit_config_profile_set.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Set a CLI configuration profile
Set a CLI configuration profile as the active profile.
The profile to be used can be managed via the STACKIT_CLI_PROFILE environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands.
The environment variable takes precedence over what is set via the commands.
A new profile is created automatically if it does not exist.
When no profile is set, the default profile is used.

```
Expand All @@ -32,7 +31,7 @@ stackit config profile set PROFILE [flags]
```
-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"]
-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")
```
Expand Down
2 changes: 1 addition & 1 deletion docs/stackit_config_profile_unset.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ stackit config profile unset [flags]
```
-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"]
-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")
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ stackit load-balancer observability-credentials cleanup [flags]

### SEE ALSO

- [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials
* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials

116 changes: 116 additions & 0 deletions internal/cmd/config/profile/create/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package create

import (
"fmt"

"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"github.com/stackitcloud/stackit-cli/internal/pkg/auth"
"github.com/stackitcloud/stackit-cli/internal/pkg/config"
"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"

"github.com/spf13/cobra"
)

const (
profileArg = "PROFILE"

noSetFlag = "no-set"
fromEmptyProfile = "empty"
)

type inputModel struct {
*globalflags.GlobalFlagModel
NoSet bool
FromEmptyProfile bool
Profile string
}

func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("create %s", profileArg),
Short: "Creates a CLI configuration profile",
Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s",
"Creates a CLI configuration profile based on the currently active profile and sets it as active.",
`The profile name can be provided via the STACKIT_CLI_PROFILE environment variable or as an argument in this command.`,
"The environment variable takes precedence over the argument.",
"If you do not want to set the profile as active, use the --no-set flag.",
"If you want to create the new profile with the initial default configurations, use the --empty flag.",
),
Args: args.SingleArg(profileArg, nil),
Example: examples.Build(
examples.NewExample(
`Create a new configuration profile "my-profile" with the current configuration, setting it as the active profile`,
"$ stackit config profile create my-profile"),
examples.NewExample(
`Create a new configuration profile "my-profile" with a default initial configuration and don't set it as the active profile`,
"$ stackit config profile create my-profile --empty --no-set"),
),
RunE: func(cmd *cobra.Command, args []string) error {
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}

err = config.CreateProfile(p, model.Profile, !model.NoSet, model.FromEmptyProfile)
if err != nil {
return fmt.Errorf("create profile: %w", err)
}

if model.NoSet {
p.Info("Successfully created profile %q\n", model.Profile)
return nil
}

p.Info("Successfully created and set active profile to %q\n", model.Profile)

flow, err := auth.GetAuthFlow()
if err != nil {
p.Debug(print.WarningLevel, "both keyring and text file storage failed to find a valid authentication flow for the active profile")
p.Warn("The active profile %q is not authenticated, please login using the 'stackit auth login' command.\n", model.Profile)
return nil
}
p.Debug(print.DebugLevel, "found valid authentication flow for active profile: %s", flow)

return nil
},
}
configureFlags(cmd)
return cmd
}

func configureFlags(cmd *cobra.Command) {
cmd.Flags().Bool(noSetFlag, false, "Do not set the profile as the active profile")
cmd.Flags().Bool(fromEmptyProfile, false, "Create the profile with the initial default configurations")
}

func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) {
profile := inputArgs[0]

err := config.ValidateProfile(profile)
if err != nil {
return nil, err
}

globalFlags := globalflags.Parse(p, cmd)

model := inputModel{
GlobalFlagModel: globalFlags,
Profile: profile,
FromEmptyProfile: flags.FlagToBoolValue(p, cmd, fromEmptyProfile),
NoSet: flags.FlagToBoolValue(p, cmd, noSetFlag),
}

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
}
156 changes: 156 additions & 0 deletions internal/cmd/config/profile/create/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package create

import (
"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"

func fixtureArgValues(mods ...func(argValues []string)) []string {
argValues := []string{
testProfile,
}
for _, mod := range mods {
mod(argValues)
}
return argValues
}

func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
Profile: testProfile,
FromEmptyProfile: false,
NoSet: false,
}
for _, mod := range mods {
mod(model)
}
return model
}

func TestParseInput(t *testing.T) {
tests := []struct {
description string
argValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argValues: fixtureArgValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no arg values",
argValues: []string{},
isValid: false,
},
{
description: "some global flag",
argValues: fixtureArgValues(),
flagValues: map[string]string{
globalflags.VerbosityFlag: globalflags.DebugVerbosity,
},
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.GlobalFlagModel.Verbosity = globalflags.DebugVerbosity
}),
},
{
description: "invalid profile",
argValues: []string{"invalid-profile-&"},
isValid: false,
},
{
description: "use default given",
argValues: fixtureArgValues(),
flagValues: map[string]string{
fromEmptyProfile: "true",
},
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.FromEmptyProfile = true
}),
},
{
description: "no set given",
argValues: fixtureArgValues(),
flagValues: map[string]string{
noSetFlag: "true",
},
isValid: true,
expectedModel: fixtureInputModel(func(model *inputModel) {
model.NoSet = true
}),
},
}

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.ValidateArgs(tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating args: %v", err)
}

err = cmd.ValidateRequiredFlags()
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error validating flags: %v", err)
}

model, err := parseInput(p, cmd, tt.argValues)
if err != nil {
if !tt.isValid {
return
}
t.Fatalf("error parsing input: %v", err)
}

if !tt.isValid {
t.Fatalf("did not fail on invalid input")
}
diff := cmp.Diff(model, tt.expectedModel)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}
2 changes: 2 additions & 0 deletions internal/cmd/config/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package profile
import (
"fmt"

"github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/create"
"github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/set"
"github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/unset"
"github.com/stackitcloud/stackit-cli/internal/pkg/args"
Expand Down Expand Up @@ -32,4 +33,5 @@ func NewCmd(p *print.Printer) *cobra.Command {
func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(set.NewCmd(p))
cmd.AddCommand(unset.NewCmd(p))
cmd.AddCommand(create.NewCmd(p))
}
Loading