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
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)
}
})
}
6 changes: 6 additions & 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 Expand Up @@ -748,6 +749,11 @@ func (c *BaseController) createConfigComponent(req Requirements) error {
return nil
}

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

return nil
}

Expand Down
54 changes: 54 additions & 0 deletions pkg/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,60 @@ func TestBaseController_createConfigComponent(t *testing.T) {
t.Errorf("Expected no error, got %v", err)
}
})

t.Run("GeneratesContextID", func(t *testing.T) {
// Given a controller with a config handler
controller, mocks := setup(t)
mockConfigHandler := config.NewMockConfigHandler()
var generateCalled bool
mockConfigHandler.GenerateContextIDFunc = func() error {
generateCalled = true
return nil
}
controller.constructors.NewConfigHandler = func(di.Injector) config.ConfigHandler {
return mockConfigHandler
}

// Clear any existing config handler
mocks.Injector.Register("configHandler", nil)

// When creating the config component
err := controller.createConfigComponent(Requirements{})

// Then context ID should be generated
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}
if !generateCalled {
t.Error("Expected GenerateContextID to be called")
}
})

t.Run("HandlesGenerateContextIDError", func(t *testing.T) {
// Given a controller with a failing config handler
controller, mocks := setup(t)
mockConfigHandler := config.NewMockConfigHandler()
mockConfigHandler.GenerateContextIDFunc = func() error {
return fmt.Errorf("failed to generate context ID")
}
controller.constructors.NewConfigHandler = func(di.Injector) config.ConfigHandler {
return mockConfigHandler
}

// Clear any existing config handler
mocks.Injector.Register("configHandler", nil)

// When creating the config component
err := controller.createConfigComponent(Requirements{})

// Then the error should be propagated
if err == nil {
t.Error("Expected error, got nil")
}
if !strings.Contains(err.Error(), "failed to generate context ID") {
t.Errorf("Expected error about context ID generation, got: %v", err)
}
})
}

func TestBaseController_createSecretsComponents(t *testing.T) {
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
Loading
Loading