diff --git a/api/v1alpha1/aws/aws_config.go b/api/v1alpha1/aws/aws_config.go index ba68405cd..89ab3c2b1 100644 --- a/api/v1alpha1/aws/aws_config.go +++ b/api/v1alpha1/aws/aws_config.go @@ -5,11 +5,11 @@ type AWSConfig struct { // Enabled indicates whether AWS integration is enabled. Enabled *bool `yaml:"enabled,omitempty"` - // AWSEndpointURL specifies the custom endpoint URL for AWS services. - AWSEndpointURL *string `yaml:"aws_endpoint_url,omitempty"` + // EndpointURL specifies the custom endpoint URL for AWS services. + EndpointURL *string `yaml:"endpoint_url,omitempty"` - // AWSProfile defines the AWS CLI profile to use for authentication. - AWSProfile *string `yaml:"aws_profile,omitempty"` + // Profile defines the AWS CLI profile to use for authentication. + Profile *string `yaml:"profile,omitempty"` // S3Hostname sets the custom hostname for the S3 service. S3Hostname *string `yaml:"s3_hostname,omitempty"` @@ -19,6 +19,9 @@ type AWSConfig struct { // Localstack contains the configuration for Localstack, a local AWS cloud emulator. Localstack *LocalstackConfig `yaml:"localstack,omitempty"` + + // Region specifies the AWS region to use. + Region *string `yaml:"region,omitempty"` } // LocalstackConfig represents the Localstack configuration @@ -32,11 +35,11 @@ func (base *AWSConfig) Merge(overlay *AWSConfig) { if overlay.Enabled != nil { base.Enabled = overlay.Enabled } - if overlay.AWSEndpointURL != nil { - base.AWSEndpointURL = overlay.AWSEndpointURL + if overlay.EndpointURL != nil { + base.EndpointURL = overlay.EndpointURL } - if overlay.AWSProfile != nil { - base.AWSProfile = overlay.AWSProfile + if overlay.Profile != nil { + base.Profile = overlay.Profile } if overlay.S3Hostname != nil { base.S3Hostname = overlay.S3Hostname @@ -55,6 +58,9 @@ func (base *AWSConfig) Merge(overlay *AWSConfig) { base.Localstack.Services = overlay.Localstack.Services } } + if overlay.Region != nil { + base.Region = overlay.Region + } } // Copy creates a deep copy of the AWSConfig object @@ -66,11 +72,11 @@ func (c *AWSConfig) Copy() *AWSConfig { if c.Enabled != nil { copy.Enabled = c.Enabled } - if c.AWSEndpointURL != nil { - copy.AWSEndpointURL = c.AWSEndpointURL + if c.EndpointURL != nil { + copy.EndpointURL = c.EndpointURL } - if c.AWSProfile != nil { - copy.AWSProfile = c.AWSProfile + if c.Profile != nil { + copy.Profile = c.Profile } if c.S3Hostname != nil { copy.S3Hostname = c.S3Hostname @@ -87,5 +93,8 @@ func (c *AWSConfig) Copy() *AWSConfig { copy.Localstack.Services = c.Localstack.Services } } + if c.Region != nil { + copy.Region = c.Region + } return copy } diff --git a/api/v1alpha1/aws/aws_config_test.go b/api/v1alpha1/aws/aws_config_test.go index 676d1d794..4931f0565 100644 --- a/api/v1alpha1/aws/aws_config_test.go +++ b/api/v1alpha1/aws/aws_config_test.go @@ -7,27 +7,29 @@ import ( func TestAWSConfig_Merge(t *testing.T) { t.Run("MergeWithNoNils", func(t *testing.T) { base := &AWSConfig{ - Enabled: ptrBool(true), - AWSEndpointURL: ptrString("https://base.aws.endpoint"), - AWSProfile: ptrString("base-profile"), - S3Hostname: ptrString("base-s3-hostname"), - MWAAEndpoint: ptrString("base-mwaa-endpoint"), + Enabled: ptrBool(true), + EndpointURL: ptrString("https://base.aws.endpoint"), + Profile: ptrString("base-profile"), + S3Hostname: ptrString("base-s3-hostname"), + MWAAEndpoint: ptrString("base-mwaa-endpoint"), Localstack: &LocalstackConfig{ Enabled: ptrBool(true), Services: []string{"s3", "lambda"}, }, + Region: ptrString("base-region"), } overlay := &AWSConfig{ - Enabled: ptrBool(false), - AWSEndpointURL: ptrString("https://overlay.aws.endpoint"), - AWSProfile: ptrString("overlay-profile"), - S3Hostname: ptrString("overlay-s3-hostname"), - MWAAEndpoint: ptrString("overlay-mwaa-endpoint"), + Enabled: ptrBool(false), + EndpointURL: ptrString("https://overlay.aws.endpoint"), + Profile: ptrString("overlay-profile"), + S3Hostname: ptrString("overlay-s3-hostname"), + MWAAEndpoint: ptrString("overlay-mwaa-endpoint"), Localstack: &LocalstackConfig{ Enabled: ptrBool(false), Services: []string{"dynamodb"}, }, + Region: ptrString("overlay-region"), } base.Merge(overlay) @@ -35,11 +37,11 @@ func TestAWSConfig_Merge(t *testing.T) { if base.Enabled == nil || *base.Enabled != false { t.Errorf("Enabled mismatch: expected false, got %v", *base.Enabled) } - if base.AWSEndpointURL == nil || *base.AWSEndpointURL != "https://overlay.aws.endpoint" { - t.Errorf("AWSEndpointURL mismatch: expected 'https://overlay.aws.endpoint', got '%s'", *base.AWSEndpointURL) + if base.EndpointURL == nil || *base.EndpointURL != "https://overlay.aws.endpoint" { + t.Errorf("EndpointURL mismatch: expected 'https://overlay.aws.endpoint', got '%s'", *base.EndpointURL) } - if base.AWSProfile == nil || *base.AWSProfile != "overlay-profile" { - t.Errorf("AWSProfile mismatch: expected 'overlay-profile', got '%s'", *base.AWSProfile) + if base.Profile == nil || *base.Profile != "overlay-profile" { + t.Errorf("Profile mismatch: expected 'overlay-profile', got '%s'", *base.Profile) } if base.S3Hostname == nil || *base.S3Hostname != "overlay-s3-hostname" { t.Errorf("S3Hostname mismatch: expected 'overlay-s3-hostname', got '%s'", *base.S3Hostname) @@ -53,28 +55,33 @@ func TestAWSConfig_Merge(t *testing.T) { if len(base.Localstack.Services) != 1 || base.Localstack.Services[0] != "dynamodb" { t.Errorf("Localstack Services mismatch: expected ['dynamodb'], got %v", base.Localstack.Services) } + if base.Region == nil || *base.Region != "overlay-region" { + t.Errorf("Region mismatch: expected 'overlay-region', got '%s'", *base.Region) + } }) t.Run("MergeWithAllNils", func(t *testing.T) { base := &AWSConfig{ - Enabled: nil, - AWSEndpointURL: nil, - AWSProfile: nil, - S3Hostname: nil, - MWAAEndpoint: nil, - Localstack: nil, + Enabled: nil, + EndpointURL: nil, + Profile: nil, + S3Hostname: nil, + MWAAEndpoint: nil, + Localstack: nil, + Region: nil, } overlay := &AWSConfig{ - Enabled: nil, - AWSEndpointURL: nil, - AWSProfile: nil, - S3Hostname: nil, - MWAAEndpoint: nil, + Enabled: nil, + EndpointURL: nil, + Profile: nil, + S3Hostname: nil, + MWAAEndpoint: nil, Localstack: &LocalstackConfig{ Enabled: nil, Services: nil, }, + Region: nil, } base.Merge(overlay) @@ -82,11 +89,11 @@ func TestAWSConfig_Merge(t *testing.T) { if base.Enabled != nil { t.Errorf("Enabled mismatch: expected nil, got %v", base.Enabled) } - if base.AWSEndpointURL != nil { - t.Errorf("AWSEndpointURL mismatch: expected nil, got '%s'", *base.AWSEndpointURL) + if base.EndpointURL != nil { + t.Errorf("EndpointURL mismatch: expected nil, got '%s'", *base.EndpointURL) } - if base.AWSProfile != nil { - t.Errorf("AWSProfile mismatch: expected nil, got '%s'", *base.AWSProfile) + if base.Profile != nil { + t.Errorf("Profile mismatch: expected nil, got '%s'", *base.Profile) } if base.S3Hostname != nil { t.Errorf("S3Hostname mismatch: expected nil, got '%s'", *base.S3Hostname) @@ -97,21 +104,25 @@ func TestAWSConfig_Merge(t *testing.T) { if base.Localstack != nil && (base.Localstack.Enabled != nil || base.Localstack.Services != nil) { t.Errorf("Localstack mismatch: expected nil, got %v", base.Localstack) } + if base.Region != nil { + t.Errorf("Region mismatch: expected nil, got '%s'", *base.Region) + } }) } func TestAWSConfig_Copy(t *testing.T) { t.Run("CopyWithNonNilValues", func(t *testing.T) { original := &AWSConfig{ - Enabled: ptrBool(true), - AWSEndpointURL: ptrString("https://original.aws.endpoint"), - AWSProfile: ptrString("original-profile"), - S3Hostname: ptrString("original-s3-hostname"), - MWAAEndpoint: ptrString("original-mwaa-endpoint"), + Enabled: ptrBool(true), + EndpointURL: ptrString("https://original.aws.endpoint"), + Profile: ptrString("original-profile"), + S3Hostname: ptrString("original-s3-hostname"), + MWAAEndpoint: ptrString("original-mwaa-endpoint"), Localstack: &LocalstackConfig{ Enabled: ptrBool(true), Services: []string{"s3", "lambda"}, }, + Region: ptrString("original-region"), } copy := original.Copy() @@ -119,11 +130,11 @@ func TestAWSConfig_Copy(t *testing.T) { if original.Enabled == nil || copy.Enabled == nil || *original.Enabled != *copy.Enabled { t.Errorf("Enabled mismatch: expected %v, got %v", *original.Enabled, *copy.Enabled) } - if original.AWSEndpointURL == nil || copy.AWSEndpointURL == nil || *original.AWSEndpointURL != *copy.AWSEndpointURL { - t.Errorf("AWSEndpointURL mismatch: expected %v, got %v", *original.AWSEndpointURL, *copy.AWSEndpointURL) + if original.EndpointURL == nil || copy.EndpointURL == nil || *original.EndpointURL != *copy.EndpointURL { + t.Errorf("EndpointURL mismatch: expected %v, got %v", *original.EndpointURL, *copy.EndpointURL) } - if original.AWSProfile == nil || copy.AWSProfile == nil || *original.AWSProfile != *copy.AWSProfile { - t.Errorf("AWSProfile mismatch: expected %v, got %v", *original.AWSProfile, *copy.AWSProfile) + if original.Profile == nil || copy.Profile == nil || *original.Profile != *copy.Profile { + t.Errorf("Profile mismatch: expected %v, got %v", *original.Profile, *copy.Profile) } if original.S3Hostname == nil || copy.S3Hostname == nil || *original.S3Hostname != *copy.S3Hostname { t.Errorf("S3Hostname mismatch: expected %v, got %v", *original.S3Hostname, *copy.S3Hostname) @@ -154,6 +165,10 @@ func TestAWSConfig_Copy(t *testing.T) { if original.Localstack.Services[0] == copy.Localstack.Services[0] { t.Errorf("Original Localstack Services was modified: expected %v, got %v", "s3", copy.Localstack.Services[0]) } + + if original.Region == nil || copy.Region == nil || *original.Region != *copy.Region { + t.Errorf("Region mismatch: expected %v, got %v", *original.Region, *copy.Region) + } }) t.Run("CopyNil", func(t *testing.T) { diff --git a/api/v1alpha1/config_types_test.go b/api/v1alpha1/config_types_test.go index 395c8262c..bd1489f49 100644 --- a/api/v1alpha1/config_types_test.go +++ b/api/v1alpha1/config_types_test.go @@ -18,8 +18,8 @@ func TestConfig_Merge(t *testing.T) { t.Run("MergeWithNonNilValues", func(t *testing.T) { base := &Context{ AWS: &aws.AWSConfig{ - Enabled: ptrBool(true), - AWSEndpointURL: ptrString("https://base.aws.endpoint"), + Enabled: ptrBool(true), + EndpointURL: ptrString("https://base.aws.endpoint"), }, Docker: &docker.DockerConfig{ Enabled: ptrBool(true), @@ -58,7 +58,7 @@ func TestConfig_Merge(t *testing.T) { overlay := &Context{ AWS: &aws.AWSConfig{ - AWSEndpointURL: ptrString("https://overlay.aws.endpoint"), + EndpointURL: ptrString("https://overlay.aws.endpoint"), }, Docker: &docker.DockerConfig{ Enabled: ptrBool(false), @@ -97,8 +97,8 @@ func TestConfig_Merge(t *testing.T) { base.Merge(overlay) - if base.AWS.AWSEndpointURL == nil || *base.AWS.AWSEndpointURL != "https://overlay.aws.endpoint" { - t.Errorf("AWS AWSEndpointURL mismatch: expected 'https://overlay.aws.endpoint', got '%s'", *base.AWS.AWSEndpointURL) + if base.AWS.EndpointURL == nil || *base.AWS.EndpointURL != "https://overlay.aws.endpoint" { + t.Errorf("AWS EndpointURL mismatch: expected 'https://overlay.aws.endpoint', got '%s'", *base.AWS.EndpointURL) } if base.Docker.Enabled == nil || *base.Docker.Enabled != false { t.Errorf("Docker Enabled mismatch: expected false, got %v", *base.Docker.Enabled) @@ -132,8 +132,8 @@ func TestConfig_Merge(t *testing.T) { t.Run("MergeWithNilOverlay", func(t *testing.T) { base := &Context{ AWS: &aws.AWSConfig{ - Enabled: ptrBool(true), - AWSEndpointURL: ptrString("https://base.aws.endpoint"), + Enabled: ptrBool(true), + EndpointURL: ptrString("https://base.aws.endpoint"), }, Docker: &docker.DockerConfig{ Enabled: ptrBool(true), @@ -173,8 +173,8 @@ func TestConfig_Merge(t *testing.T) { var overlay *Context = nil base.Merge(overlay) - if base.AWS.AWSEndpointURL == nil || *base.AWS.AWSEndpointURL != "https://base.aws.endpoint" { - t.Errorf("AWS AWSEndpointURL mismatch: expected 'https://base.aws.endpoint', got '%s'", *base.AWS.AWSEndpointURL) + if base.AWS.EndpointURL == nil || *base.AWS.EndpointURL != "https://base.aws.endpoint" { + t.Errorf("AWS EndpointURL mismatch: expected 'https://base.aws.endpoint', got '%s'", *base.AWS.EndpointURL) } if base.Docker.Enabled == nil || *base.Docker.Enabled != true { t.Errorf("Docker Enabled mismatch: expected true, got %v", *base.Docker.Enabled) @@ -210,7 +210,7 @@ func TestConfig_Merge(t *testing.T) { overlay := &Context{ AWS: &aws.AWSConfig{ - AWSEndpointURL: ptrString("https://overlay.aws.endpoint"), + EndpointURL: ptrString("https://overlay.aws.endpoint"), }, Docker: &docker.DockerConfig{ Enabled: ptrBool(false), @@ -249,8 +249,8 @@ func TestConfig_Merge(t *testing.T) { base.Merge(overlay) - if base.AWS.AWSEndpointURL == nil || *base.AWS.AWSEndpointURL != "https://overlay.aws.endpoint" { - t.Errorf("AWS AWSEndpointURL mismatch: expected 'https://overlay.aws.endpoint', got '%s'", *base.AWS.AWSEndpointURL) + if base.AWS.EndpointURL == nil || *base.AWS.EndpointURL != "https://overlay.aws.endpoint" { + t.Errorf("AWS EndpointURL mismatch: expected 'https://overlay.aws.endpoint', got '%s'", *base.AWS.EndpointURL) } if base.Docker.Enabled == nil || *base.Docker.Enabled != false { t.Errorf("Docker Enabled mismatch: expected false, got %v", *base.Docker.Enabled) @@ -305,8 +305,8 @@ func TestConfig_Copy(t *testing.T) { "KEY": "value", }, AWS: &aws.AWSConfig{ - Enabled: ptrBool(true), - AWSEndpointURL: ptrString("https://original.aws.endpoint"), + Enabled: ptrBool(true), + EndpointURL: ptrString("https://original.aws.endpoint"), }, Docker: &docker.DockerConfig{ Enabled: ptrBool(true), diff --git a/aqua.yaml b/aqua.yaml index bec7ced26..ddbbf30a4 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -30,3 +30,4 @@ packages: - name: helm/helm@v3.17.1 - name: 1password/cli@v2.30.3 - name: fluxcd/flux2@v2.5.0 +- name: aws/aws-cli@2.24.10 diff --git a/go.mod b/go.mod index 79c4b0efc..16123a17e 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/zclconf/go-cty v1.16.2 golang.org/x/crypto v0.34.0 golang.org/x/sys v0.30.0 + gopkg.in/ini.v1 v1.67.0 k8s.io/api v0.32.2 k8s.io/apimachinery v0.32.2 k8s.io/client-go v0.32.2 @@ -169,7 +170,6 @@ require ( google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.32.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect diff --git a/pkg/config/yaml_config_handler_test.go b/pkg/config/yaml_config_handler_test.go index 1d82d660e..161eaa4f1 100644 --- a/pkg/config/yaml_config_handler_test.go +++ b/pkg/config/yaml_config_handler_test.go @@ -174,7 +174,7 @@ func TestYamlConfigHandler_Get(t *testing.T) { // When setting the default context (should not be used) defaultContext := v1alpha1.Context{ AWS: &aws.AWSConfig{ - AWSEndpointURL: ptrString("http://default.aws.endpoint"), + EndpointURL: ptrString("http://default.aws.endpoint"), }, } handler.SetDefault(defaultContext) @@ -384,7 +384,7 @@ func TestYamlConfigHandler_SaveConfig(t *testing.T) { "email": "john.doe@example.com", }, AWS: &aws.AWSConfig{ - AWSEndpointURL: nil, + EndpointURL: nil, }, }, }, @@ -487,7 +487,7 @@ func TestYamlConfigHandler_GetInt(t *testing.T) { Contexts: map[string]*v1alpha1.Context{ "default": { AWS: &aws.AWSConfig{ - AWSEndpointURL: ptrString("notAnInt"), + EndpointURL: ptrString("notAnInt"), }, }, }, @@ -1040,7 +1040,7 @@ func TestSetValueByPath(t *testing.T) { "level2": "value2", }, AWS: &aws.AWSConfig{ - AWSEndpointURL: ptrString("http://aws.test:4566"), + EndpointURL: ptrString("http://aws.test:4566"), }, }, }, diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index fdc3d7837..d3f7fde0d 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -42,7 +42,12 @@ const ( // renovate: datasource=docker depName=localstack/localstack DEFAULT_AWS_LOCALSTACK_IMAGE = "localstack/localstack:3.8.1" // renovate: datasource=docker depName=localstack/localstack-pro - DEFAULT_AWS_LOCALSTACK_PRO_IMAGE = "localstack/localstack-pro:3.8.1" + DEFAULT_AWS_LOCALSTACK_PRO_IMAGE = "localstack/localstack-pro:3.8.1" + DEFAULT_AWS_REGION = "us-east-1" + DEFAULT_AWS_LOCALSTACK_PORT = "4566" + DEFAULT_AWS_LOCALSTACK_ACCESS_KEY = "LSIAQAAAAAAVNCBMPNSG" + // #nosec G101 -- These are development secrets and are safe to be hardcoded. + DEFAULT_AWS_LOCALSTACK_SECRET_KEY = "LSIAQAAAAAAVNCBMPNSG" ) // Default DNS settings diff --git a/pkg/controller/mock_controller.go b/pkg/controller/mock_controller.go index 24cdbb605..d1add3191 100644 --- a/pkg/controller/mock_controller.go +++ b/pkg/controller/mock_controller.go @@ -148,6 +148,10 @@ func (m *MockController) CreateProjectComponents() error { kustomizeGenerator := generators.NewMockGenerator() m.injector.Register("kustomizeGenerator", kustomizeGenerator) + // Create a new mock aws generator + awsGenerator := generators.NewMockGenerator() + m.injector.Register("awsGenerator", awsGenerator) + return nil } diff --git a/pkg/controller/real_controller.go b/pkg/controller/real_controller.go index bf488ae5d..20a2aa78e 100644 --- a/pkg/controller/real_controller.go +++ b/pkg/controller/real_controller.go @@ -68,7 +68,7 @@ func (c *RealController) CreateCommonComponents() error { } // Initializes project components like generators and tools manager. Registers -// and initializes blueprint, terraform, and kustomize generators. Determines +// and initializes blueprint, terraform, kustomize, and AWS generators. Determines // and sets the tools manager: aqua, asdf, or default, based on config or setup. func (c *RealController) CreateProjectComponents() error { gitGenerator := generators.NewGitGenerator(c.injector) @@ -83,6 +83,11 @@ func (c *RealController) CreateProjectComponents() error { kustomizeGenerator := generators.NewKustomizeGenerator(c.injector) c.injector.Register("kustomizeGenerator", kustomizeGenerator) + if c.configHandler.GetBool("aws.enabled") { + awsGenerator := generators.NewAWSGenerator(c.injector) + c.injector.Register("awsGenerator", awsGenerator) + } + toolsManagerType := c.configHandler.GetString("toolsManager") var toolsManager tools.ToolsManager diff --git a/pkg/di/mock_injector.go b/pkg/di/mock_injector.go index a13660873..e54b5ed9c 100644 --- a/pkg/di/mock_injector.go +++ b/pkg/di/mock_injector.go @@ -9,6 +9,7 @@ import ( type MockInjector struct { *BaseInjector resolveAllErrors map[interface{}]error + resolveErrors map[string]error mu sync.RWMutex } @@ -17,6 +18,7 @@ func NewMockInjector() *MockInjector { return &MockInjector{ BaseInjector: NewInjector(), resolveAllErrors: make(map[interface{}]error), + resolveErrors: make(map[string]error), } } @@ -27,11 +29,22 @@ func (m *MockInjector) SetResolveAllError(targetType interface{}, err error) { m.resolveAllErrors[targetType] = err } +// SetResolveError sets a specific error to be returned when resolving a specific name +func (m *MockInjector) SetResolveError(name string, err error) { + m.mu.Lock() + defer m.mu.Unlock() + m.resolveErrors[name] = err +} + // Resolve overrides the RealInjector's Resolve method to add error simulation func (m *MockInjector) Resolve(name string) interface{} { m.mu.RLock() defer m.mu.RUnlock() + if err, exists := m.resolveErrors[name]; exists { + return err + } + return m.BaseInjector.Resolve(name) } diff --git a/pkg/env/aws_env.go b/pkg/env/aws_env.go index 5255da1b1..62d063662 100644 --- a/pkg/env/aws_env.go +++ b/pkg/env/aws_env.go @@ -26,14 +26,6 @@ func NewAwsEnvPrinter(injector di.Injector) *AwsEnvPrinter { func (e *AwsEnvPrinter) GetEnvVars() (map[string]string, error) { envVars := make(map[string]string) - // Get the context configuration - contextConfigData := e.configHandler.GetConfig() - - // Ensure the context configuration and AWS-specific settings are available. - if contextConfigData == nil || contextConfigData.AWS == nil { - return nil, fmt.Errorf("context configuration or AWS configuration is missing") - } - // Determine the root directory for configuration files. configRoot, err := e.configHandler.GetConfigRoot() if err != nil { @@ -50,17 +42,11 @@ func (e *AwsEnvPrinter) GetEnvVars() (map[string]string, error) { if awsConfigPath != "" { envVars["AWS_CONFIG_FILE"] = awsConfigPath } - if contextConfigData.AWS.AWSProfile != nil { - envVars["AWS_PROFILE"] = *contextConfigData.AWS.AWSProfile - } - if contextConfigData.AWS.AWSEndpointURL != nil { - envVars["AWS_ENDPOINT_URL"] = *contextConfigData.AWS.AWSEndpointURL - } - if contextConfigData.AWS.S3Hostname != nil { - envVars["S3_HOSTNAME"] = *contextConfigData.AWS.S3Hostname - } - if contextConfigData.AWS.MWAAEndpoint != nil { - envVars["MWAA_ENDPOINT"] = *contextConfigData.AWS.MWAAEndpoint + + // Get the AWS profile from the config handler + awsProfile := e.configHandler.GetString("aws.profile", "default") + if awsProfile != "" { + envVars["AWS_PROFILE"] = awsProfile } return envVars, nil diff --git a/pkg/env/aws_env_test.go b/pkg/env/aws_env_test.go index 21b4f0cd8..ccd9fea69 100644 --- a/pkg/env/aws_env_test.go +++ b/pkg/env/aws_env_test.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "reflect" + "strings" "testing" "github.com/windsorcli/cli/api/v1alpha1" @@ -35,10 +36,10 @@ func setupSafeAwsEnvMocks(injector ...di.Injector) *AwsEnvMocks { mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ AWS: &aws.AWSConfig{ - AWSProfile: stringPtr("default"), - AWSEndpointURL: stringPtr("https://aws.endpoint"), - S3Hostname: stringPtr("s3.amazonaws.com"), - MWAAEndpoint: stringPtr("https://mwaa.endpoint"), + Profile: stringPtr("default"), + EndpointURL: stringPtr("https://aws.endpoint"), + S3Hostname: stringPtr("s3.amazonaws.com"), + MWAAEndpoint: stringPtr("https://mwaa.endpoint"), }, } } @@ -92,35 +93,35 @@ func TestAwsEnv_GetEnvVars(t *testing.T) { } }) - t.Run("MissingConfiguration", func(t *testing.T) { - // Use setupSafeAwsEnvMocks to create mocks - mocks := setupSafeAwsEnvMocks() + // t.Run("MissingConfiguration", func(t *testing.T) { + // // Use setupSafeAwsEnvMocks to create mocks + // mocks := setupSafeAwsEnvMocks() - // Override the GetConfigFunc to return nil for AWS configuration - mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { - return &v1alpha1.Context{AWS: nil} - } + // // Override the GetConfigFunc to return nil for AWS configuration + // mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + // return &v1alpha1.Context{AWS: nil} + // } - mockInjector := mocks.Injector + // mockInjector := mocks.Injector - awsEnvPrinter := NewAwsEnvPrinter(mockInjector) - awsEnvPrinter.Initialize() + // awsEnvPrinter := NewAwsEnvPrinter(mockInjector) + // awsEnvPrinter.Initialize() - // Capture stdout - output := captureStdout(t, func() { - // When calling GetEnvVars - _, err := awsEnvPrinter.GetEnvVars() - if err != nil { - fmt.Println(err) - } - }) + // // Capture stdout + // output := captureStdout(t, func() { + // // When calling GetEnvVars + // _, err := awsEnvPrinter.GetEnvVars() + // if err != nil { + // fmt.Println(err) + // } + // }) - // Then the output should indicate the missing configuration - expectedOutput := "context configuration or AWS configuration is missing\n" - if output != expectedOutput { - t.Errorf("output = %v, want %v", output, expectedOutput) - } - }) + // // Then the output should indicate the missing configuration + // expectedOutput := "context configuration or AWS configuration is missing\n" + // if output != expectedOutput { + // t.Errorf("output = %v, want %v", output, expectedOutput) + // } + // }) t.Run("NoAwsConfigFile", func(t *testing.T) { // Use setupSafeAwsEnvMocks to create mocks @@ -130,10 +131,10 @@ func TestAwsEnv_GetEnvVars(t *testing.T) { mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { return &v1alpha1.Context{ AWS: &aws.AWSConfig{ - AWSProfile: stringPtr("default"), - AWSEndpointURL: stringPtr("https://example.com"), - S3Hostname: stringPtr("s3.example.com"), - MWAAEndpoint: stringPtr("mwaa.example.com"), + Profile: stringPtr("default"), + EndpointURL: stringPtr("https://example.com"), + S3Hostname: stringPtr("s3.example.com"), + MWAAEndpoint: stringPtr("mwaa.example.com"), }, } } @@ -225,42 +226,39 @@ func TestAwsEnv_Print(t *testing.T) { // Verify that PrintEnvVarsFunc was called with the correct envVars expectedEnvVars := map[string]string{ - "AWS_CONFIG_FILE": filepath.FromSlash("/mock/config/root/.aws/config"), - "AWS_PROFILE": "default", - "AWS_ENDPOINT_URL": "https://aws.endpoint", - "S3_HOSTNAME": "s3.amazonaws.com", - "MWAA_ENDPOINT": "https://mwaa.endpoint", + "AWS_CONFIG_FILE": filepath.FromSlash("/mock/config/root/.aws/config"), + "AWS_PROFILE": "default", } if !reflect.DeepEqual(capturedEnvVars, expectedEnvVars) { t.Errorf("capturedEnvVars = %v, want %v", capturedEnvVars, expectedEnvVars) } }) - t.Run("Error", func(t *testing.T) { + t.Run("ErrorRetrievingConfigRoot", func(t *testing.T) { // Use setupSafeAwsEnvMocks to create mocks mocks := setupSafeAwsEnvMocks() + mockInjector := mocks.Injector - // Set AWS configuration to nil to simulate the error condition - mocks.ConfigHandler.GetConfigFunc = func() *v1alpha1.Context { - return &v1alpha1.Context{ - AWS: nil, - } + // Override the GetConfigRoot function to simulate an error + mocks.ConfigHandler.GetConfigRootFunc = func() (string, error) { + return "", fmt.Errorf("mock config root error") } - mockInjector := mocks.Injector awsEnvPrinter := NewAwsEnvPrinter(mockInjector) awsEnvPrinter.Initialize() - // Call Print and expect an error - err := awsEnvPrinter.Print() - if err == nil { - t.Error("expected error, got nil") - } + // Capture stdout + output := captureStdout(t, func() { + // When calling Print + err := awsEnvPrinter.Print() + if err != nil { + fmt.Println(err) + } + }) - // Verify the error message - expectedError := "error getting environment variables: context configuration or AWS configuration is missing" - if err.Error() != expectedError { - t.Errorf("error = %v, want %v", err.Error(), expectedError) + // Then the output should indicate the error + if !strings.Contains(output, "mock config root error") { + t.Errorf("output = %v, want it to contain %v", output, "mock config root error") } }) } diff --git a/pkg/generators/aws_generator.go b/pkg/generators/aws_generator.go new file mode 100644 index 000000000..a1f165ffa --- /dev/null +++ b/pkg/generators/aws_generator.go @@ -0,0 +1,94 @@ +// AWSGenerator scaffolding +package generators + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/windsorcli/cli/pkg/constants" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/services" +) + +// AWSGenerator is a generator that creates AWS configuration files. +type AWSGenerator struct { + BaseGenerator +} + +// NewAWSGenerator creates a new AWSGenerator instance. +func NewAWSGenerator(injector di.Injector) *AWSGenerator { + return &AWSGenerator{ + BaseGenerator: BaseGenerator{injector: injector}, + } +} + +// Write creates an "aws" directory in the project root and modifies +// the AWS config file if it exists. It ensures the default section +// has cli_pager, region, and output set. It also modifies the +// specific profile section and s3 block based on configuration. +func (g *AWSGenerator) Write() error { + configRoot, err := g.configHandler.GetConfigRoot() + if err != nil { + return err + } + + awsConfigFilePath := filepath.Join(configRoot, ".aws", "config") + if _, err := osStat(awsConfigFilePath); os.IsNotExist(err) { + awsFolderPath := filepath.Dir(awsConfigFilePath) + if err := osMkdirAll(awsFolderPath, os.ModePerm); err != nil { + return err + } + } + + cfg, err := iniLoad(awsConfigFilePath) + if err != nil { + cfg = iniEmpty() + } + + // Set default section values + defaultSection := cfg.Section("default") + defaultSection.Key("cli_pager").SetValue(g.configHandler.GetString("aws.cli_pager", "")) + defaultSection.Key("output").SetValue(g.configHandler.GetString("aws.output", "text")) + defaultSection.Key("region").SetValue(g.configHandler.GetString("aws.region", constants.DEFAULT_AWS_REGION)) + + // Set profile-specific section values + profile := g.configHandler.GetString("aws.profile", "default") + sectionName := "default" + if profile != "default" { + sectionName = "profile " + profile + } + + section := cfg.Section(sectionName) + section.Key("region").SetValue(g.configHandler.GetString("aws.region", constants.DEFAULT_AWS_REGION)) + + // Access Localstack configuration + if g.configHandler.GetBool("aws.localstack.enabled", false) { + service, ok := g.injector.Resolve("localstackService").(services.Service) + if !ok { + return fmt.Errorf("localstackService not found") + } + tld := g.configHandler.GetString("dns.domain", "test") + fullName := service.GetName() + "." + tld + + // Build a single endpoint + localstackPort := constants.DEFAULT_AWS_LOCALSTACK_PORT + localstackEndpoint := "http://" + fullName + ":" + localstackPort + + // Modify AWS config with Localstack endpoint + section.Key("endpoint_url").SetValue(localstackEndpoint) + + // Set AWS access key and secret key for Localstack using recommended values + section.Key("aws_access_key_id").SetValue(constants.DEFAULT_AWS_LOCALSTACK_ACCESS_KEY) + section.Key("aws_secret_access_key").SetValue(constants.DEFAULT_AWS_LOCALSTACK_SECRET_KEY) + } + + if err := iniSaveTo(cfg, awsConfigFilePath); err != nil { + return err + } + + return nil +} + +// Ensure AWSGenerator implements the Generator interface +var _ Generator = (*AWSGenerator)(nil) diff --git a/pkg/generators/aws_generator_test.go b/pkg/generators/aws_generator_test.go new file mode 100644 index 000000000..528812283 --- /dev/null +++ b/pkg/generators/aws_generator_test.go @@ -0,0 +1,434 @@ +package generators + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/windsorcli/cli/pkg/config" + "github.com/windsorcli/cli/pkg/constants" + "github.com/windsorcli/cli/pkg/di" + "github.com/windsorcli/cli/pkg/services" + sh "github.com/windsorcli/cli/pkg/shell" + "gopkg.in/ini.v1" +) + +func setupSafeAwsGeneratorMocks(injector ...di.Injector) MockComponents { + // Use the provided injector if available, otherwise create a new one + var mockInjector di.Injector + if len(injector) > 0 { + mockInjector = injector[0] + } else { + mockInjector = di.NewInjector() + } + + // Mock the osStat function to simulate file existence + osStat = func(name string) (os.FileInfo, error) { + if name == filepath.Join("/mock/config/root", ".aws", "config") { + return nil, nil // Simulate that the file exists + } + return nil, os.ErrNotExist + } + + // Mock the osMkdirAll function + osMkdirAll = func(path string, perm os.FileMode) error { + return nil + } + + // Mock the iniLoad function + iniLoad = func(_ interface{}, _ ...interface{}) (*ini.File, error) { + file := iniEmpty() + return file, nil + } + + // Mock the iniSaveTo function to simulate saving the ini file + iniSaveTo = func(cfg *ini.File, filename string) error { + if filename == filepath.Join("/mock/config/root", ".aws", "config") { + return nil // Simulate successful save + } + return nil // Simulate successful save for any file + } + + // Mock the osWriteFile function to simulate file writing + osWriteFile = func(name string, data []byte, perm os.FileMode) error { + if name == filepath.Join("/mock/config/root", ".aws", "config") { + return nil // Simulate successful write + } + return nil // Simulate successful write for any file + } + + // Create a new mock config handler + mockConfigHandler := config.NewMockConfigHandler() + mockInjector.Register("configHandler", mockConfigHandler) + + // Mock the configHandler to return a mock config root + mockConfigHandler.GetConfigRootFunc = func() (string, error) { + return filepath.Join("/mock/config/root"), nil + } + + // Mock the GetString method to return default values for AWS configuration + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + switch key { + case "aws.cli_pager": + return "" + case "aws.output": + return "text" + case "aws.region": + return constants.DEFAULT_AWS_REGION + case "aws.profile": + return "default" + case "dns.domain": + return "test" + default: + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + } + + // Mock the GetBool method to return false for aws.localstack.enabled + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "aws.localstack.enabled" { + return false + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + + // Create a new mock shell + mockShell := sh.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return filepath.Join("/mock/project/root"), nil + } + mockInjector.Register("shell", mockShell) + + // Create a new mock localstack service + mockLocalstackService := services.NewMockService() + mockLocalstackService.GetNameFunc = func() string { + return "aws" + } + mockInjector.Register("localstackService", mockLocalstackService) + + return MockComponents{ + Injector: mockInjector, + MockConfigHandler: mockConfigHandler, + MockShell: mockShell, + } +} + +func TestAWSGenerator_Write(t *testing.T) { + t.Run("SuccessCreatingAwsConfig", func(t *testing.T) { + // Use setupSafeAwsGeneratorMocks to create mock components + mocks := setupSafeAwsGeneratorMocks() + + // Save the original osStat and osWriteFile functions + originalStat := osStat + originalWriteFile := osWriteFile + defer func() { + osStat = originalStat + osWriteFile = originalWriteFile + }() + + // Mock the osStat function to simulate os.IsNotExist for awsConfigFilePath + osStat = func(name string) (os.FileInfo, error) { + if name == filepath.Join("/mock/config/root", ".aws", "config") { + return nil, os.ErrNotExist + } + return nil, nil + } + + // Mock the osWriteFile function to validate that it is called with the expected parameters + osWriteFile = func(filename string, data []byte, perm os.FileMode) error { + expectedFilePath := filepath.Join("/mock/config/root", ".aws", "config") + if filename != expectedFilePath { + t.Errorf("Unexpected filename for osWriteFile: %s", filename) + } + // Additional checks on data can be added here if needed + return nil + } + + // Create a new AWSGenerator using the mock injector + generator := NewAWSGenerator(mocks.Injector) + + generator.Initialize() + + // Execute the Write method + err := generator.Write() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + }) + + t.Run("SuccessLocalstackEnabled", func(t *testing.T) { + mocks := setupSafeAwsGeneratorMocks() + + // Mock the GetBool method to return true for aws.localstack.enabled + mocks.MockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "aws.localstack.enabled" { + return true + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + + // Save the original iniSaveTo function + originalIniSaveTo := iniSaveTo + defer func() { + iniSaveTo = originalIniSaveTo + }() + + // Mock the iniSaveTo function to validate that it is called with the expected parameters + iniSaveTo = func(cfg *ini.File, filename string) error { + expectedFilePath := filepath.Join("/mock/config/root", ".aws", "config") + if filename != expectedFilePath { + t.Errorf("Unexpected filename for iniSaveTo: %s", filename) + } + // Additional checks on cfg can be added here if needed + return nil + } + + // Create a new AWSGenerator using the mock injector + generator := NewAWSGenerator(mocks.Injector) + + generator.Initialize() + + // Execute the Write method + err := generator.Write() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + }) + + t.Run("ErrorGettingConfigRoot", func(t *testing.T) { + mocks := setupSafeAwsGeneratorMocks() + + // Mock the GetConfigRoot method to return an error + mocks.MockConfigHandler.GetConfigRootFunc = func() (string, error) { + return "", fmt.Errorf("mocked error in GetConfigRoot") + } + + // Create a new AWSGenerator using the mock injector + generator := NewAWSGenerator(mocks.Injector) + + generator.Initialize() + + // Execute the Write method and expect an error + err := generator.Write() + if err == nil { + t.Fatalf("expected an error, got nil") + } + + expectedErrorMessage := "mocked error in GetConfigRoot" + if err.Error() != expectedErrorMessage { + t.Errorf("expected error message %q, got %q", expectedErrorMessage, err.Error()) + } + }) + + t.Run("ErrorCreatingDirectory", func(t *testing.T) { + mocks := setupSafeAwsGeneratorMocks() + + // Mock the GetConfigRoot method to return a valid path + mocks.MockConfigHandler.GetConfigRootFunc = func() (string, error) { + return filepath.Join("/mock/config/root"), nil + } + + // Mock the osStat function to simulate the file does not exist + osStat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + defer func() { osStat = os.Stat }() // Restore original function after test + + // Mock the osMkdirAll function to return an error + osMkdirAll = func(path string, perm os.FileMode) error { + return fmt.Errorf("mocked error in osMkdirAll") + } + defer func() { osMkdirAll = os.MkdirAll }() // Restore original function after test + + // Create a new AWSGenerator using the mock injector + generator := NewAWSGenerator(mocks.Injector) + + generator.Initialize() + + // Execute the Write method and expect an error + err := generator.Write() + if err == nil { + t.Fatalf("expected an error, got nil") + } + + expectedErrorMessage := "mocked error in osMkdirAll" + if err.Error() != expectedErrorMessage { + t.Errorf("expected error message %q, got %q", expectedErrorMessage, err.Error()) + } + }) + + t.Run("NoIniFile", func(t *testing.T) { + mocks := setupSafeAwsGeneratorMocks() + + // Mock the GetConfigRoot method to return a valid path + mocks.MockConfigHandler.GetConfigRootFunc = func() (string, error) { + return filepath.Join("/mock/config/root"), nil + } + + // Mock the osStat function to simulate the file exists + osStat = func(name string) (os.FileInfo, error) { + return nil, nil + } + defer func() { osStat = os.Stat }() // Restore original function after test + + // Flag to check if iniLoad was called + iniLoadCalled := false + + // Mock the iniLoad function to set the flag when called and return an error + originalIniLoad := iniLoad + iniLoad = func(_ interface{}, _ ...interface{}) (*ini.File, error) { + iniLoadCalled = true + return nil, fmt.Errorf("mocked error in iniLoad") + } + defer func() { iniLoad = originalIniLoad }() // Restore original shim after test + + // Create a new AWSGenerator using the mock injector + generator := NewAWSGenerator(mocks.Injector) + + generator.Initialize() + + // Execute the Write method + err := generator.Write() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + // Validate that iniLoad was called + if !iniLoadCalled { + t.Errorf("expected iniLoad to be called, but it was not") + } + }) + + t.Run("SuccessWithNonDefaultProfile", func(t *testing.T) { + mocks := setupSafeAwsGeneratorMocks() + + // Mock the GetConfigRoot method to return a valid path + mocks.MockConfigHandler.GetConfigRootFunc = func() (string, error) { + return filepath.Join("/mock/config/root"), nil + } + + // Mock the osStat function to simulate the file exists + osStat = func(name string) (os.FileInfo, error) { + return nil, nil + } + defer func() { osStat = os.Stat }() // Restore original function after test + + // Mock the iniLoad function to return an empty ini file + originalIniLoad := iniLoad + iniLoad = func(_ interface{}, _ ...interface{}) (*ini.File, error) { + return iniEmpty(), nil + } + defer func() { iniLoad = originalIniLoad }() // Restore original shim after test + + // Mock the iniSaveTo function to validate the region key is set correctly + originalIniSaveTo := iniSaveTo + iniSaveTo = func(cfg *ini.File, filename string) error { + expectedRegion := mocks.MockConfigHandler.GetString("aws.region", constants.DEFAULT_AWS_REGION) + sectionName := "profile non-default" + if cfg.Section(sectionName).Key("region").String() != expectedRegion { + t.Errorf("expected region %q, got %q", expectedRegion, cfg.Section(sectionName).Key("region").String()) + } + return nil + } + defer func() { iniSaveTo = originalIniSaveTo }() // Restore original shim after test + + // Mock the GetString method to return a non-default profile and a specific region + mocks.MockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "aws.profile" { + return "non-default" + } + if key == "aws.region" { + return "us-east-1" + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return "" + } + + // Create a new AWSGenerator using the mock injector + generator := NewAWSGenerator(mocks.Injector) + + generator.Initialize() + + // Execute the Write method + err := generator.Write() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + }) + + t.Run("FailedResolvingLocalstackService", func(t *testing.T) { + // Create a new mock injector + mockInjector := di.NewMockInjector() + + // Use setupSafeAwsGeneratorMocks to create mock components with the mock injector + mocks := setupSafeAwsGeneratorMocks(mockInjector) + + // Mock the GetBool method to simulate Localstack being enabled + mocks.MockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "aws.localstack.enabled" { + return true + } + if len(defaultValue) > 0 { + return defaultValue[0] + } + return false + } + + // Intentionally do not register the localstackService to simulate a resolution failure + mockInjector.SetResolveError("localstackService", fmt.Errorf("mocked error in Resolve")) + + // Create a new AWSGenerator using the mock injector + generator := NewAWSGenerator(mockInjector) + + generator.Initialize() + + // Execute the Write method and expect an error + err := generator.Write() + if err == nil { + t.Fatalf("expected error due to failed resolving of localstackService, got nil") + } + expectedError := "localstackService not found" + if err.Error() != expectedError { + t.Errorf("expected error %q, got %q", expectedError, err.Error()) + } + }) + + t.Run("ErrorSavingIniFile", func(t *testing.T) { + mocks := setupSafeAwsGeneratorMocks() + + // Mock the iniSaveTo function to return an error + originalIniSaveTo := iniSaveTo + defer func() { iniSaveTo = originalIniSaveTo }() // Ensure the original function is restored after the test + + iniSaveTo = func(cfg *ini.File, filename string) error { + return fmt.Errorf("mocked error in iniSaveTo") + } + + // Create a new AWSGenerator using the mock injector + generator := NewAWSGenerator(mocks.Injector) + + generator.Initialize() + + // Execute the Write method and expect an error + err := generator.Write() + if err == nil { + t.Fatalf("expected error due to iniSaveTo failure, got nil") + } + expectedError := "mocked error in iniSaveTo" + if err.Error() != expectedError { + t.Errorf("expected error %q, got %q", expectedError, err.Error()) + } + }) +} diff --git a/pkg/generators/shims.go b/pkg/generators/shims.go index 513802314..72bbd3a8e 100644 --- a/pkg/generators/shims.go +++ b/pkg/generators/shims.go @@ -4,6 +4,7 @@ import ( "os" "github.com/goccy/go-yaml" + "gopkg.in/ini.v1" ) // osWriteFile is a shim for os.WriteFile @@ -20,3 +21,14 @@ var osStat = os.Stat // yamlMarshal is a shim for yaml.Marshal var yamlMarshal = yaml.Marshal + +// iniLoad is a shim for ini.Load used in AWSGenerator +var iniLoad = ini.Load + +// iniEmpty is a shim for ini.Empty used in AWSGenerator +var iniEmpty = ini.Empty + +// iniSaveTo is a shim for cfg.SaveTo used in AWSGenerator +var iniSaveTo = func(cfg *ini.File, filename string) error { + return cfg.SaveTo(filename) +} diff --git a/pkg/services/localstack_service.go b/pkg/services/localstack_service.go index 116933401..b031c978d 100644 --- a/pkg/services/localstack_service.go +++ b/pkg/services/localstack_service.go @@ -1,7 +1,9 @@ package services import ( + "fmt" "os" + "strconv" "strings" "github.com/compose-spec/compose-go/types" @@ -24,31 +26,34 @@ func NewLocalstackService(injector di.Injector) *LocalstackService { } } -// GetComposeConfig returns the top-level compose configuration including a list of container data for docker-compose. +// GetComposeConfig constructs and returns a Docker Compose configuration for the Localstack service. +// It retrieves the context configuration, checks for a Localstack authentication token, and determines +// the appropriate image to use. It also gathers the list of Localstack services to enable, constructs +// the full domain name, and sets up the service configuration with environment variables, labels, and +// port settings. If an authentication token is present, it adds it to the service secrets. func (s *LocalstackService) GetComposeConfig() (*types.Config, error) { - // Get the context configuration contextConfig := s.configHandler.GetConfig() - - // Get the localstack auth token localstackAuthToken := os.Getenv("LOCALSTACK_AUTH_TOKEN") - // Get the image to use image := constants.DEFAULT_AWS_LOCALSTACK_IMAGE if localstackAuthToken != "" { image = constants.DEFAULT_AWS_LOCALSTACK_PRO_IMAGE } - // Get the localstack services to enable servicesList := "" if contextConfig.AWS.Localstack.Services != nil { servicesList = strings.Join(contextConfig.AWS.Localstack.Services, ",") } - // Get the domain from the configuration tld := s.configHandler.GetString("dns.domain", "test") fullName := s.name + "." + tld - // Create the service config + port, err := strconv.ParseUint(constants.DEFAULT_AWS_LOCALSTACK_PORT, 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid port format: %w", err) + } + port32 := uint32(port) + services := []types.ServiceConfig{ { Name: fullName, @@ -63,14 +68,19 @@ func (s *LocalstackService) GetComposeConfig() (*types.Config, error) { "SERVICES": ptrString(servicesList), }, Labels: map[string]string{ - "role": "localstack", + "role": "aws", "managed_by": "windsor", - "wildcard": "true", + }, + Ports: []types.ServicePortConfig{ + { + Target: port32, + Published: constants.DEFAULT_AWS_LOCALSTACK_PORT, + Protocol: "tcp", + }, }, }, } - // If the localstack auth token is set, add it to the environment if localstackAuthToken != "" { services[0].Environment["LOCALSTACK_AUTH_TOKEN"] = ptrString("${LOCALSTACK_AUTH_TOKEN}") }