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
5 changes: 5 additions & 0 deletions api/v1alpha1/config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Config struct {

// Context represents the context configuration
type Context struct {
ID *string `yaml:"id,omitempty"`
ProjectName *string `yaml:"projectName,omitempty"`
Blueprint *string `yaml:"blueprint,omitempty"`
Environment map[string]string `yaml:"environment,omitempty"`
Expand All @@ -40,6 +41,9 @@ func (base *Context) Merge(overlay *Context) {
if overlay == nil {
return
}
if overlay.ID != nil {
base.ID = overlay.ID
}
if overlay.ProjectName != nil {
base.ProjectName = overlay.ProjectName
}
Expand Down Expand Up @@ -123,6 +127,7 @@ func (c *Context) DeepCopy() *Context {
}
}
return &Context{
ID: c.ID,
ProjectName: c.ProjectName,
Blueprint: c.Blueprint,
Environment: environmentCopy,
Expand Down
16 changes: 16 additions & 0 deletions api/v1alpha1/config_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,22 @@ func TestConfig_Merge(t *testing.T) {
t.Errorf("ProjectName mismatch: expected 'OverlayProject', got '%s'", *base.ProjectName)
}
})

t.Run("MergeWithID", func(t *testing.T) {
base := &Context{
ID: ptrString("base-id"),
}

overlay := &Context{
ID: ptrString("overlay-id"),
}

base.Merge(overlay)

if base.ID == nil || *base.ID != "overlay-id" {
t.Errorf("ID mismatch: expected 'overlay-id', got '%s'", *base.ID)
}
})
}

func TestConfig_Copy(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,11 @@ var initCmd = &cobra.Command{
cliConfigPath = yamlPath
}

// Set the context ID
if err := configHandler.GenerateContextID(); err != nil {
return fmt.Errorf("failed to generate context ID: %w", err)
}

// Save the cli configuration
if err := configHandler.SaveConfig(cliConfigPath); err != nil {
return fmt.Errorf("Error saving config file: %w", err)
Expand Down
33 changes: 33 additions & 0 deletions cmd/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,4 +802,37 @@ func TestInitCmd(t *testing.T) {
t.Errorf("Expected new.key=new-value, got %v", actual)
}
})

t.Run("GenerateContextIDError", func(t *testing.T) {
// Given a set of mocks with proper configuration
mocks := setupInitMocks(t, nil)

// Override config handler to return error for GenerateContextID
mockConfigHandler := config.NewMockConfigHandler()
mockConfigHandler.GetContextFunc = func() string { return "" }
mockConfigHandler.SetContextFunc = func(context string) error { return nil }
mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { return "" }
mockConfigHandler.SetDefaultFunc = func(config v1alpha1.Context) error { return nil }
mockConfigHandler.SetContextValueFunc = func(key string, value any) error { return nil }
mockConfigHandler.SaveConfigFunc = func(path string) error { return nil }
mockConfigHandler.GenerateContextIDFunc = func() error { return fmt.Errorf("generate context id error") }
mocks.Controller.ResolveConfigHandlerFunc = func() config.ConfigHandler { return mockConfigHandler }

// Set up command arguments
rootCmd.SetArgs([]string{"init"})

// When executing the command
err := Execute(mocks.Controller)

// Then error should occur
if err == nil {
t.Error("Expected error, got nil")
}

// And error should contain generate context id error message
expectedError := "failed to generate context ID: generate context id error"
if err.Error() != expectedError {
t.Errorf("Expected error %q, got %q", expectedError, err.Error())
}
})
}
1 change: 1 addition & 0 deletions pkg/config/config_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type ConfigHandler interface {
Clean() error
IsLoaded() bool
SetSecretsProvider(provider secrets.SecretsProvider)
GenerateContextID() error
}

const (
Expand Down
9 changes: 9 additions & 0 deletions pkg/config/mock_config_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type MockConfigHandler struct {
GetConfigRootFunc func() (string, error)
CleanFunc func() error
SetSecretsProviderFunc func(provider secrets.SecretsProvider)
GenerateContextIDFunc func() error
}

// =============================================================================
Expand Down Expand Up @@ -216,5 +217,13 @@ func (m *MockConfigHandler) SetSecretsProvider(provider secrets.SecretsProvider)
}
}

// GenerateContextID calls the mock GenerateContextIDFunc if set, otherwise returns nil
func (m *MockConfigHandler) GenerateContextID() error {
if m.GenerateContextIDFunc != nil {
return m.GenerateContextIDFunc()
}
return nil
}

