From 217cffdd7b5d9d047aef0603f531fc562e5c7439 Mon Sep 17 00:00:00 2001 From: rmvangun <85766511+rmvangun@users.noreply.github.com> Date: Wed, 10 Dec 2025 00:43:21 -0500 Subject: [PATCH 01/14] feat(terraform): Generate kubelogin spn auth override for Azure AKS (#1988) Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/composer/composer.go | 1 + pkg/composer/composer_test.go | 2 +- pkg/constants/constants.go | 10 + pkg/provisioner/terraform/stack.go | 4 + pkg/provisioner/terraform/stack_test.go | 26 ++ pkg/runtime/env/terraform_env.go | 91 +++++- pkg/runtime/env/terraform_env_test.go | 382 ++++++++++++++++++++++++ pkg/runtime/tools/tools_manager.go | 47 +++ pkg/runtime/tools/tools_manager_test.go | 215 +++++++++++++ 9 files changed, 776 insertions(+), 2 deletions(-) diff --git a/pkg/composer/composer.go b/pkg/composer/composer.go index dd16b51af..ec06f8339 100644 --- a/pkg/composer/composer.go +++ b/pkg/composer/composer.go @@ -176,6 +176,7 @@ func (r *Composer) generateGitignore() error { ".windsor/", ".volumes/", "terraform/**/backend_override.tf", + "terraform/**/providers_override.tf", "contexts/**/.kube/", "contexts/**/.talos/", "contexts/**/.omni/", diff --git a/pkg/composer/composer_test.go b/pkg/composer/composer_test.go index 75a4794bd..12339eccc 100644 --- a/pkg/composer/composer_test.go +++ b/pkg/composer/composer_test.go @@ -1114,7 +1114,7 @@ func TestComposer_generateGitignore(t *testing.T) { } contentStr := string(content) - requiredEntries := []string{".windsor/", ".volumes/", "terraform/**/backend_override.tf"} + requiredEntries := []string{".windsor/", ".volumes/", "terraform/**/backend_override.tf", "terraform/**/providers_override.tf"} for _, entry := range requiredEntries { if !strings.Contains(contentStr, entry) { t.Errorf("Expected .gitignore to contain %s", entry) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 136f9a301..b9c0d762c 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -112,6 +112,16 @@ const MinimumVersion1Password = "2.15.0" const MinimumVersionAWSCLI = "2.15.0" +const MinimumVersionKubelogin = "0.1.7" + +// DefaultAKSOIDCServerID is the standard Azure AKS OIDC server ID (application ID of the +// Microsoft-managed enterprise application "Azure Kubernetes Service AAD Server"). +// This is the same for all AKS clusters with AKS-managed Azure AD enabled. +const DefaultAKSOIDCServerID = "6dae42f8-4368-4678-94ff-3960e28e3630" + +// DefaultAKSOIDCClientID is the standard Azure AKS OIDC client ID used for all AKS clusters. +const DefaultAKSOIDCClientID = "80faf920-1908-4b52-b5ef-a8e7bedfc67a" + const DefaultNodeHealthCheckTimeout = 5 * time.Minute const DefaultNodeHealthCheckPollInterval = 10 * time.Second diff --git a/pkg/provisioner/terraform/stack.go b/pkg/provisioner/terraform/stack.go index 3d8517cb2..25aef2fd5 100644 --- a/pkg/provisioner/terraform/stack.go +++ b/pkg/provisioner/terraform/stack.go @@ -247,6 +247,10 @@ func (s *TerraformStack) Down(blueprint *blueprintv1alpha1.Blueprint) error { if err := s.shims.Remove(filepath.Join(component.FullPath, "backend_override.tf")); err != nil && !os.IsNotExist(err) { return fmt.Errorf("error removing backend_override.tf from %s: %w", component.Path, err) } + + if err := s.shims.Remove(filepath.Join(component.FullPath, "providers_override.tf")); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("error removing providers_override.tf from %s: %w", component.Path, err) + } } return nil diff --git a/pkg/provisioner/terraform/stack_test.go b/pkg/provisioner/terraform/stack_test.go index 0a328604b..94d4116ea 100644 --- a/pkg/provisioner/terraform/stack_test.go +++ b/pkg/provisioner/terraform/stack_test.go @@ -697,6 +697,32 @@ func TestStack_Down(t *testing.T) { t.Errorf("Expected remove error, got: %v", err) } }) + + t.Run("ErrorRemovingProvidersOverride", func(t *testing.T) { + stack, mocks := setup(t) + projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") + providersOverridePath := filepath.Join(projectRoot, ".windsor", "contexts", "local", "remote", "path", "providers_override.tf") + if err := os.MkdirAll(filepath.Dir(providersOverridePath), 0755); err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + if err := os.WriteFile(providersOverridePath, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create providers override file: %v", err) + } + mocks.Shims.Remove = func(path string) error { + if strings.Contains(path, "providers_override.tf") { + return fmt.Errorf("remove error") + } + return nil + } + blueprint := createTestBlueprint() + err := stack.Down(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error removing providers_override.tf") { + t.Errorf("Expected remove error, got: %v", err) + } + }) } func TestNewShims(t *testing.T) { diff --git a/pkg/runtime/env/terraform_env.go b/pkg/runtime/env/terraform_env.go index 457e16273..356e5ff21 100644 --- a/pkg/runtime/env/terraform_env.go +++ b/pkg/runtime/env/terraform_env.go @@ -15,6 +15,7 @@ import ( "github.com/goccy/go-yaml" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" + "github.com/windsorcli/cli/pkg/constants" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -106,7 +107,10 @@ func (e *TerraformEnvPrinter) GetEnvVars() (map[string]string, error) { // PostEnvHook executes operations after setting the environment variables. func (e *TerraformEnvPrinter) PostEnvHook(directory ...string) error { - return e.generateBackendOverrideTf(directory...) + if err := e.generateBackendOverrideTf(directory...); err != nil { + return err + } + return e.generateProvidersOverrideTf(directory...) } // GenerateTerraformArgs constructs Terraform CLI arguments and environment variables for given project and module paths. @@ -492,6 +496,91 @@ func (e *TerraformEnvPrinter) generateBackendOverrideTf(directory ...string) err return nil } +// generateProvidersOverrideTf creates a providers_override.tf file when using Azure + AKS +// to configure Kubernetes provider authentication via Entra ID using kubelogin with SPN authentication. +// Detects SPN mode when AZURE_CLIENT_SECRET is set and validates required environment variables. +// This enables generic Kubernetes modules to work with AKS clusters using Entra AD authentication +// without requiring provider blocks in the modules themselves. +func (e *TerraformEnvPrinter) generateProvidersOverrideTf(directory ...string) error { + var currentPath string + if len(directory) > 0 { + currentPath = filepath.Clean(directory[0]) + } else { + var err error + currentPath, err = e.shims.Getwd() + if err != nil { + return fmt.Errorf("error getting current directory: %w", err) + } + } + + projectPath, err := e.findRelativeTerraformProjectPath(directory...) + if err != nil { + return fmt.Errorf("error finding project path: %w", err) + } + + if projectPath == "" { + return nil + } + + azureEnabled := e.configHandler.GetBool("azure.enabled", false) + clusterDriver := e.configHandler.GetString("cluster.driver", "") + + if !azureEnabled || clusterDriver != "aks" { + providersOverridePath := filepath.Join(currentPath, "providers_override.tf") + if _, err := e.shims.Stat(providersOverridePath); err == nil { + if err := e.shims.Remove(providersOverridePath); err != nil { + return fmt.Errorf("error removing providers_override.tf: %w", err) + } + } + return nil + } + + config := e.configHandler.GetConfig() + if config == nil || config.Azure == nil { + return nil + } + + azureEnv := "AzurePublicCloud" + if config.Azure.Environment != nil { + azureEnv = *config.Azure.Environment + } + + azureClientSecret := e.shims.Getenv("AZURE_CLIENT_SECRET") + + if azureClientSecret == "" { + providersOverridePath := filepath.Join(currentPath, "providers_override.tf") + if _, err := e.shims.Stat(providersOverridePath); err == nil { + if err := e.shims.Remove(providersOverridePath); err != nil { + return fmt.Errorf("error removing providers_override.tf: %w", err) + } + } + return nil + } + + providerConfig := fmt.Sprintf(`provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "kubelogin" + args = [ + "get-token", + "--login", "spn", + "--environment", "%s", + "--server-id", "%s", + ] + } +} +`, azureEnv, constants.DefaultAKSOIDCServerID) + + providersOverridePath := filepath.Join(currentPath, "providers_override.tf") + + err = e.shims.WriteFile(providersOverridePath, []byte(providerConfig), os.ModePerm) + if err != nil { + return fmt.Errorf("error writing providers_override.tf: %w", err) + } + + return nil +} + // generateBackendConfigArgs constructs backend config args for terraform commands. // It reads the backend type from the config and adds relevant key-value pairs. // The function supports local, s3, kubernetes, and azurerm backends. diff --git a/pkg/runtime/env/terraform_env_test.go b/pkg/runtime/env/terraform_env_test.go index 5bb6d471d..6f768a7e0 100644 --- a/pkg/runtime/env/terraform_env_test.go +++ b/pkg/runtime/env/terraform_env_test.go @@ -10,6 +10,9 @@ import ( "testing" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" + v1alpha1 "github.com/windsorcli/cli/api/v1alpha1" + "github.com/windsorcli/cli/api/v1alpha1/azure" + "github.com/windsorcli/cli/pkg/constants" "github.com/windsorcli/cli/pkg/runtime/config" ) @@ -915,6 +918,385 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { }) } +func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupTerraformEnvMocks(t) + printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("Success", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure + AKS enabled and AZURE_CLIENT_SECRET set + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + mocks.Shims.Getenv = func(key string) string { + if key == "AZURE_CLIENT_SECRET" { + return "test-secret" + } + return "" + } + + // Mock WriteFile to capture the output + var writtenData []byte + mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { + writtenData = data + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur and the expected provider config should be written + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + expectedContent := fmt.Sprintf(`provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "kubelogin" + args = [ + "get-token", + "--login", "spn", + "--environment", "AzurePublicCloud", + "--server-id", "%s", + ] + } +} +`, constants.DefaultAKSOIDCServerID) + if string(writtenData) != expectedContent { + t.Errorf("Expected provider config %q, got %q", expectedContent, string(writtenData)) + } + }) + + t.Run("AzureNotEnabled", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure disabled + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", false) + mocks.ConfigHandler.Set("cluster.driver", "aks") + + // Mock Stat and Remove to verify file deletion + fileExists := true + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "providers_override.tf") { + if fileExists { + return nil, nil + } + return nil, os.ErrNotExist + } + return nil, os.ErrNotExist + } + + var fileRemoved bool + mocks.Shims.Remove = func(name string) error { + if strings.Contains(name, "providers_override.tf") { + fileRemoved = true + fileExists = false + return nil + } + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur and the file should be removed + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if !fileRemoved { + t.Error("Expected providers_override.tf to be removed") + } + }) + + t.Run("NotAKS", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure enabled but not AKS + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "eks") + + // Mock Stat and Remove to verify file deletion + fileExists := true + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "providers_override.tf") { + if fileExists { + return nil, nil + } + return nil, os.ErrNotExist + } + return nil, os.ErrNotExist + } + + var fileRemoved bool + mocks.Shims.Remove = func(name string) error { + if strings.Contains(name, "providers_override.tf") { + fileRemoved = true + fileExists = false + return nil + } + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur and the file should be removed + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if !fileRemoved { + t.Error("Expected providers_override.tf to be removed") + } + }) + + t.Run("NoAzureConfig", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure enabled but no Azure config + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + + // Mock GetConfig to return nil + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return nil + } + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "azure.enabled" { + return true + } + return false + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "aks" + } + return "" + } + printer.configHandler = mockConfigHandler + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("AZURE_CLIENT_SECRETNotSet", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure + AKS enabled but no AZURE_CLIENT_SECRET + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + mocks.Shims.Getenv = func(key string) string { + return "" + } + + // Mock Stat and Remove to verify file deletion + fileExists := true + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "providers_override.tf") { + if fileExists { + return nil, nil + } + return nil, os.ErrNotExist + } + return nil, os.ErrNotExist + } + + var fileRemoved bool + mocks.Shims.Remove = func(name string) error { + if strings.Contains(name, "providers_override.tf") { + fileRemoved = true + fileExists = false + return nil + } + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur and the file should be removed + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if !fileRemoved { + t.Error("Expected providers_override.tf to be removed") + } + }) + + t.Run("CustomAzureEnvironment", func(t *testing.T) { + // Given a TerraformEnvPrinter with custom Azure environment + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + mocks.Shims.Getenv = func(key string) string { + if key == "AZURE_CLIENT_SECRET" { + return "test-secret" + } + return "" + } + + // Mock config with custom environment + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "azure.enabled" { + return true + } + return false + } + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "aks" + } + return "" + } + azureEnv := "AzureUSGovernment" + mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + return &v1alpha1.Context{ + Azure: &azure.AzureConfig{ + Environment: &azureEnv, + }, + } + } + printer.configHandler = mockConfigHandler + + // Mock WriteFile to capture the output + var writtenData []byte + mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { + writtenData = data + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur and the expected provider config should be written with custom environment + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + expectedContent := fmt.Sprintf(`provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "kubelogin" + args = [ + "get-token", + "--login", "spn", + "--environment", "AzureUSGovernment", + "--server-id", "%s", + ] + } +} +`, constants.DefaultAKSOIDCServerID) + if string(writtenData) != expectedContent { + t.Errorf("Expected provider config %q, got %q", expectedContent, string(writtenData)) + } + }) + + t.Run("NoProjectPath", func(t *testing.T) { + // Given a TerraformEnvPrinter with no Terraform project path + printer, mocks := setup(t) + mocks.Shims.Glob = func(pattern string) ([]string, error) { + return nil, nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("ErrorGettingCurrentDirectory", func(t *testing.T) { + // Given a TerraformEnvPrinter with failing Getwd + printer, mocks := setup(t) + mocks.Shims.Getwd = func() (string, error) { + return "", fmt.Errorf("mock error getting current directory") + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then an error should be returned + if err == nil { + t.Errorf("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error getting current directory") { + t.Errorf("Expected error message to contain 'error getting current directory', got %v", err) + } + }) + + t.Run("ErrorWritingFile", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure + AKS enabled and AZURE_CLIENT_SECRET set + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + mocks.Shims.Getenv = func(key string) string { + if key == "AZURE_CLIENT_SECRET" { + return "test-secret" + } + return "" + } + + // Mock WriteFile to return error + mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { + return fmt.Errorf("mock error writing file") + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then an error should be returned + if err == nil { + t.Errorf("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error writing providers_override.tf") { + t.Errorf("Expected error message to contain 'error writing providers_override.tf', got %v", err) + } + }) + + t.Run("ErrorRemovingFile", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure not enabled and existing file + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", false) + mocks.ConfigHandler.Set("cluster.driver", "aks") + + // Mock Stat to return file exists + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, "providers_override.tf") { + return nil, nil + } + return nil, os.ErrNotExist + } + + // Mock Remove to return error + mocks.Shims.Remove = func(name string) error { + if strings.Contains(name, "providers_override.tf") { + return fmt.Errorf("mock error removing file") + } + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then an error should be returned + if err == nil { + t.Errorf("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error removing providers_override.tf") { + t.Errorf("Expected error message to contain 'error removing providers_override.tf', got %v", err) + } + }) +} + func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { t.Helper() diff --git a/pkg/runtime/tools/tools_manager.go b/pkg/runtime/tools/tools_manager.go index a531f8ac8..e8af66e43 100644 --- a/pkg/runtime/tools/tools_manager.go +++ b/pkg/runtime/tools/tools_manager.go @@ -101,6 +101,14 @@ func (t *BaseToolsManager) Check() error { } } + if t.configHandler.GetBool("azure.enabled") { + if err := t.checkKubelogin(); err != nil { + spin.Stop() + fmt.Fprintf(os.Stderr, "\033[31m✗ %s - Failed\033[0m\n", message) + return fmt.Errorf("kubelogin check failed: %v", err) + } + } + spin.Stop() fmt.Fprintf(os.Stderr, "\033[32m✔\033[0m %s - \033[32mDone\033[0m\n", message) return nil @@ -227,6 +235,45 @@ func (t *BaseToolsManager) checkOnePassword() error { return nil } +// checkKubelogin ensures kubelogin is available in the system's PATH using execLookPath. +// It checks for 'kubelogin' in the system's PATH, verifies its version, and validates +// required environment variables for SPN authentication if AZURE_CLIENT_SECRET is set. +// Returns nil if found and meets the minimum version requirement, else an error indicating it is not available or outdated. +func (t *BaseToolsManager) checkKubelogin() error { + if _, err := execLookPath("kubelogin"); err != nil { + return fmt.Errorf("kubelogin is not available in the PATH") + } + + out, err := t.shell.ExecSilent("kubelogin", "--version") + if err != nil { + return fmt.Errorf("kubelogin is not available in the PATH") + } + + version := extractVersion(out) + if version == "" { + return fmt.Errorf("failed to extract kubelogin version") + } + + if compareVersion(version, constants.MinimumVersionKubelogin) < 0 { + return fmt.Errorf("kubelogin version %s is below the minimum required version %s", version, constants.MinimumVersionKubelogin) + } + + azureClientSecret := os.Getenv("AZURE_CLIENT_SECRET") + if azureClientSecret != "" { + azureClientID := os.Getenv("AZURE_CLIENT_ID") + azureTenantID := os.Getenv("AZURE_TENANT_ID") + + if azureClientID == "" { + return fmt.Errorf("AZURE_CLIENT_SECRET is set but AZURE_CLIENT_ID is missing - both are required for SPN authentication") + } + if azureTenantID == "" { + return fmt.Errorf("AZURE_CLIENT_SECRET is set but AZURE_TENANT_ID is missing - both are required for SPN authentication") + } + } + + return nil +} + // compareVersion is a helper function to compare two version strings. // It returns -1 if version1 < version2, 0 if version1 == version2, and 1 if version1 > version2. func compareVersion(version1, version2 string) int { diff --git a/pkg/runtime/tools/tools_manager_test.go b/pkg/runtime/tools/tools_manager_test.go index 36adad75c..4e46ebdf7 100644 --- a/pkg/runtime/tools/tools_manager_test.go +++ b/pkg/runtime/tools/tools_manager_test.go @@ -860,6 +860,221 @@ func TestToolsManager_checkOnePassword(t *testing.T) { }) } +// Tests for kubelogin version validation +func TestToolsManager_checkKubelogin(t *testing.T) { + setup := func(t *testing.T) (*Mocks, *BaseToolsManager) { + t.Helper() + mocks := setupMocks(t) + toolsManager := NewToolsManager(mocks.ConfigHandler, mocks.Shell) + return mocks, toolsManager + } + + t.Run("Success", func(t *testing.T) { + // Given kubelogin is available with correct version + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return fmt.Sprintf("kubelogin version %s", constants.MinimumVersionKubelogin), nil + } + return "", fmt.Errorf("command not found") + } + // When checking kubelogin version + err := toolsManager.checkKubelogin() + // Then no error should be returned + if err != nil { + t.Errorf("Expected checkKubelogin to succeed, but got error: %v", err) + } + }) + + t.Run("KubeloginNotAvailable", func(t *testing.T) { + // Given kubelogin is not found in PATH + _, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "", exec.ErrNotFound + } + return originalExecLookPath(name) + } + // When checking kubelogin version + err := toolsManager.checkKubelogin() + // Then an error indicating kubelogin is not available should be returned + if err == nil || !strings.Contains(err.Error(), "kubelogin is not available in the PATH") { + t.Errorf("Expected kubelogin not available error, got %v", err) + } + }) + + t.Run("KubeloginVersionInvalidResponse", func(t *testing.T) { + // Given kubelogin version response is invalid + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return "Invalid version response", nil + } + return "", fmt.Errorf("command not found") + } + // When checking kubelogin version + err := toolsManager.checkKubelogin() + // Then an error indicating version extraction failed should be returned + if err == nil || !strings.Contains(err.Error(), "failed to extract kubelogin version") { + t.Errorf("Expected failed to extract kubelogin version error, got %v", err) + } + }) + + t.Run("KubeloginVersionTooLow", func(t *testing.T) { + // Given kubelogin version is below minimum required version + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return "kubelogin version 0.1.0", nil + } + return "", fmt.Errorf("command not found") + } + // When checking kubelogin version + err := toolsManager.checkKubelogin() + // Then an error indicating version is too low should be returned + if err == nil || !strings.Contains(err.Error(), "kubelogin version 0.1.0 is below the minimum required version") { + t.Errorf("Expected kubelogin version too low error, got %v", err) + } + }) + + t.Run("KubeloginCommandError", func(t *testing.T) { + // Given kubelogin command execution fails + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return "", fmt.Errorf("kubelogin is not available in the PATH") + } + return "", fmt.Errorf("command not found") + } + // When checking kubelogin version + err := toolsManager.checkKubelogin() + // Then an error indicating kubelogin is not available should be returned + if err == nil || !strings.Contains(err.Error(), "kubelogin is not available in the PATH") { + t.Errorf("Expected kubelogin is not available in the PATH error, got %v", err) + } + }) + + t.Run("AZURE_CLIENT_SECRETSetButAZURE_CLIENT_IDMissing", func(t *testing.T) { + // Given AZURE_CLIENT_SECRET is set but AZURE_CLIENT_ID is missing + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return fmt.Sprintf("kubelogin version %s", constants.MinimumVersionKubelogin), nil + } + return "", fmt.Errorf("command not found") + } + os.Setenv("AZURE_CLIENT_SECRET", "test-secret") + defer os.Unsetenv("AZURE_CLIENT_SECRET") + os.Unsetenv("AZURE_CLIENT_ID") + os.Unsetenv("AZURE_TENANT_ID") + // When checking kubelogin + err := toolsManager.checkKubelogin() + // Then an error indicating AZURE_CLIENT_ID is missing should be returned + if err == nil || !strings.Contains(err.Error(), "AZURE_CLIENT_SECRET is set but AZURE_CLIENT_ID is missing") { + t.Errorf("Expected AZURE_CLIENT_ID missing error, got %v", err) + } + }) + + t.Run("AZURE_CLIENT_SECRETSetButAZURE_TENANT_IDMissing", func(t *testing.T) { + // Given AZURE_CLIENT_SECRET is set but AZURE_TENANT_ID is missing + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return fmt.Sprintf("kubelogin version %s", constants.MinimumVersionKubelogin), nil + } + return "", fmt.Errorf("command not found") + } + os.Setenv("AZURE_CLIENT_SECRET", "test-secret") + os.Setenv("AZURE_CLIENT_ID", "test-client-id") + defer func() { + os.Unsetenv("AZURE_CLIENT_SECRET") + os.Unsetenv("AZURE_CLIENT_ID") + }() + os.Unsetenv("AZURE_TENANT_ID") + // When checking kubelogin + err := toolsManager.checkKubelogin() + // Then an error indicating AZURE_TENANT_ID is missing should be returned + if err == nil || !strings.Contains(err.Error(), "AZURE_CLIENT_SECRET is set but AZURE_TENANT_ID is missing") { + t.Errorf("Expected AZURE_TENANT_ID missing error, got %v", err) + } + }) + + t.Run("AZURE_CLIENT_SECRETSetWithAllRequiredVars", func(t *testing.T) { + // Given AZURE_CLIENT_SECRET is set with all required environment variables + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return fmt.Sprintf("kubelogin version %s", constants.MinimumVersionKubelogin), nil + } + return "", fmt.Errorf("command not found") + } + os.Setenv("AZURE_CLIENT_SECRET", "test-secret") + os.Setenv("AZURE_CLIENT_ID", "test-client-id") + os.Setenv("AZURE_TENANT_ID", "test-tenant-id") + defer func() { + os.Unsetenv("AZURE_CLIENT_SECRET") + os.Unsetenv("AZURE_CLIENT_ID") + os.Unsetenv("AZURE_TENANT_ID") + }() + // When checking kubelogin + err := toolsManager.checkKubelogin() + // Then no error should be returned + if err != nil { + t.Errorf("Expected checkKubelogin to succeed with all required env vars, but got error: %v", err) + } + }) +} + // ============================================================================= // Test Helpers // ============================================================================= From bdf6537824aa18a0b6b5fb0fa6760ad9f882dae0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 11:12:43 +0000 Subject: [PATCH 02/14] chore(deps): update coredns/coredns docker tag to v1.13.2 (#1989) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pkg/constants/constants.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index b9c0d762c..469e9cf6b 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -83,7 +83,7 @@ const DefaultAWSLocalstackImage = "localstack/localstack:4.11.1" const DefaultAWSLocalstackProImage = "localstack/localstack-pro:4.11.1" // renovate: datasource=docker depName=coredns/coredns -const DefaultDNSImage = "coredns/coredns:1.13.1" +const DefaultDNSImage = "coredns/coredns:1.13.2" // renovate: datasource=docker depName=registry const RegistryDefaultImage = "registry:3.0.0" From 36eab1b864f865dc118bd359ef05ecaeb3b62ce6 Mon Sep 17 00:00:00 2001 From: rmvangun <85766511+rmvangun@users.noreply.github.com> Date: Wed, 10 Dec 2025 09:39:23 -0500 Subject: [PATCH 03/14] feat(terraform): Add more auth modes for AKS (#1992) Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/provisioner/terraform/stack.go | 7 + pkg/provisioner/terraform/stack_test.go | 26 ++++ pkg/runtime/env/terraform_env.go | 20 +-- pkg/runtime/env/terraform_env_test.go | 182 ++++++++++++++++++++---- pkg/runtime/tools/tools_manager.go | 29 ++-- pkg/runtime/tools/tools_manager_test.go | 59 ++++++++ 6 files changed, 279 insertions(+), 44 deletions(-) diff --git a/pkg/provisioner/terraform/stack.go b/pkg/provisioner/terraform/stack.go index 25aef2fd5..41804b49d 100644 --- a/pkg/provisioner/terraform/stack.go +++ b/pkg/provisioner/terraform/stack.go @@ -159,6 +159,13 @@ func (s *TerraformStack) Up(blueprint *blueprintv1alpha1.Blueprint) error { return fmt.Errorf("error removing backend override file for %s: %w", component.Path, err) } } + + providersOverridePath := filepath.Join(component.FullPath, "providers_override.tf") + if _, err := s.shims.Stat(providersOverridePath); err == nil { + if err := s.shims.Remove(providersOverridePath); err != nil { + return fmt.Errorf("error removing providers override file for %s: %w", component.Path, err) + } + } } return nil diff --git a/pkg/provisioner/terraform/stack_test.go b/pkg/provisioner/terraform/stack_test.go index 94d4116ea..42510f0a8 100644 --- a/pkg/provisioner/terraform/stack_test.go +++ b/pkg/provisioner/terraform/stack_test.go @@ -444,6 +444,32 @@ func TestStack_Up(t *testing.T) { t.Errorf("Expected remove error, got: %v", err) } }) + + t.Run("ErrorRemovingProvidersOverride", func(t *testing.T) { + stack, mocks := setup(t) + projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") + providersOverridePath := filepath.Join(projectRoot, ".windsor", "contexts", "local", "remote", "path", "providers_override.tf") + if err := os.MkdirAll(filepath.Dir(providersOverridePath), 0755); err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + if err := os.WriteFile(providersOverridePath, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create providers override file: %v", err) + } + mocks.Shims.Remove = func(path string) error { + if strings.Contains(path, "providers_override.tf") { + return fmt.Errorf("remove error") + } + return nil + } + blueprint := createTestBlueprint() + err := stack.Up(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error removing providers override file") { + t.Errorf("Expected remove error, got: %v", err) + } + }) } func TestStack_Down(t *testing.T) { diff --git a/pkg/runtime/env/terraform_env.go b/pkg/runtime/env/terraform_env.go index 356e5ff21..125f87be8 100644 --- a/pkg/runtime/env/terraform_env.go +++ b/pkg/runtime/env/terraform_env.go @@ -546,15 +546,15 @@ func (e *TerraformEnvPrinter) generateProvidersOverrideTf(directory ...string) e } azureClientSecret := e.shims.Getenv("AZURE_CLIENT_SECRET") + azureFederatedTokenFile := e.shims.Getenv("AZURE_FEDERATED_TOKEN_FILE") - if azureClientSecret == "" { - providersOverridePath := filepath.Join(currentPath, "providers_override.tf") - if _, err := e.shims.Stat(providersOverridePath); err == nil { - if err := e.shims.Remove(providersOverridePath); err != nil { - return fmt.Errorf("error removing providers_override.tf: %w", err) - } - } - return nil + var loginMode string + if azureFederatedTokenFile != "" { + loginMode = "workloadidentity" + } else if azureClientSecret != "" { + loginMode = "spn" + } else { + loginMode = "azurecli" } providerConfig := fmt.Sprintf(`provider "kubernetes" { @@ -563,13 +563,13 @@ func (e *TerraformEnvPrinter) generateProvidersOverrideTf(directory ...string) e command = "kubelogin" args = [ "get-token", - "--login", "spn", + "--login", "%s", "--environment", "%s", "--server-id", "%s", ] } } -`, azureEnv, constants.DefaultAKSOIDCServerID) +`, loginMode, azureEnv, constants.DefaultAKSOIDCServerID) providersOverridePath := filepath.Join(currentPath, "providers_override.tf") diff --git a/pkg/runtime/env/terraform_env_test.go b/pkg/runtime/env/terraform_env_test.go index 6f768a7e0..d528c3edd 100644 --- a/pkg/runtime/env/terraform_env_test.go +++ b/pkg/runtime/env/terraform_env_test.go @@ -927,7 +927,100 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { return printer, mocks } - t.Run("Success", func(t *testing.T) { + t.Run("SuccessWithWorkloadIdentity", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure + AKS enabled and AZURE_FEDERATED_TOKEN_FILE set + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + mocks.Shims.Getenv = func(key string) string { + if key == "AZURE_FEDERATED_TOKEN_FILE" { + return "/path/to/token/file" + } + return "" + } + + // Mock WriteFile to capture the output + var writtenData []byte + mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { + writtenData = data + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur and the expected provider config with Workload Identity should be written + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + expectedContent := fmt.Sprintf(`provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "kubelogin" + args = [ + "get-token", + "--login", "workloadidentity", + "--environment", "AzurePublicCloud", + "--server-id", "%s", + ] + } +} +`, constants.DefaultAKSOIDCServerID) + if string(writtenData) != expectedContent { + t.Errorf("Expected provider config %q, got %q", expectedContent, string(writtenData)) + } + }) + + t.Run("WorkloadIdentityPriorityOverSPN", func(t *testing.T) { + // Given a TerraformEnvPrinter with both AZURE_FEDERATED_TOKEN_FILE and AZURE_CLIENT_SECRET set + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + mocks.Shims.Getenv = func(key string) string { + if key == "AZURE_FEDERATED_TOKEN_FILE" { + return "/path/to/token/file" + } + if key == "AZURE_CLIENT_SECRET" { + return "test-secret" + } + return "" + } + + // Mock WriteFile to capture the output + var writtenData []byte + mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { + writtenData = data + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur and Workload Identity should be used (higher priority) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + expectedContent := fmt.Sprintf(`provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "kubelogin" + args = [ + "get-token", + "--login", "workloadidentity", + "--environment", "AzurePublicCloud", + "--server-id", "%s", + ] + } +} +`, constants.DefaultAKSOIDCServerID) + if string(writtenData) != expectedContent { + t.Errorf("Expected provider config %q, got %q", expectedContent, string(writtenData)) + } + }) + + t.Run("SuccessWithSPN", func(t *testing.T) { // Given a TerraformEnvPrinter with Azure + AKS enabled and AZURE_CLIENT_SECRET set printer, mocks := setup(t) mocks.ConfigHandler.Set("azure.enabled", true) @@ -949,7 +1042,7 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { // When generateProvidersOverrideTf is called err := printer.generateProvidersOverrideTf() - // Then no error should occur and the expected provider config should be written + // Then no error should occur and the expected provider config with SPN should be written if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -972,6 +1065,48 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { } }) + t.Run("SuccessWithAzureCLI", func(t *testing.T) { + // Given a TerraformEnvPrinter with Azure + AKS enabled but no AZURE_CLIENT_SECRET (fallback to Azure CLI) + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + mocks.Shims.Getenv = func(key string) string { + return "" + } + + // Mock WriteFile to capture the output + var writtenData []byte + mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { + writtenData = data + return nil + } + + // When generateProvidersOverrideTf is called + err := printer.generateProvidersOverrideTf() + + // Then no error should occur and the expected provider config with azurecli should be written + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + expectedContent := fmt.Sprintf(`provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "kubelogin" + args = [ + "get-token", + "--login", "azurecli", + "--environment", "AzurePublicCloud", + "--server-id", "%s", + ] + } +} +`, constants.DefaultAKSOIDCServerID) + if string(writtenData) != expectedContent { + t.Errorf("Expected provider config %q, got %q", expectedContent, string(writtenData)) + } + }) + t.Run("AzureNotEnabled", func(t *testing.T) { // Given a TerraformEnvPrinter with Azure disabled printer, mocks := setup(t) @@ -1095,37 +1230,36 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { return "" } - // Mock Stat and Remove to verify file deletion - fileExists := true - mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if strings.Contains(name, "providers_override.tf") { - if fileExists { - return nil, nil - } - return nil, os.ErrNotExist - } - return nil, os.ErrNotExist - } - - var fileRemoved bool - mocks.Shims.Remove = func(name string) error { - if strings.Contains(name, "providers_override.tf") { - fileRemoved = true - fileExists = false - return nil - } + // Mock WriteFile to capture the output + var writtenData []byte + mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { + writtenData = data return nil } // When generateProvidersOverrideTf is called err := printer.generateProvidersOverrideTf() - // Then no error should occur and the file should be removed + // Then no error should occur and the expected provider config with azurecli should be written if err != nil { t.Errorf("Expected no error, got %v", err) } - if !fileRemoved { - t.Error("Expected providers_override.tf to be removed") + + expectedContent := fmt.Sprintf(`provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "kubelogin" + args = [ + "get-token", + "--login", "azurecli", + "--environment", "AzurePublicCloud", + "--server-id", "%s", + ] + } +} +`, constants.DefaultAKSOIDCServerID) + if string(writtenData) != expectedContent { + t.Errorf("Expected provider config %q, got %q", expectedContent, string(writtenData)) } }) diff --git a/pkg/runtime/tools/tools_manager.go b/pkg/runtime/tools/tools_manager.go index e8af66e43..9c550d342 100644 --- a/pkg/runtime/tools/tools_manager.go +++ b/pkg/runtime/tools/tools_manager.go @@ -258,16 +258,25 @@ func (t *BaseToolsManager) checkKubelogin() error { return fmt.Errorf("kubelogin version %s is below the minimum required version %s", version, constants.MinimumVersionKubelogin) } - azureClientSecret := os.Getenv("AZURE_CLIENT_SECRET") - if azureClientSecret != "" { - azureClientID := os.Getenv("AZURE_CLIENT_ID") - azureTenantID := os.Getenv("AZURE_TENANT_ID") - - if azureClientID == "" { - return fmt.Errorf("AZURE_CLIENT_SECRET is set but AZURE_CLIENT_ID is missing - both are required for SPN authentication") - } - if azureTenantID == "" { - return fmt.Errorf("AZURE_CLIENT_SECRET is set but AZURE_TENANT_ID is missing - both are required for SPN authentication") + validationRules := []struct { + triggerVar string + authMethod string + }{ + {"AZURE_FEDERATED_TOKEN_FILE", "Workload Identity"}, + {"AZURE_CLIENT_SECRET", "SPN"}, + } + + for _, rule := range validationRules { + if os.Getenv(rule.triggerVar) != "" { + azureClientID := os.Getenv("AZURE_CLIENT_ID") + azureTenantID := os.Getenv("AZURE_TENANT_ID") + + if azureClientID == "" { + return fmt.Errorf("%s is set but AZURE_CLIENT_ID is missing - both are required for %s authentication", rule.triggerVar, rule.authMethod) + } + if azureTenantID == "" { + return fmt.Errorf("%s is set but AZURE_TENANT_ID is missing - both are required for %s authentication", rule.triggerVar, rule.authMethod) + } } } diff --git a/pkg/runtime/tools/tools_manager_test.go b/pkg/runtime/tools/tools_manager_test.go index 4e46ebdf7..0c7ce9b91 100644 --- a/pkg/runtime/tools/tools_manager_test.go +++ b/pkg/runtime/tools/tools_manager_test.go @@ -983,6 +983,65 @@ func TestToolsManager_checkKubelogin(t *testing.T) { } }) + t.Run("AZURE_FEDERATED_TOKEN_FILESetButAZURE_CLIENT_IDMissing", func(t *testing.T) { + // Given AZURE_FEDERATED_TOKEN_FILE is set but AZURE_CLIENT_ID is missing + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return fmt.Sprintf("kubelogin version %s", constants.MinimumVersionKubelogin), nil + } + return "", fmt.Errorf("command not found") + } + os.Setenv("AZURE_FEDERATED_TOKEN_FILE", "/path/to/token") + defer os.Unsetenv("AZURE_FEDERATED_TOKEN_FILE") + os.Unsetenv("AZURE_CLIENT_ID") + os.Unsetenv("AZURE_TENANT_ID") + // When checking kubelogin + err := toolsManager.checkKubelogin() + // Then an error indicating AZURE_CLIENT_ID is missing should be returned + if err == nil || !strings.Contains(err.Error(), "AZURE_FEDERATED_TOKEN_FILE is set but AZURE_CLIENT_ID is missing") { + t.Errorf("Expected AZURE_CLIENT_ID missing error, got %v", err) + } + }) + + t.Run("AZURE_FEDERATED_TOKEN_FILESetButAZURE_TENANT_IDMissing", func(t *testing.T) { + // Given AZURE_FEDERATED_TOKEN_FILE is set but AZURE_TENANT_ID is missing + mocks, toolsManager := setup(t) + originalExecLookPath := execLookPath + execLookPath = func(name string) (string, error) { + if name == "kubelogin" { + return "/usr/bin/kubelogin", nil + } + return originalExecLookPath(name) + } + mocks.Shell.ExecSilentFunc = func(name string, args ...string) (string, error) { + if name == "kubelogin" && args[0] == "--version" { + return fmt.Sprintf("kubelogin version %s", constants.MinimumVersionKubelogin), nil + } + return "", fmt.Errorf("command not found") + } + os.Setenv("AZURE_FEDERATED_TOKEN_FILE", "/path/to/token") + os.Setenv("AZURE_CLIENT_ID", "test-client-id") + defer func() { + os.Unsetenv("AZURE_FEDERATED_TOKEN_FILE") + os.Unsetenv("AZURE_CLIENT_ID") + }() + os.Unsetenv("AZURE_TENANT_ID") + // When checking kubelogin + err := toolsManager.checkKubelogin() + // Then an error indicating AZURE_TENANT_ID is missing should be returned + if err == nil || !strings.Contains(err.Error(), "AZURE_FEDERATED_TOKEN_FILE is set but AZURE_TENANT_ID is missing") { + t.Errorf("Expected AZURE_TENANT_ID missing error, got %v", err) + } + }) + t.Run("AZURE_CLIENT_SECRETSetButAZURE_CLIENT_IDMissing", func(t *testing.T) { // Given AZURE_CLIENT_SECRET is set but AZURE_CLIENT_ID is missing mocks, toolsManager := setup(t) From 070df4097a535227c026a324485deee7a12e9043 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 19:42:33 +0000 Subject: [PATCH 04/14] chore(deps): update dependency aws/aws-cli to v2.32.13 (#1986) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- aqua.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqua.yaml b/aqua.yaml index 64454a4f5..148d34135 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -30,4 +30,4 @@ packages: - name: helm/helm@v4.0.1 - name: 1password/cli@v2.30.3 - name: fluxcd/flux2@v2.7.5 -- name: aws/aws-cli@2.32.12 +- name: aws/aws-cli@2.32.13 From a55e7569ccebcb1c2f6053f461a15fa758f4625f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Dec 2025 22:48:51 +0000 Subject: [PATCH 05/14] chore(deps): update dependency kubernetes/kubectl to v1.34.3 (#1990) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- aqua.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqua.yaml b/aqua.yaml index 148d34135..af633ca14 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -15,7 +15,7 @@ packages: - name: siderolabs/talos@v1.11.5 - name: siderolabs/omni/omnictl@v1.3.4 - name: siderolabs/omni/omni@v1.3.4 -- name: kubernetes/kubectl@v1.34.2 +- name: kubernetes/kubectl@v1.34.3 - name: go-task/task@v3.45.5 - name: golang/go@go1.25.5 - name: getsops/sops@v3.9.0 From 0ca139f7249b7d6bf65be3668265e362b4667af8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 03:40:54 +0000 Subject: [PATCH 06/14] chore(deps): update dependency aws/aws-cli to v2.32.14 (#1993) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- aqua.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqua.yaml b/aqua.yaml index af633ca14..ed4232342 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -30,4 +30,4 @@ packages: - name: helm/helm@v4.0.1 - name: 1password/cli@v2.30.3 - name: fluxcd/flux2@v2.7.5 -- name: aws/aws-cli@2.32.13 +- name: aws/aws-cli@2.32.14 From b84727a55fcb505ed739f40dfcbf24523ddb0fe5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 05:38:50 +0000 Subject: [PATCH 07/14] chore(deps): update dependency helm/helm to v4.0.2 (#1994) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- aqua.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqua.yaml b/aqua.yaml index ed4232342..4bf2b15d2 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -27,7 +27,7 @@ packages: - name: google/go-jsonnet@v0.21.0 - name: mikefarah/yq@v4.49.2 - name: goreleaser/goreleaser@v2.13.1 -- name: helm/helm@v4.0.1 +- name: helm/helm@v4.0.2 - name: 1password/cli@v2.30.3 - name: fluxcd/flux2@v2.7.5 - name: aws/aws-cli@2.32.14 From a2a3e0ab1bb2f440650c269a036134d1e45351a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 08:26:20 +0000 Subject: [PATCH 08/14] fix(deps): update kubernetes packages to v0.34.3 (#1991) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index f62eedcd6..cf9c8bb89 100644 --- a/go.mod +++ b/go.mod @@ -25,9 +25,9 @@ require ( github.com/spf13/pflag v1.0.10 github.com/zclconf/go-cty v1.17.0 golang.org/x/crypto v0.46.0 - k8s.io/api v0.34.2 - k8s.io/apimachinery v0.34.2 - k8s.io/client-go v0.34.2 + k8s.io/api v0.34.3 + k8s.io/apimachinery v0.34.3 + k8s.io/client-go v0.34.3 sigs.k8s.io/controller-runtime v0.22.4 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index 4c6e4b4a6..129b78e16 100644 --- a/go.sum +++ b/go.sum @@ -625,14 +625,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= -k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= +k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= +k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= k8s.io/apiextensions-apiserver v0.34.2 h1:WStKftnGeoKP4AZRz/BaAAEJvYp4mlZGN0UCv+uvsqo= k8s.io/apiextensions-apiserver v0.34.2/go.mod h1:398CJrsgXF1wytdaanynDpJ67zG4Xq7yj91GrmYN2SE= -k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= -k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= -k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= +k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= +k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= +k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= From efa930a4a0a1e3248fccbad038a848776571665f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:30:09 +0000 Subject: [PATCH 09/14] chore(deps): update dependency aquaproj/aqua-registry to v4.445.0 (#1987) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- aqua.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqua.yaml b/aqua.yaml index 4bf2b15d2..3e9649e49 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -9,7 +9,7 @@ # - all registries: - type: standard - ref: v4.444.2 # renovate: depName=aquaproj/aqua-registry + ref: v4.445.0 # renovate: depName=aquaproj/aqua-registry packages: - name: hashicorp/terraform@v1.14.1 - name: siderolabs/talos@v1.11.5 From ff2dea7e246464f0a6e733c341293e2ad6e42525 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:33:21 +0000 Subject: [PATCH 10/14] chore(deps): update dependency securego/gosec to v2.22.11 (#1995) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 2 +- aqua.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8e33db3bb..8232e3f9d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -128,7 +128,7 @@ jobs: run: go install ./... - name: Run Gosec Security Scanner - uses: securego/gosec@6be2b51fd78feca86af91f5186b7964d76cb1256 # v2.22.10 + uses: securego/gosec@424fc4cd9c82ea0fd6bee9cd49c2db2c3cc0c93f # v2.22.11 with: args: ./... env: diff --git a/aqua.yaml b/aqua.yaml index 3e9649e49..47aff9c9f 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -22,7 +22,7 @@ packages: - name: abiosoft/colima@v0.9.1 - name: lima-vm/lima@v2.0.2 - name: docker/cli@v27.4.1 -- name: securego/gosec@v2.22.10 +- name: securego/gosec@v2.22.11 - name: docker/compose@v5.0.0 - name: google/go-jsonnet@v0.21.0 - name: mikefarah/yq@v4.49.2 From 7c60a07f814ce3d586ecdd96a04bdd3bb65236ca Mon Sep 17 00:00:00 2001 From: rmvangun <85766511+rmvangun@users.noreply.github.com> Date: Thu, 11 Dec 2025 19:16:07 -0500 Subject: [PATCH 11/14] fix(terraform): Properly detect OIDC in github actions (#1998) Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/env/terraform_env.go | 32 +++++--- pkg/runtime/env/terraform_env_test.go | 102 ++++++++++++++++++-------- 2 files changed, 93 insertions(+), 41 deletions(-) diff --git a/pkg/runtime/env/terraform_env.go b/pkg/runtime/env/terraform_env.go index 125f87be8..eae47a2f4 100644 --- a/pkg/runtime/env/terraform_env.go +++ b/pkg/runtime/env/terraform_env.go @@ -545,17 +545,7 @@ func (e *TerraformEnvPrinter) generateProvidersOverrideTf(directory ...string) e azureEnv = *config.Azure.Environment } - azureClientSecret := e.shims.Getenv("AZURE_CLIENT_SECRET") - azureFederatedTokenFile := e.shims.Getenv("AZURE_FEDERATED_TOKEN_FILE") - - var loginMode string - if azureFederatedTokenFile != "" { - loginMode = "workloadidentity" - } else if azureClientSecret != "" { - loginMode = "spn" - } else { - loginMode = "azurecli" - } + loginMode := e.detectKubeloginMode() providerConfig := fmt.Sprintf(`provider "kubernetes" { exec { @@ -581,6 +571,26 @@ func (e *TerraformEnvPrinter) generateProvidersOverrideTf(directory ...string) e return nil } +// detectKubeloginMode determines which kubelogin authentication mode to use based on +// environment variables. Priority order: GitHub Actions OIDC, Kubernetes pod workload +// identity, and fallback to Azure CLI for local development. +func (e *TerraformEnvPrinter) detectKubeloginMode() string { + actionsToken := e.shims.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") + actionsURL := e.shims.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL") + if actionsToken != "" && actionsURL != "" { + return "workloadidentity" + } + + federatedTokenFile := e.shims.Getenv("AZURE_FEDERATED_TOKEN_FILE") + if federatedTokenFile != "" { + if _, err := e.shims.Stat(federatedTokenFile); err == nil { + return "workloadidentity" + } + } + + return "azurecli" +} + // generateBackendConfigArgs constructs backend config args for terraform commands. // It reads the backend type from the config and adds relevant key-value pairs. // The function supports local, s3, kubernetes, and azurerm backends. diff --git a/pkg/runtime/env/terraform_env_test.go b/pkg/runtime/env/terraform_env_test.go index d528c3edd..57227f449 100644 --- a/pkg/runtime/env/terraform_env_test.go +++ b/pkg/runtime/env/terraform_env_test.go @@ -927,8 +927,7 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { return printer, mocks } - t.Run("SuccessWithWorkloadIdentity", func(t *testing.T) { - // Given a TerraformEnvPrinter with Azure + AKS enabled and AZURE_FEDERATED_TOKEN_FILE set + t.Run("SuccessWithKubernetesPodWorkloadIdentity", func(t *testing.T) { printer, mocks := setup(t) mocks.ConfigHandler.Set("azure.enabled", true) mocks.ConfigHandler.Set("cluster.driver", "aks") @@ -938,18 +937,65 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { } return "" } + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if name == "/path/to/token/file" { + return nil, nil + } + return nil, os.ErrNotExist + } + + var writtenData []byte + mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { + writtenData = data + return nil + } + + err := printer.generateProvidersOverrideTf() + + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + expectedContent := fmt.Sprintf(`provider "kubernetes" { + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "kubelogin" + args = [ + "get-token", + "--login", "workloadidentity", + "--environment", "AzurePublicCloud", + "--server-id", "%s", + ] + } +} +`, constants.DefaultAKSOIDCServerID) + if string(writtenData) != expectedContent { + t.Errorf("Expected provider config %q, got %q", expectedContent, string(writtenData)) + } + }) + + t.Run("SuccessWithGitHubActionsWorkloadIdentity", func(t *testing.T) { + printer, mocks := setup(t) + mocks.ConfigHandler.Set("azure.enabled", true) + mocks.ConfigHandler.Set("cluster.driver", "aks") + mocks.Shims.Getenv = func(key string) string { + if key == "ACTIONS_ID_TOKEN_REQUEST_TOKEN" { + return "test-token" + } + if key == "ACTIONS_ID_TOKEN_REQUEST_URL" { + return "https://test-url" + } + return "" + } - // Mock WriteFile to capture the output var writtenData []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { writtenData = data return nil } - // When generateProvidersOverrideTf is called err := printer.generateProvidersOverrideTf() - // Then no error should occur and the expected provider config with Workload Identity should be written if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -972,32 +1018,37 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { } }) - t.Run("WorkloadIdentityPriorityOverSPN", func(t *testing.T) { - // Given a TerraformEnvPrinter with both AZURE_FEDERATED_TOKEN_FILE and AZURE_CLIENT_SECRET set + t.Run("GitHubActionsPriorityOverKubernetesPod", func(t *testing.T) { printer, mocks := setup(t) mocks.ConfigHandler.Set("azure.enabled", true) mocks.ConfigHandler.Set("cluster.driver", "aks") mocks.Shims.Getenv = func(key string) string { + if key == "ACTIONS_ID_TOKEN_REQUEST_TOKEN" { + return "test-token" + } + if key == "ACTIONS_ID_TOKEN_REQUEST_URL" { + return "https://test-url" + } if key == "AZURE_FEDERATED_TOKEN_FILE" { return "/path/to/token/file" } - if key == "AZURE_CLIENT_SECRET" { - return "test-secret" - } return "" } + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if name == "/path/to/token/file" { + return nil, nil + } + return nil, os.ErrNotExist + } - // Mock WriteFile to capture the output var writtenData []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { writtenData = data return nil } - // When generateProvidersOverrideTf is called err := printer.generateProvidersOverrideTf() - // Then no error should occur and Workload Identity should be used (higher priority) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -1020,29 +1071,28 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { } }) - t.Run("SuccessWithSPN", func(t *testing.T) { - // Given a TerraformEnvPrinter with Azure + AKS enabled and AZURE_CLIENT_SECRET set + t.Run("KubernetesPodTokenFileNotExists", func(t *testing.T) { printer, mocks := setup(t) mocks.ConfigHandler.Set("azure.enabled", true) mocks.ConfigHandler.Set("cluster.driver", "aks") mocks.Shims.Getenv = func(key string) string { - if key == "AZURE_CLIENT_SECRET" { - return "test-secret" + if key == "AZURE_FEDERATED_TOKEN_FILE" { + return "/path/to/nonexistent/file" } return "" } + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } - // Mock WriteFile to capture the output var writtenData []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { writtenData = data return nil } - // When generateProvidersOverrideTf is called err := printer.generateProvidersOverrideTf() - // Then no error should occur and the expected provider config with SPN should be written if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -1053,7 +1103,7 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { command = "kubelogin" args = [ "get-token", - "--login", "spn", + "--login", "azurecli", "--environment", "AzurePublicCloud", "--server-id", "%s", ] @@ -1264,18 +1314,13 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { }) t.Run("CustomAzureEnvironment", func(t *testing.T) { - // Given a TerraformEnvPrinter with custom Azure environment printer, mocks := setup(t) mocks.ConfigHandler.Set("azure.enabled", true) mocks.ConfigHandler.Set("cluster.driver", "aks") mocks.Shims.Getenv = func(key string) string { - if key == "AZURE_CLIENT_SECRET" { - return "test-secret" - } return "" } - // Mock config with custom environment mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { if key == "azure.enabled" { @@ -1299,17 +1344,14 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { } printer.configHandler = mockConfigHandler - // Mock WriteFile to capture the output var writtenData []byte mocks.Shims.WriteFile = func(filename string, data []byte, perm os.FileMode) error { writtenData = data return nil } - // When generateProvidersOverrideTf is called err := printer.generateProvidersOverrideTf() - // Then no error should occur and the expected provider config should be written with custom environment if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -1320,7 +1362,7 @@ func TestTerraformEnv_generateProvidersOverrideTf(t *testing.T) { command = "kubelogin" args = [ "get-token", - "--login", "spn", + "--login", "azurecli", "--environment", "AzureUSGovernment", "--server-id", "%s", ] From bcb767ec5b22a76130a73a197069be50609d6bf9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 03:01:11 +0000 Subject: [PATCH 12/14] chore(deps): update dependency aws/aws-cli to v2.32.15 (#1999) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- aqua.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqua.yaml b/aqua.yaml index 47aff9c9f..2c1a69cf7 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -30,4 +30,4 @@ packages: - name: helm/helm@v4.0.2 - name: 1password/cli@v2.30.3 - name: fluxcd/flux2@v2.7.5 -- name: aws/aws-cli@2.32.14 +- name: aws/aws-cli@2.32.15 From cff96674a0c1db7757f0c804798094e43515c58f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 05:31:56 +0000 Subject: [PATCH 13/14] chore(deps): update dependency hashicorp/terraform to v1.14.2 (#1996) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- aqua.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aqua.yaml b/aqua.yaml index 2c1a69cf7..6d30679c8 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -11,7 +11,7 @@ registries: - type: standard ref: v4.445.0 # renovate: depName=aquaproj/aqua-registry packages: -- name: hashicorp/terraform@v1.14.1 +- name: hashicorp/terraform@v1.14.2 - name: siderolabs/talos@v1.11.5 - name: siderolabs/omni/omnictl@v1.3.4 - name: siderolabs/omni/omni@v1.3.4 From a4b874a60d98a278079b1f5535d9bdc5263c83b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 05:32:09 +0000 Subject: [PATCH 14/14] chore(deps): update localstack/localstack docker tag to v4.12.0 --- pkg/constants/constants.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 469e9cf6b..63cf69caf 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -77,7 +77,7 @@ const DefaultKustomizationWaitPollInterval = 5 * time.Second const DefaultKustomizationWaitMaxFailures = 5 // renovate: datasource=docker depName=localstack/localstack -const DefaultAWSLocalstackImage = "localstack/localstack:4.11.1" +const DefaultAWSLocalstackImage = "localstack/localstack:4.12.0" // renovate: datasource=docker depName=localstack/localstack-pro const DefaultAWSLocalstackProImage = "localstack/localstack-pro:4.11.1"