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
32 changes: 10 additions & 22 deletions internal/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,20 @@ const (
ServiceAccountCustomEndpointKey = "service_account_custom_endpoint"
SKECustomEndpointKey = "ske_custom_endpoint"

ProjectNameKey = "project_name"

AsyncDefault = false
SessionTimeLimitDefault = "2h"
)

// Backend config keys
const (
configFolder = "stackit"
configFolder = "stackit"

configFileName = "cli-config"
configFileExtension = "json"
ProjectNameKey = "project_name"

profileFileName = "cli-profile"
profileFileExtension = "txt"
)

var ConfigKeys = []string{
Expand Down Expand Up @@ -72,17 +76,14 @@ var ConfigKeys = []string{
SKECustomEndpointKey,
}

var folderPath string
var configFolderPath string

func InitConfig() {
configDir, err := os.UserConfigDir()
cobra.CheckErr(err)
configFolderPath := filepath.Join(configDir, configFolder)
configFolderPath = filepath.Join(configDir, configFolder)
configFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension))

// Write config dir path to global variable
folderPath = configFolderPath

// This hack is required to allow creating the config file with `viper.WriteConfig`
// see https://github.com/spf13/viper/issues/851#issuecomment-789393451
viper.SetConfigFile(configFilePath)
Expand All @@ -107,22 +108,9 @@ func InitConfig() {
viper.SetEnvPrefix("stackit")
}

func createFolderIfNotExists() error {
_, err := os.Stat(folderPath)
if os.IsNotExist(err) {
err := os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
return err
}
} else if err != nil {
return err
}
return nil
}

// Write saves the config file (wrapping `viper.WriteConfig`) and ensures that its directory exists
func Write() error {
if err := createFolderIfNotExists(); err != nil {
if err := createFolderIfNotExists(configFolderPath); err != nil {
return fmt.Errorf("create config directory: %w", err)
}
return viper.WriteConfig()
Expand Down
40 changes: 40 additions & 0 deletions internal/pkg/config/file_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package config

import (
"fmt"
"os"
)

// createFolderIfNotExists creates a folder if it does not exist.
func createFolderIfNotExists(folderPath string) error {
_, err := os.Stat(folderPath)
if os.IsNotExist(err) {
err := os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
return err
}
} else if err != nil {
return err
}
return nil
}

// readFileIfExists reads the contents of a file and returns it as a string, along with a boolean indicating if the file exists.
// If the file does not exist, it returns an empty string and no error.
// If the file exists but cannot be read, it returns an error.
func readFileIfExists(filePath string) (contents string, exists bool, err error) {
_, err = os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
return "", false, nil
}
return "", true, err
}

content, err := os.ReadFile(filePath)
if err != nil {
return "", true, fmt.Errorf("read file: %w", err)
}

return string(content), true, nil
}
89 changes: 89 additions & 0 deletions internal/pkg/config/file_utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package config

import (
"os"
"testing"
)

func TestCreateFolderIfNotExists(t *testing.T) {
tests := []struct {
description string
folderPath string
needsCleanUp bool
}{
{
description: "folder exists",
folderPath: "test-data/folder-exists",
},
{
description: "folder does not exist",
folderPath: "test-data/folder-does-not-exist",
needsCleanUp: true,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := createFolderIfNotExists(tt.folderPath)
if err != nil {
t.Errorf("create folder: %v", err)
}

// Check if the folder was created
_, err = os.Stat(tt.folderPath)
if os.IsNotExist(err) {
t.Errorf("expected folder to exist but it does not")
}

// Clean up
if tt.needsCleanUp {
err = os.RemoveAll(tt.folderPath)
if err != nil {
t.Errorf("remove folder: %v", err)
}
}
})
}
}

func TestReadFileIfExists(t *testing.T) {
tests := []struct {
description string
filePath string
exists bool
content string
}{
{
description: "file exists",
filePath: "test-data/file-with-content.txt",
exists: true,
content: "my-content",
},
{
description: "file does not exist",
filePath: "test-data/file-does-not-exist.txt",
content: "",
},
{
description: "empty file",
filePath: "test-data/empty-file.txt",
exists: true,
content: "",
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
content, exists, err := readFileIfExists(tt.filePath)
if err != nil {
t.Errorf("read file: %v", err)
}
if exists != tt.exists {
t.Errorf("expected exists to be %t but got %t", tt.exists, exists)
}
if content != tt.content {
t.Errorf("expected content to be %q but got %q", tt.content, content)
}
})
}
}
51 changes: 51 additions & 0 deletions internal/pkg/config/profiles.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package config

import (
"fmt"
"os"
"path/filepath"
"regexp"
)

// GetProfile returns the current profile to be used by the CLI.
//
// The profile is determined by the value of the STACKIT_CLI_PROFILE environment variable, or, if not set,
// by the contents of the profile file in the CLI config folder.
//
// If the environment variable is not set and the profile file does not exist, it returns an empty string.
//
// If the profile is not valid, it returns an error.
func GetProfile() (string, error) {
profile, profileSet := os.LookupEnv("STACKIT_CLI_PROFILE")
if !profileSet {
profileFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension))
contents, exists, err := readFileIfExists(profileFilePath)
if err != nil {
return "", fmt.Errorf("read profile from file: %w", err)
}
if !exists {
return "", nil
}
profile = contents
}

err := validateProfile(profile)
if err != nil {
return "", fmt.Errorf("validate profile from env var: %w", err)
}
return profile, nil
}

// validateProfile validates the profile name.
// It can only use letters, numbers, or "-" and cannot be empty.
// If the profile is invalid, it returns an error.
func validateProfile(profile string) error {
match, err := regexp.MatchString("^[a-zA-Z0-9-]+$", profile)
if err != nil {
return fmt.Errorf("match string regex: %w", err)
}
if !match {
return fmt.Errorf("profile name can only contain letters, numbers, and \"-\" and cannot be empty")
}
return nil
}
59 changes: 59 additions & 0 deletions internal/pkg/config/profiles_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package config

import "testing"

func TestValidateProfile(t *testing.T) {
tests := []struct {
description string
profile string
isValid bool
}{
{
description: "valid profile with letters",
profile: "myprofile",
isValid: true,
},
{
description: "valid profile with uppercase letters",
profile: "myProfile",
isValid: true,
},
{
description: "valid with letters and hyphen",
profile: "my-profile",
isValid: true,
},
{
description: "valid with letters, numbers, and hyphen",
profile: "my-profile-123",
isValid: true,
},
{
description: "invalid empty",
profile: "",
isValid: false,
},
{
description: "invalid with special characters",
profile: "my_profile",
isValid: false,
},
{
description: "invalid with spaces",
profile: "my profile",
isValid: false,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := validateProfile(tt.profile)
if tt.isValid && err != nil {
t.Errorf("expected profile to be valid but got error: %v", err)
}
if !tt.isValid && err == nil {
t.Errorf("expected profile to be invalid but got no error")
}
})
}
}
Empty file.
1 change: 1 addition & 0 deletions internal/pkg/config/test-data/file-with-content.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
my-content
Empty file.