// Ensure MockConfigHandler implements ConfigHandler
var _ ConfigHandler = (*MockConfigHandler)(nil)
30 changes: 30 additions & 0 deletions pkg/config/mock_config_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,3 +705,33 @@ func TestMockConfigHandler_SetSecretsProvider(t *testing.T) {
handler.SetSecretsProvider(mockProvider)
})
}

func TestMockConfigHandler_GenerateContextID(t *testing.T) {
t.Run("WithMockFunction", func(t *testing.T) {
// Given a mock config handler with GenerateContextIDFunc set
handler := NewMockConfigHandler()
mockErr := fmt.Errorf("mock generate context ID error")
handler.GenerateContextIDFunc = func() error { return mockErr }

// When GenerateContextID is called
err := handler.GenerateContextID()

// Then the error should match the expected mock error
if err != mockErr {
t.Errorf("Expected error = %v, got = %v", mockErr, err)
}
})

t.Run("WithNoFuncSet", func(t *testing.T) {
// Given a mock config handler without GenerateContextIDFunc set
handler := NewMockConfigHandler()

// When GenerateContextID is called
err := handler.GenerateContextID()

// Then no error should be returned
if err != nil {
t.Errorf("Expected error = %v, got = %v", nil, err)
}
})
}
39 changes: 21 additions & 18 deletions pkg/config/shims.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"crypto/rand"
"os"

"github.com/goccy/go-yaml"
Expand All @@ -12,29 +13,31 @@ import (

// Shims provides mockable wrappers around system and runtime functions
type Shims struct {
ReadFile func(string) ([]byte, error)
WriteFile func(string, []byte, os.FileMode) error
RemoveAll func(string) error
Getenv func(string) string
Setenv func(string, string) error
Stat func(string) (os.FileInfo, error)
MkdirAll func(string, os.FileMode) error
YamlMarshal func(any) ([]byte, error)
YamlUnmarshal func([]byte, any) error
ReadFile func(string) ([]byte, error)
WriteFile func(string, []byte, os.FileMode) error
RemoveAll func(string) error
Getenv func(string) string
Setenv func(string, string) error
Stat func(string) (os.FileInfo, error)
MkdirAll func(string, os.FileMode) error
YamlMarshal func(any) ([]byte, error)
YamlUnmarshal func([]byte, any) error
CryptoRandRead func([]byte) (int, error)
}

// NewShims creates a new Shims instance with default implementations
func NewShims() *Shims {
return &Shims{
ReadFile: os.ReadFile,
WriteFile: os.WriteFile,
RemoveAll: os.RemoveAll,
Getenv: os.Getenv,
Setenv: os.Setenv,
Stat: os.Stat,
MkdirAll: os.MkdirAll,
YamlMarshal: yaml.Marshal,
YamlUnmarshal: yaml.Unmarshal,
ReadFile: os.ReadFile,
WriteFile: os.WriteFile,
RemoveAll: os.RemoveAll,
Getenv: os.Getenv,
Setenv: os.Setenv,
Stat: os.Stat,
MkdirAll: os.MkdirAll,
YamlMarshal: yaml.Marshal,
YamlUnmarshal: yaml.Unmarshal,
CryptoRandRead: func(b []byte) (int, error) { return rand.Read(b) },
}
}

Expand Down
20 changes: 20 additions & 0 deletions pkg/config/yaml_config_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,3 +645,23 @@ func convertValue(value string, targetType reflect.Type) (any, error) {

return convertedValue, nil
}

// GenerateContextID generates a random context ID if one doesn't exist
func (y *YamlConfigHandler) GenerateContextID() error {
if y.GetString("id") != "" {
return nil
}

const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 7)
if _, err := y.shims.CryptoRandRead(b); err != nil {
return fmt.Errorf("failed to generate random context ID: %w", err)
}

for i := range b {
b[i] = charset[int(b[i])%len(charset)]
}

id := "w" + string(b)
return y.SetContextValue("id", id)
}
84 changes: 83 additions & 1 deletion pkg/config/yaml_config_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1650,6 +1650,7 @@ func TestYamlConfigHandler_SetContextValue(t *testing.T) {
t.Fatalf("Failed to initialize handler: %v", err)
}
handler.shims = mocks.Shims
handler.path = filepath.Join(t.TempDir(), "config.yaml")
return handler, mocks
}

