Skip to content
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
1 change: 1 addition & 0 deletions docs/stackit_config_profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 export](./stackit_config_profile_export.md) - Exports 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
Expand Down
43 changes: 43 additions & 0 deletions docs/stackit_config_profile_export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## stackit config profile export

Exports a CLI configuration profile

### Synopsis

Exports a CLI configuration profile.

```
stackit config profile export PROFILE_NAME [flags]
```

### Examples

```
Export a profile with name "PROFILE_NAME" to a file in your current directory
$ stackit config profile export PROFILE_NAME

Export a profile with name "PROFILE_NAME"" to a specific file path FILE_PATH
$ stackit config profile export PROFILE_NAME --file-path FILE_PATH
```

### Options

```
-f, --file-path string If set, writes the config to the given file path. If unset, writes the config to you current directory with the name of the profile. E.g. '--file-path ~/my-config.json'
-h, --help Help for "stackit config profile export"
```

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

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

import (
"fmt"
"path/filepath"

"github.com/stackitcloud/stackit-cli/internal/pkg/args"
"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 (
profileNameArg = "PROFILE_NAME"

filePathFlag = "file-path"

configFileExtension = "json"
)

type inputModel struct {
*globalflags.GlobalFlagModel
ProfileName string
FilePath string
}

func NewCmd(p *print.Printer) *cobra.Command {
cmd := &cobra.Command{
Use: fmt.Sprintf("export %s", profileNameArg),
Short: "Exports a CLI configuration profile",
Long: "Exports a CLI configuration profile.",
Example: examples.Build(
examples.NewExample(
`Export a profile with name "PROFILE_NAME" to a file in your current directory`,
"$ stackit config profile export PROFILE_NAME",
),
examples.NewExample(
`Export a profile with name "PROFILE_NAME"" to a specific file path FILE_PATH`,
"$ stackit config profile export PROFILE_NAME --file-path FILE_PATH",
),
),
Args: args.SingleArg(profileNameArg, nil),
RunE: func(cmd *cobra.Command, args []string) error {
model, err := parseInput(p, cmd, args)
if err != nil {
return err
}

err = config.ExportProfile(p, model.ProfileName, model.FilePath)
if err != nil {
return fmt.Errorf("could not export profile: %w", err)
}

p.Info("Exported profile %q to %q\n", model.ProfileName, model.FilePath)

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

func configureFlags(cmd *cobra.Command) {
cmd.Flags().StringP(filePathFlag, "f", "", "If set, writes the config to the given file path. If unset, writes the config to you current directory with the name of the profile. E.g. '--file-path ~/my-config.json'")
}

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

model := inputModel{
GlobalFlagModel: globalFlags,
ProfileName: profileName,
FilePath: flags.FlagToStringValue(p, cmd, filePathFlag),
}

// If filePath contains does not contain a file name, then add a default name
if model.FilePath == "" {
exportFileName := fmt.Sprintf("%s.%s", model.ProfileName, configFileExtension)
model.FilePath = filepath.Join(model.FilePath, exportFileName)
}

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

import (
"fmt"
"testing"

"github.com/stackitcloud/stackit-cli/internal/pkg/globalflags"
"github.com/stackitcloud/stackit-cli/internal/pkg/print"

"github.com/google/go-cmp/cmp"
)

const (
testProfileArg = "default"
testExportPath = "/tmp/stackit-profiles/" + testProfileArg + ".json"
)

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

func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string {
flagValues := map[string]string{
filePathFlag: testExportPath,
}
for _, mod := range mods {
mod(flagValues)
}
return flagValues
}

func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel {
model := &inputModel{
GlobalFlagModel: &globalflags.GlobalFlagModel{
Verbosity: globalflags.VerbosityDefault,
},
ProfileName: testProfileArg,
FilePath: testExportPath,
}
for _, mod := range mods {
mod(model)
}
return model
}

func TestParseInput(t *testing.T) {
tests := []struct {
description string
argsValues []string
flagValues map[string]string
isValid bool
expectedModel *inputModel
}{
{
description: "base",
argsValues: fixtureArgValues(),
flagValues: fixtureFlagValues(),
isValid: true,
expectedModel: fixtureInputModel(),
},
{
description: "no values",
argsValues: []string{},
flagValues: map[string]string{},
isValid: false,
},
{
description: "no args",
argsValues: []string{},
flagValues: fixtureFlagValues(),
isValid: false,
},
{
description: "no flags",
argsValues: fixtureArgValues(),
flagValues: map[string]string{},
isValid: true,
expectedModel: fixtureInputModel(func(inputModel *inputModel) {
inputModel.FilePath = fmt.Sprintf("%s.json", testProfileArg)
}),
},
{
description: "custom file-path without file extension",
argsValues: fixtureArgValues(),
flagValues: fixtureFlagValues(
func(flagValues map[string]string) {
flagValues[filePathFlag] = "./my-exported-config"
}),
isValid: true,
expectedModel: fixtureInputModel(func(inputModel *inputModel) {
inputModel.FilePath = "./my-exported-config"
}),
},
}

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.argsValues)
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.argsValues)
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 @@ -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"
"github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/export"
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"
Expand Down Expand Up @@ -40,4 +41,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) {
cmd.AddCommand(list.NewCmd(p))
cmd.AddCommand(delete.NewCmd(p))
cmd.AddCommand(importProfile.NewCmd(p))
cmd.AddCommand(export.NewCmd(p))
}
6 changes: 5 additions & 1 deletion internal/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ var configFolderPath string
var profileFilePath string

func InitConfig() {
defaultConfigFolderPath = getInitialConfigDir()
initConfig(getInitialConfigDir())
}

func initConfig(configPath string) {
defaultConfigFolderPath = configPath
profileFilePath = getInitialProfileFilePath() // Profile file path is in the default config folder

configProfile, err := GetProfile()
Expand Down
39 changes: 39 additions & 0 deletions internal/pkg/config/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,42 @@ func ImportProfile(p *print.Printer, profileName, config string, setAsActive boo

return nil
}

// ExportProfile exports a profile configuration
// Is exports the profile to the exportPath. The exportPath must contain the filename.
func ExportProfile(p *print.Printer, profile, exportPath string) error {
err := ValidateProfile(profile)
if err != nil {
return fmt.Errorf("validate profile name: %w", err)
}

exists, err := ProfileExists(profile)
if err != nil {
return fmt.Errorf("check if profile exists: %w", err)
}
if !exists {
return &errors.ProfileDoesNotExistError{Profile: profile}
}

profilePath := GetProfileFolderPath(profile)
configFile := getConfigFilePath(profilePath)

stats, err := os.Stat(exportPath)
if err == nil {
if stats.IsDir() {
return fmt.Errorf("export path %q is a directory. Please specify a full path", exportPath)
}
return &errors.FileAlreadyExistsError{Filename: exportPath}
}

err = fileutils.CopyFile(configFile, exportPath)
if err != nil {
return fmt.Errorf("export config file to %q: %w", exportPath, err)
}

if p != nil {
p.Debug(print.DebugLevel, "exported profile %q to %q", profile, exportPath)
}

return nil
}
Loading
Loading