Expand Down Expand Up @@ -1991,7 +1992,6 @@ func TestYamlConfigHandler_ConvertValue(t *testing.T) {

// When converting the value
_, err := convertValue(value, targetType)

// Then an error should be returned
if err == nil {
t.Fatal("Expected error for integer overflow")
Expand Down Expand Up @@ -2397,3 +2397,85 @@ func Test_setValueByPath(t *testing.T) {
}
})
}

func TestYamlConfigHandler_GenerateContextID(t *testing.T) {
setup := func(t *testing.T) (*YamlConfigHandler, *Mocks) {
mocks := setupMocks(t)
handler := NewYamlConfigHandler(mocks.Injector)
handler.shims = mocks.Shims
if err := handler.Initialize(); err != nil {
t.Fatalf("Failed to initialize handler: %v", err)
}
return handler, mocks
}

t.Run("WhenContextIDExists", func(t *testing.T) {
// Given a set of safe mocks and a YamlConfigHandler
handler, _ := setup(t)

// And an existing context ID
existingID := "w1234567"
handler.SetContextValue("id", existingID)

// When GenerateContextID is called
err := handler.GenerateContextID()

// Then no error should be returned
if err != nil {
t.Fatalf("GenerateContextID() unexpected error: %v", err)
}

// And the existing ID should remain unchanged
if got := handler.GetString("id"); got != existingID {
t.Errorf("Expected ID = %v, got = %v", existingID, got)
}
})

t.Run("WhenContextIDDoesNotExist", func(t *testing.T) {
// Given a set of safe mocks and a YamlConfigHandler
handler, _ := setup(t)

// When GenerateContextID is called
err := handler.GenerateContextID()

// Then no error should be returned
if err != nil {
t.Fatalf("GenerateContextID() unexpected error: %v", err)
}

// And a new ID should be generated
id := handler.GetString("id")
if id == "" {
t.Fatal("Expected non-empty ID")
}

// And the ID should start with 'w' and be 8 characters long
if len(id) != 8 || !strings.HasPrefix(id, "w") {
t.Errorf("Expected ID to start with 'w' and be 8 characters long, got: %s", id)
}
})

t.Run("WhenRandomGenerationFails", func(t *testing.T) {
// Given a set of safe mocks and a YamlConfigHandler
handler, _ := setup(t)

// And a mocked crypto/rand that fails
handler.shims.CryptoRandRead = func([]byte) (int, error) {
return 0, fmt.Errorf("mocked crypto/rand error")
}

// When GenerateContextID is called
err := handler.GenerateContextID()

// Then an error should be returned
if err == nil {
t.Fatal("Expected error, got nil")
}

// And the error message should be as expected
expectedError := "failed to generate random context ID: mocked crypto/rand error"
if err.Error() != expectedError {
t.Errorf("Expected error = %v, got = %v", expectedError, err)
}
})
}
1 change: 1 addition & 0 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ func (c *BaseController) InitializeWithRequirements(req Requirements) error {
if err := c.CreateComponents(); err != nil {
return fmt.Errorf("failed to create components: %w", err)
}

if err := c.InitializeComponents(); err != nil {
return fmt.Errorf("failed to initialize components: %w", err)
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/env/terraform_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func (e *TerraformEnvPrinter) GetEnvVars() (map[string]string, error) {
"TF_CLI_ARGS_import",
"TF_CLI_ARGS_destroy",
"TF_VAR_context_path",
"TF_VAR_context_id",
"TF_VAR_os_type",
}

Expand Down Expand Up @@ -119,6 +120,7 @@ func (e *TerraformEnvPrinter) GetEnvVars() (map[string]string, error) {
envVars["TF_CLI_ARGS_import"] = strings.TrimSpace(strings.Join(varFileArgs, " "))
envVars["TF_CLI_ARGS_destroy"] = strings.TrimSpace(strings.Join(varFileArgs, " "))
envVars["TF_VAR_context_path"] = strings.TrimSpace(filepath.ToSlash(configRoot))
envVars["TF_VAR_context_id"] = strings.TrimSpace(e.configHandler.GetString("id", ""))

// Set os_type based on the OS
if e.shims.Goos() == "windows" {
Expand Down
1 change: 1 addition & 0 deletions pkg/env/terraform_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ func TestTerraformEnv_Print(t *testing.T) {
filepath.Join(configRoot, "terraform/project/path.tfvars"),
filepath.Join(configRoot, "terraform/project/path.tfvars.json")),
"TF_VAR_context_path": configRoot,
"TF_VAR_context_id": "",
"TF_VAR_os_type": expectedOSType,
}

Expand Down
Loading