From 90aacaddb3958ad4894a6eed2a97f68dfbca7fb2 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sun, 2 Nov 2025 19:04:37 -0500 Subject: [PATCH 1/4] refactor(context): Migrate build ID to leverage context vs. pipeline The build ID functionailty has been migrated to the context object, dropping pipeline or runtime use from the command. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- cmd/build_id.go | 27 ++- cmd/build_id_test.go | 12 +- pkg/composer/blueprint/blueprint_handler.go | 31 +-- .../blueprint_handler_private_test.go | 26 +- .../blueprint_handler_public_test.go | 4 +- pkg/context/context.go | 223 +++++++++++++++--- pkg/context/env/aws_env.go | 2 +- pkg/context/env/aws_env_test.go | 2 +- pkg/context/env/azure_env.go | 2 +- pkg/context/env/azure_env_test.go | 2 +- pkg/context/env/docker_env.go | 2 +- pkg/context/env/docker_env_test.go | 2 +- pkg/context/env/env.go | 4 +- pkg/context/env/env_test.go | 2 +- pkg/context/env/kube_env.go | 2 +- pkg/context/env/kube_env_test.go | 2 +- pkg/context/env/mock_env.go | 2 +- pkg/context/env/mock_env_test.go | 2 +- pkg/context/env/shims.go | 2 +- pkg/context/env/shims_test.go | 2 +- pkg/context/env/talos_env.go | 2 +- pkg/context/env/talos_env_test.go | 2 +- pkg/context/env/terraform_env.go | 2 +- pkg/context/env/terraform_env_test.go | 86 +++---- pkg/context/env/windsor_env.go | 135 ++++++++++- pkg/context/env/windsor_env_test.go | 9 +- pkg/pipelines/init.go | 6 +- pkg/runtime/runtime_loaders.go | 6 +- 28 files changed, 419 insertions(+), 182 deletions(-) diff --git a/cmd/build_id.go b/cmd/build_id.go index f667a1b3b..cb38bffe4 100644 --- a/cmd/build_id.go +++ b/cmd/build_id.go @@ -1,12 +1,11 @@ package cmd import ( - "context" "fmt" "github.com/spf13/cobra" + "github.com/windsorcli/cli/pkg/context" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/pipelines" ) var buildIdNewFlag bool @@ -27,26 +26,28 @@ Examples: BUILD_ID=$(windsor build-id --new) && docker build -t myapp:$BUILD_ID .`, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - // Get shared dependency injector from context injector := cmd.Context().Value(injectorKey).(di.Injector) - // Set up the build ID pipeline - buildIDPipeline, err := pipelines.WithPipeline(injector, cmd.Context(), "buildIDPipeline") + execCtx := &context.ExecutionContext{ + Injector: injector, + } + + execCtx, err := context.NewContext(execCtx) if err != nil { - return fmt.Errorf("failed to set up build ID pipeline: %w", err) + return fmt.Errorf("failed to initialize context: %w", err) } - // Create execution context with flags - ctx := cmd.Context() + var buildID string if buildIdNewFlag { - ctx = context.WithValue(ctx, "new", true) + buildID, err = execCtx.GenerateBuildID() + } else { + buildID, err = execCtx.GetBuildID() } - - // Execute the build ID pipeline - if err := buildIDPipeline.Execute(ctx); err != nil { - return fmt.Errorf("failed to execute build ID pipeline: %w", err) + if err != nil { + return fmt.Errorf("failed to manage build ID: %w", err) } + fmt.Printf("%s\n", buildID) return nil }, } diff --git a/cmd/build_id_test.go b/cmd/build_id_test.go index 1e77dc4d6..8157347f7 100644 --- a/cmd/build_id_test.go +++ b/cmd/build_id_test.go @@ -21,7 +21,11 @@ func TestBuildIDCmd(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given proper output capture and mock setup _, stderr := setup(t) - setupMocks(t) + mocks := setupMocks(t) + + // Set up command context with injector + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) rootCmd.SetArgs([]string{"build-id"}) @@ -42,7 +46,11 @@ func TestBuildIDCmd(t *testing.T) { t.Run("SuccessWithNewFlag", func(t *testing.T) { // Given proper output capture and mock setup _, stderr := setup(t) - setupMocks(t) + mocks := setupMocks(t) + + // Set up command context with injector + ctx := context.WithValue(context.Background(), injectorKey, mocks.Injector) + rootCmd.SetContext(ctx) rootCmd.SetArgs([]string{"build-id", "--new"}) diff --git a/pkg/composer/blueprint/blueprint_handler.go b/pkg/composer/blueprint/blueprint_handler.go index 7098af252..2bd914bef 100644 --- a/pkg/composer/blueprint/blueprint_handler.go +++ b/pkg/composer/blueprint/blueprint_handler.go @@ -18,6 +18,7 @@ import ( _ "embed" "github.com/goccy/go-yaml" + contextpkg "github.com/windsorcli/cli/pkg/context" "github.com/windsorcli/cli/pkg/context/config" "github.com/windsorcli/cli/pkg/constants" "github.com/windsorcli/cli/pkg/di" @@ -1683,7 +1684,12 @@ func (b *BaseBlueprintHandler) applyValuesConfigMaps() error { mergedCommonValues["REGISTRY_URL"] = registryURL mergedCommonValues["LOCAL_VOLUME_PATH"] = localVolumePath - buildID, err := b.getBuildIDFromFile() + execCtx := &contextpkg.ExecutionContext{ + Shell: b.shell, + ConfigHandler: b.configHandler, + Injector: b.injector, + } + buildID, err := execCtx.GetBuildID() if err != nil { return fmt.Errorf("failed to get build ID: %w", err) } @@ -1839,29 +1845,6 @@ func (b *BaseBlueprintHandler) flattenValuesToConfigMap(values map[string]any, p return nil } -// getBuildIDFromFile returns the build ID string from the .windsor/.build-id file in the project root directory. -// It locates the project root using the shell interface, constructs the build ID file path, and attempts to read the file. -// If the file does not exist, it returns an empty string and no error. If the file exists, it reads and trims whitespace from the contents. -// Returns the build ID string or an error if the file cannot be read. -func (b *BaseBlueprintHandler) getBuildIDFromFile() (string, error) { - projectRoot, err := b.shell.GetProjectRoot() - if err != nil { - return "", fmt.Errorf("failed to get project root: %w", err) - } - - buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") - - if _, err := b.shims.Stat(buildIDPath); os.IsNotExist(err) { - return "", nil - } - - data, err := b.shims.ReadFile(buildIDPath) - if err != nil { - return "", fmt.Errorf("failed to read build ID file: %w", err) - } - - return strings.TrimSpace(string(data)), nil -} // deepMergeMaps returns a new map from a deep merge of base and overlay maps. // Overlay values take precedence; nested maps merge recursively. Non-map overlay values replace base values. diff --git a/pkg/composer/blueprint/blueprint_handler_private_test.go b/pkg/composer/blueprint/blueprint_handler_private_test.go index 37bf45f61..0284009b9 100644 --- a/pkg/composer/blueprint/blueprint_handler_private_test.go +++ b/pkg/composer/blueprint/blueprint_handler_private_test.go @@ -255,8 +255,9 @@ func TestBaseBlueprintHandler_applyValuesConfigMaps(t *testing.T) { t.Run("SuccessWithKustomizationSubstitutions", func(t *testing.T) { handler := setup(t) - configRoot := "/test/config" - projectRoot := "/test/project" + tmpDir := t.TempDir() + configRoot := filepath.Join(tmpDir, "config") + projectRoot := filepath.Join(tmpDir, "project") mockConfigHandler := handler.configHandler.(*config.MockConfigHandler) mockConfigHandler.GetConfigRootFunc = func() (string, error) { @@ -1249,26 +1250,21 @@ contexts: t.Fatalf("failed to initialize handler: %v", err) } - // Set up build ID by mocking the file system + // Set up build ID by ensuring project root is writable and creating the build ID file testBuildID := "build-1234567890" projectRoot, err := mocks.Shell.GetProjectRoot() if err != nil { t.Fatalf("failed to get project root: %v", err) } - buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") + buildIDDir := filepath.Join(projectRoot, ".windsor") + buildIDPath := filepath.Join(buildIDDir, ".build-id") - // Mock the file system to return our test build ID - handler.shims.Stat = func(path string) (os.FileInfo, error) { - if path == buildIDPath { - return mockFileInfo{name: ".build-id", isDir: false}, nil - } - return nil, os.ErrNotExist + // Ensure the directory exists and create the build ID file + if err := os.MkdirAll(buildIDDir, 0755); err != nil { + t.Fatalf("failed to create build ID directory: %v", err) } - handler.shims.ReadFile = func(path string) ([]byte, error) { - if path == buildIDPath { - return []byte(testBuildID), nil - } - return []byte{}, nil + if err := os.WriteFile(buildIDPath, []byte(testBuildID), 0644); err != nil { + t.Fatalf("failed to create build ID file: %v", err) } // Mock the kubernetes manager to capture the ConfigMap data diff --git a/pkg/composer/blueprint/blueprint_handler_public_test.go b/pkg/composer/blueprint/blueprint_handler_public_test.go index 453e47064..040e711d4 100644 --- a/pkg/composer/blueprint/blueprint_handler_public_test.go +++ b/pkg/composer/blueprint/blueprint_handler_public_test.go @@ -357,9 +357,9 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Create mock shell and kubernetes manager mockShell := shell.NewMockShell() - // Set default GetProjectRoot implementation + // Set default GetProjectRoot implementation to use writable temp directory mockShell.GetProjectRootFunc = func() (string, error) { - return "/mock/project", nil + return tmpDir, nil } mockKubernetesManager := kubernetes.NewMockKubernetesManager(nil) diff --git a/pkg/context/context.go b/pkg/context/context.go index d64dc0ce7..6c6b899ad 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -1,11 +1,18 @@ package context import ( + "crypto/rand" "fmt" "maps" + "math/big" + "os" + "path/filepath" + "strconv" + "strings" + "time" "github.com/windsorcli/cli/pkg/context/config" - envvars "github.com/windsorcli/cli/pkg/context/env" + "github.com/windsorcli/cli/pkg/context/env" "github.com/windsorcli/cli/pkg/context/secrets" "github.com/windsorcli/cli/pkg/context/shell" "github.com/windsorcli/cli/pkg/context/tools" @@ -44,13 +51,13 @@ type ExecutionContext struct { // EnvPrinters contains environment printers for various providers and tools EnvPrinters struct { - AwsEnv envvars.EnvPrinter - AzureEnv envvars.EnvPrinter - DockerEnv envvars.EnvPrinter - KubeEnv envvars.EnvPrinter - TalosEnv envvars.EnvPrinter - TerraformEnv envvars.EnvPrinter - WindsorEnv envvars.EnvPrinter + AwsEnv env.EnvPrinter + AzureEnv env.EnvPrinter + DockerEnv env.EnvPrinter + KubeEnv env.EnvPrinter + TalosEnv env.EnvPrinter + TerraformEnv env.EnvPrinter + WindsorEnv env.EnvPrinter } // ToolsManager manages tool installation and configuration @@ -83,24 +90,6 @@ func NewContext(ctx *ExecutionContext) (*ExecutionContext, error) { } injector := ctx.Injector - if ctx.ConfigHandler == nil { - if existing := injector.Resolve("configHandler"); existing != nil { - if configHandler, ok := existing.(config.ConfigHandler); ok { - ctx.ConfigHandler = configHandler - } else { - ctx.ConfigHandler = config.NewConfigHandler(injector) - injector.Register("configHandler", ctx.ConfigHandler) - } - } else { - ctx.ConfigHandler = config.NewConfigHandler(injector) - injector.Register("configHandler", ctx.ConfigHandler) - } - - if err := ctx.ConfigHandler.Initialize(); err != nil { - return nil, fmt.Errorf("failed to initialize config handler: %w", err) - } - } - if ctx.Shell == nil { if existing := injector.Resolve("shell"); existing != nil { if shellInstance, ok := existing.(shell.Shell); ok { @@ -121,6 +110,24 @@ func NewContext(ctx *ExecutionContext) (*ExecutionContext, error) { } } + if ctx.ConfigHandler == nil { + if existing := injector.Resolve("configHandler"); existing != nil { + if configHandler, ok := existing.(config.ConfigHandler); ok { + ctx.ConfigHandler = configHandler + } else { + ctx.ConfigHandler = config.NewConfigHandler(injector) + injector.Register("configHandler", ctx.ConfigHandler) + } + } else { + ctx.ConfigHandler = config.NewConfigHandler(injector) + injector.Register("configHandler", ctx.ConfigHandler) + } + + if err := ctx.ConfigHandler.Initialize(); err != nil { + return nil, fmt.Errorf("failed to initialize config handler: %w", err) + } + } + if ctx.envVars == nil { ctx.envVars = make(map[string]string) } @@ -132,7 +139,7 @@ func NewContext(ctx *ExecutionContext) (*ExecutionContext, error) { } // ============================================================================= -// Environment Public Methods +// Public Methods // ============================================================================= // LoadEnvironment loads environment variables and aliases from all configured environment printers. @@ -243,8 +250,59 @@ func (ctx *ExecutionContext) GetAliases() map[string]string { return result } +// GetBuildID retrieves the current build ID from the .windsor/.build-id file. +// If no build ID exists, a new one is generated, persisted, and returned. +// Returns the build ID string or an error if retrieval or persistence fails. +func (ctx *ExecutionContext) GetBuildID() (string, error) { + projectRoot, err := ctx.Shell.GetProjectRoot() + if err != nil { + return "", fmt.Errorf("failed to get project root: %w", err) + } + + buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") + var buildID string + + if _, err := os.Stat(buildIDPath); os.IsNotExist(err) { + buildID = "" + } else { + data, err := os.ReadFile(buildIDPath) + if err != nil { + return "", fmt.Errorf("failed to read build ID file: %w", err) + } + buildID = strings.TrimSpace(string(data)) + } + + if buildID == "" { + newBuildID, err := ctx.generateBuildID() + if err != nil { + return "", fmt.Errorf("failed to generate build ID: %w", err) + } + if err := ctx.writeBuildIDToFile(newBuildID); err != nil { + return "", fmt.Errorf("failed to set build ID: %w", err) + } + return newBuildID, nil + } + + return buildID, nil +} + +// GenerateBuildID generates a new build ID and persists it to the .windsor/.build-id file, +// overwriting any existing value. Returns the new build ID or an error if generation or persistence fails. +func (ctx *ExecutionContext) GenerateBuildID() (string, error) { + newBuildID, err := ctx.generateBuildID() + if err != nil { + return "", fmt.Errorf("failed to generate build ID: %w", err) + } + + if err := ctx.writeBuildIDToFile(newBuildID); err != nil { + return "", fmt.Errorf("failed to set build ID: %w", err) + } + + return newBuildID, nil +} + // ============================================================================= -// Environment Private Methods +// Private Methods // ============================================================================= // initializeEnvPrinters initializes environment printers based on configuration settings. @@ -252,33 +310,33 @@ func (ctx *ExecutionContext) GetAliases() map[string]string { // based on the current configuration state. func (ctx *ExecutionContext) initializeEnvPrinters() { if ctx.EnvPrinters.AwsEnv == nil && ctx.ConfigHandler.GetBool("aws.enabled", false) { - ctx.EnvPrinters.AwsEnv = envvars.NewAwsEnvPrinter(ctx.Injector) + ctx.EnvPrinters.AwsEnv = env.NewAwsEnvPrinter(ctx.Injector) ctx.Injector.Register("awsEnv", ctx.EnvPrinters.AwsEnv) } if ctx.EnvPrinters.AzureEnv == nil && ctx.ConfigHandler.GetBool("azure.enabled", false) { - ctx.EnvPrinters.AzureEnv = envvars.NewAzureEnvPrinter(ctx.Injector) + ctx.EnvPrinters.AzureEnv = env.NewAzureEnvPrinter(ctx.Injector) ctx.Injector.Register("azureEnv", ctx.EnvPrinters.AzureEnv) } if ctx.EnvPrinters.DockerEnv == nil && ctx.ConfigHandler.GetBool("docker.enabled", false) { - ctx.EnvPrinters.DockerEnv = envvars.NewDockerEnvPrinter(ctx.Injector) + ctx.EnvPrinters.DockerEnv = env.NewDockerEnvPrinter(ctx.Injector) ctx.Injector.Register("dockerEnv", ctx.EnvPrinters.DockerEnv) } if ctx.EnvPrinters.KubeEnv == nil && ctx.ConfigHandler.GetBool("cluster.enabled", false) { - ctx.EnvPrinters.KubeEnv = envvars.NewKubeEnvPrinter(ctx.Injector) + ctx.EnvPrinters.KubeEnv = env.NewKubeEnvPrinter(ctx.Injector) ctx.Injector.Register("kubeEnv", ctx.EnvPrinters.KubeEnv) } if ctx.EnvPrinters.TalosEnv == nil && (ctx.ConfigHandler.GetString("cluster.driver", "") == "talos" || ctx.ConfigHandler.GetString("cluster.driver", "") == "omni") { - ctx.EnvPrinters.TalosEnv = envvars.NewTalosEnvPrinter(ctx.Injector) + ctx.EnvPrinters.TalosEnv = env.NewTalosEnvPrinter(ctx.Injector) ctx.Injector.Register("talosEnv", ctx.EnvPrinters.TalosEnv) } if ctx.EnvPrinters.TerraformEnv == nil && ctx.ConfigHandler.GetBool("terraform.enabled", false) { - ctx.EnvPrinters.TerraformEnv = envvars.NewTerraformEnvPrinter(ctx.Injector) + ctx.EnvPrinters.TerraformEnv = env.NewTerraformEnvPrinter(ctx.Injector) ctx.Injector.Register("terraformEnv", ctx.EnvPrinters.TerraformEnv) } if ctx.EnvPrinters.WindsorEnv == nil { - ctx.EnvPrinters.WindsorEnv = envvars.NewWindsorEnvPrinter(ctx.Injector) + ctx.EnvPrinters.WindsorEnv = env.NewWindsorEnvPrinter(ctx.Injector) ctx.Injector.Register("windsorEnv", ctx.EnvPrinters.WindsorEnv) } } @@ -312,8 +370,8 @@ func (ctx *ExecutionContext) initializeSecretsProviders() { // getAllEnvPrinters returns all environment printers in a consistent order. // This ensures that environment variables are processed in a predictable sequence // with WindsorEnv being processed last to take precedence. -func (ctx *ExecutionContext) getAllEnvPrinters() []envvars.EnvPrinter { - return []envvars.EnvPrinter{ +func (ctx *ExecutionContext) getAllEnvPrinters() []env.EnvPrinter { + return []env.EnvPrinter{ ctx.EnvPrinters.AwsEnv, ctx.EnvPrinters.AzureEnv, ctx.EnvPrinters.DockerEnv, @@ -362,3 +420,94 @@ func (ctx *ExecutionContext) loadSecrets() error { return nil } + +// writeBuildIDToFile writes the provided build ID string to the .windsor/.build-id file in the project root. +// Ensures the .windsor directory exists before writing. Returns an error if directory creation or file write fails. +func (ctx *ExecutionContext) writeBuildIDToFile(buildID string) error { + projectRoot, err := ctx.Shell.GetProjectRoot() + if err != nil { + return fmt.Errorf("failed to get project root: %w", err) + } + + buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") + buildIDDir := filepath.Dir(buildIDPath) + + if err := os.MkdirAll(buildIDDir, 0755); err != nil { + return fmt.Errorf("failed to create build ID directory: %w", err) + } + + return os.WriteFile(buildIDPath, []byte(buildID), 0644) +} + +// generateBuildID generates and returns a build ID string in the format YYMMDD.RANDOM.#. +// YYMMDD is the current date (year, month, day), RANDOM is a random three-digit number for collision prevention, +// and # is a sequential counter incremented for each build on the same day. If a build ID already exists for the current day, +// the counter is incremented; otherwise, a new build ID is generated with counter set to 1. Ensures global ordering and uniqueness. +// Returns the build ID string or an error if generation or retrieval fails. +func (ctx *ExecutionContext) generateBuildID() (string, error) { + now := time.Now() + yy := now.Year() % 100 + mm := int(now.Month()) + dd := now.Day() + datePart := fmt.Sprintf("%02d%02d%02d", yy, mm, dd) + + projectRoot, err := ctx.Shell.GetProjectRoot() + if err != nil { + return "", fmt.Errorf("failed to get project root: %w", err) + } + + buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") + var existingBuildID string + + if _, err := os.Stat(buildIDPath); os.IsNotExist(err) { + existingBuildID = "" + } else { + data, err := os.ReadFile(buildIDPath) + if err != nil { + return "", fmt.Errorf("failed to read build ID file: %w", err) + } + existingBuildID = strings.TrimSpace(string(data)) + } + + if existingBuildID != "" { + return ctx.incrementBuildID(existingBuildID, datePart) + } + + random, err := rand.Int(rand.Reader, big.NewInt(1000)) + if err != nil { + return "", fmt.Errorf("failed to generate random number: %w", err) + } + counter := 1 + randomPart := fmt.Sprintf("%03d", random.Int64()) + counterPart := fmt.Sprintf("%d", counter) + + return fmt.Sprintf("%s.%s.%s", datePart, randomPart, counterPart), nil +} + +// incrementBuildID parses an existing build ID and increments its counter component. +// If the date component differs from the current date, generates a new random number and resets the counter to 1. +// Returns the incremented or reset build ID string, or an error if the input format is invalid. +func (ctx *ExecutionContext) incrementBuildID(existingBuildID, currentDate string) (string, error) { + parts := strings.Split(existingBuildID, ".") + if len(parts) != 3 { + return "", fmt.Errorf("invalid build ID format: %s", existingBuildID) + } + + existingDate := parts[0] + existingRandom := parts[1] + existingCounter, err := strconv.Atoi(parts[2]) + if err != nil { + return "", fmt.Errorf("invalid counter component: %s", parts[2]) + } + + if existingDate != currentDate { + random, err := rand.Int(rand.Reader, big.NewInt(1000)) + if err != nil { + return "", fmt.Errorf("failed to generate random number: %w", err) + } + return fmt.Sprintf("%s.%03d.1", currentDate, random.Int64()), nil + } + + existingCounter++ + return fmt.Sprintf("%s.%s.%d", existingDate, existingRandom, existingCounter), nil +} diff --git a/pkg/context/env/aws_env.go b/pkg/context/env/aws_env.go index 2aa8d4fcf..a30fab7eb 100644 --- a/pkg/context/env/aws_env.go +++ b/pkg/context/env/aws_env.go @@ -3,7 +3,7 @@ // The AwsEnvPrinter handles AWS profile, endpoint, and S3 configuration settings, // ensuring proper AWS CLI integration and environment setup for AWS operations. -package envvars +package env import ( "fmt" diff --git a/pkg/context/env/aws_env_test.go b/pkg/context/env/aws_env_test.go index 23a81ab89..7bc7ce082 100644 --- a/pkg/context/env/aws_env_test.go +++ b/pkg/context/env/aws_env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "fmt" diff --git a/pkg/context/env/azure_env.go b/pkg/context/env/azure_env.go index df9395588..8443f1dc5 100644 --- a/pkg/context/env/azure_env.go +++ b/pkg/context/env/azure_env.go @@ -3,7 +3,7 @@ // The AzureEnvPrinter handles Azure configuration settings and environment setup, // ensuring proper Azure CLI integration and environment setup for operations. -package envvars +package env import ( "fmt" diff --git a/pkg/context/env/azure_env_test.go b/pkg/context/env/azure_env_test.go index 59d0c785a..606792903 100644 --- a/pkg/context/env/azure_env_test.go +++ b/pkg/context/env/azure_env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "fmt" diff --git a/pkg/context/env/docker_env.go b/pkg/context/env/docker_env.go index f246b2b4b..be0c0efdf 100644 --- a/pkg/context/env/docker_env.go +++ b/pkg/context/env/docker_env.go @@ -3,7 +3,7 @@ // The DockerEnvPrinter handles Docker host, context, and registry configuration settings, // ensuring proper Docker CLI integration and environment setup for container operations. -package envvars +package env import ( "fmt" diff --git a/pkg/context/env/docker_env_test.go b/pkg/context/env/docker_env_test.go index d17ce4733..320b5533b 100644 --- a/pkg/context/env/docker_env_test.go +++ b/pkg/context/env/docker_env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "errors" diff --git a/pkg/context/env/env.go b/pkg/context/env/env.go index 6da3d3729..11bde2e14 100644 --- a/pkg/context/env/env.go +++ b/pkg/context/env/env.go @@ -3,15 +3,15 @@ // The EnvPrinter acts as the central environment orchestrator for the application, // coordinating environment variable management, shell integration, and configuration persistence. -package envvars +package env import ( "fmt" "slices" "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/context/shell" + "github.com/windsorcli/cli/pkg/di" ) // ============================================================================= diff --git a/pkg/context/env/env_test.go b/pkg/context/env/env_test.go index 8f5efda0e..8471185d9 100644 --- a/pkg/context/env/env_test.go +++ b/pkg/context/env/env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "os" diff --git a/pkg/context/env/kube_env.go b/pkg/context/env/kube_env.go index fffcba577..df08bfb91 100644 --- a/pkg/context/env/kube_env.go +++ b/pkg/context/env/kube_env.go @@ -3,7 +3,7 @@ // The KubeEnvPrinter handles kubeconfig, context, and persistent volume configuration settings, // ensuring proper kubectl integration and environment setup for Kubernetes operations. -package envvars +package env import ( "context" diff --git a/pkg/context/env/kube_env_test.go b/pkg/context/env/kube_env_test.go index 2c0e54c4e..f6f4866de 100644 --- a/pkg/context/env/kube_env_test.go +++ b/pkg/context/env/kube_env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "errors" diff --git a/pkg/context/env/mock_env.go b/pkg/context/env/mock_env.go index ac86aca6a..2b636fcca 100644 --- a/pkg/context/env/mock_env.go +++ b/pkg/context/env/mock_env.go @@ -3,7 +3,7 @@ // The MockEnvPrinter enables testing of environment-dependent functionality, // allowing for controlled simulation of environment operations in tests. -package envvars +package env // ============================================================================= // Types diff --git a/pkg/context/env/mock_env_test.go b/pkg/context/env/mock_env_test.go index 6f3f4fa80..c488b6226 100644 --- a/pkg/context/env/mock_env_test.go +++ b/pkg/context/env/mock_env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "fmt" diff --git a/pkg/context/env/shims.go b/pkg/context/env/shims.go index da68e102c..5bfc641d1 100644 --- a/pkg/context/env/shims.go +++ b/pkg/context/env/shims.go @@ -3,7 +3,7 @@ // It serves as a testing aid by allowing system calls to be intercepted // It enables dependency injection and test isolation for system-level operations -package envvars +package env import ( "crypto/rand" diff --git a/pkg/context/env/shims_test.go b/pkg/context/env/shims_test.go index 28663b4da..76768cf73 100644 --- a/pkg/context/env/shims_test.go +++ b/pkg/context/env/shims_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "testing" diff --git a/pkg/context/env/talos_env.go b/pkg/context/env/talos_env.go index 39f71e0b2..ce95d18cc 100644 --- a/pkg/context/env/talos_env.go +++ b/pkg/context/env/talos_env.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "fmt" diff --git a/pkg/context/env/talos_env_test.go b/pkg/context/env/talos_env_test.go index e469f1919..2957489ad 100644 --- a/pkg/context/env/talos_env_test.go +++ b/pkg/context/env/talos_env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "errors" diff --git a/pkg/context/env/terraform_env.go b/pkg/context/env/terraform_env.go index 0c6df3aec..7ec30c9ff 100644 --- a/pkg/context/env/terraform_env.go +++ b/pkg/context/env/terraform_env.go @@ -3,7 +3,7 @@ // The TerraformEnvPrinter handles backend configuration, variable files, and state management, // ensuring proper Terraform CLI integration and environment setup for infrastructure operations. -package envvars +package env import ( "fmt" diff --git a/pkg/context/env/terraform_env_test.go b/pkg/context/env/terraform_env_test.go index 1efb606aa..759bfc94d 100644 --- a/pkg/context/env/terraform_env_test.go +++ b/pkg/context/env/terraform_env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "fmt" @@ -9,9 +9,7 @@ import ( "strings" "testing" - blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/context/config" - "github.com/windsorcli/cli/pkg/composer/blueprint" ) // ============================================================================= @@ -23,13 +21,6 @@ func setupTerraformEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Pass the mock config handler to setupMocks mocks := setupMocks(t, opts...) - // Create and register mock blueprint handler - mockBlueprint := blueprint.NewMockBlueprintHandler(mocks.Injector) - mockBlueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{} - } - mocks.Injector.Register("blueprintHandler", mockBlueprint) - mocks.Shims.Getwd = func() (string, error) { // Use platform-agnostic path return filepath.Join("mock", "project", "root", "terraform", "project", "path"), nil @@ -1503,21 +1494,26 @@ terraform: t.Run("EmptyTerraformOutput", func(t *testing.T) { printer, mocks := setup(t) - // Get the blueprint handler from the injector and configure it - blueprintHandler := mocks.Injector.Resolve("blueprintHandler").(*blueprint.MockBlueprintHandler) - blueprintHandler.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "base", - FullPath: "/project/.windsor/.tf_modules/base", - DependsOn: []string{}, - }, - { - Path: "app", - FullPath: "/project/.windsor/.tf_modules/app", - DependsOn: []string{"base"}, - }, + // Mock blueprint.yaml content + configRoot, _ := mocks.ConfigHandler.GetConfigRoot() + blueprintPath := filepath.Join(configRoot, "blueprint.yaml") + blueprintYAML := `apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: base + fullPath: /project/.windsor/.tf_modules/base + dependsOn: [] + - path: app + fullPath: /project/.windsor/.tf_modules/app + dependsOn: [base]` + originalReadFile := mocks.Shims.ReadFile + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if filename == blueprintPath { + return []byte(blueprintYAML), nil } + return originalReadFile(filename) } // Mock terraform output with empty response @@ -1550,18 +1546,6 @@ terraform: t.Run("NoCurrentComponent", func(t *testing.T) { printer, mocks := setup(t) - // Get the blueprint handler from the injector and configure it - blueprintHandler := mocks.Injector.Resolve("blueprintHandler").(*blueprint.MockBlueprintHandler) - blueprintHandler.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "vpc", - FullPath: "/project/.windsor/.tf_modules/vpc", - DependsOn: []string{}, - }, - } - } - // Set up the current working directory to not match any component mocks.Shims.Getwd = func() (string, error) { return filepath.FromSlash("/project/terraform/nonexistent"), nil @@ -1692,22 +1676,26 @@ terraform: t.Run("ParallelismOnlyAppliedToMatchingComponent", func(t *testing.T) { mocks := setupTerraformEnvMocks(t) - // Set up blueprint handler with parallelism for different component - mockBlueprint := blueprint.NewMockBlueprintHandler(mocks.Injector) + // Mock blueprint.yaml with parallelism for different component parallelism := 10 - mockBlueprint.GetTerraformComponentsFunc = func() []blueprintv1alpha1.TerraformComponent { - return []blueprintv1alpha1.TerraformComponent{ - { - Path: "other/path", - Parallelism: ¶llelism, - }, - { - Path: "test/path", - // No parallelism set - }, + configRoot, _ := mocks.ConfigHandler.GetConfigRoot() + blueprintPath := filepath.Join(configRoot, "blueprint.yaml") + blueprintYAML := fmt.Sprintf(`apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: other/path + parallelism: %d + - path: test/path`, parallelism) + + originalReadFile := mocks.Shims.ReadFile + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if filename == blueprintPath { + return []byte(blueprintYAML), nil } + return originalReadFile(filename) } - mocks.Injector.Register("blueprintHandler", mockBlueprint) printer := &TerraformEnvPrinter{ BaseEnvPrinter: *NewBaseEnvPrinter(mocks.Injector), diff --git a/pkg/context/env/windsor_env.go b/pkg/context/env/windsor_env.go index 6c07702eb..68d78a741 100644 --- a/pkg/context/env/windsor_env.go +++ b/pkg/context/env/windsor_env.go @@ -3,14 +3,18 @@ // The WindsorEnvPrinter handles context, project root, and secrets management, // ensuring proper Windsor CLI integration and environment setup for application operations. -package envvars +package env import ( + "crypto/rand" "fmt" + "math/big" "os" "path/filepath" "regexp" + "strconv" "strings" + "time" "github.com/windsorcli/cli/pkg/context/secrets" "github.com/windsorcli/cli/pkg/di" @@ -96,12 +100,6 @@ func (e *WindsorEnvPrinter) GetEnvVars() (map[string]string, error) { contextID := e.configHandler.GetString("id", "") envVars["WINDSOR_CONTEXT_ID"] = contextID - // Get build ID from the .windsor/.build-id file - buildID, err := e.getBuildIDFromFile() - if err == nil && buildID != "" { - envVars["BUILD_ID"] = buildID - } - projectRoot, err := e.shell.GetProjectRoot() if err != nil { return nil, fmt.Errorf("error retrieving project root: %w", err) @@ -114,6 +112,11 @@ func (e *WindsorEnvPrinter) GetEnvVars() (map[string]string, error) { } envVars["WINDSOR_SESSION_TOKEN"] = sessionToken + buildID, err := e.getBuildID() + if err == nil && buildID != "" { + envVars["BUILD_ID"] = buildID + } + originalEnvVars := e.configHandler.GetStringMap("environment") re := regexp.MustCompile(`\${{\s*(.*?)\s*}}`) @@ -224,25 +227,131 @@ func (e *WindsorEnvPrinter) shouldUseCache() bool { return noCache == "" || noCache == "0" || noCache == "false" || noCache == "False" } -// getBuildIDFromFile retrieves the build ID from the .windsor/.build-id file -func (e *WindsorEnvPrinter) getBuildIDFromFile() (string, error) { +// getBuildID retrieves the current build ID from the .windsor/.build-id file. +// If no build ID exists, a new one is generated, persisted, and returned. +// Returns the build ID string or an error if retrieval or persistence fails. +func (e *WindsorEnvPrinter) getBuildID() (string, error) { projectRoot, err := e.shell.GetProjectRoot() if err != nil { return "", fmt.Errorf("failed to get project root: %w", err) } buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") + var buildID string if _, err := e.shims.Stat(buildIDPath); os.IsNotExist(err) { - return "", nil + buildID = "" + } else { + data, err := e.shims.ReadFile(buildIDPath) + if err != nil { + return "", fmt.Errorf("failed to read build ID file: %w", err) + } + buildID = strings.TrimSpace(string(data)) + } + + if buildID == "" { + newBuildID, err := e.generateBuildID() + if err != nil { + return "", fmt.Errorf("failed to generate build ID: %w", err) + } + if err := e.writeBuildIDToFile(newBuildID); err != nil { + return "", fmt.Errorf("failed to set build ID: %w", err) + } + return newBuildID, nil } - data, err := e.shims.ReadFile(buildIDPath) + return buildID, nil +} + +// writeBuildIDToFile writes the provided build ID string to the .windsor/.build-id file in the project root. +// Ensures the .windsor directory exists before writing. Returns an error if directory creation or file write fails. +func (e *WindsorEnvPrinter) writeBuildIDToFile(buildID string) error { + projectRoot, err := e.shell.GetProjectRoot() if err != nil { - return "", fmt.Errorf("failed to read build ID file: %w", err) + return fmt.Errorf("failed to get project root: %w", err) + } + + buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") + buildIDDir := filepath.Dir(buildIDPath) + + if err := e.shims.MkdirAll(buildIDDir, 0755); err != nil { + return fmt.Errorf("failed to create build ID directory: %w", err) + } + + return e.shims.WriteFile(buildIDPath, []byte(buildID), 0644) +} + +// generateBuildID generates and returns a build ID string in the format YYMMDD.RANDOM.#. +// YYMMDD is the current date (year, month, day), RANDOM is a random three-digit number for collision prevention, +// and # is a sequential counter incremented for each build on the same day. If a build ID already exists for the current day, +// the counter is incremented; otherwise, a new build ID is generated with counter set to 1. Ensures global ordering and uniqueness. +// Returns the build ID string or an error if generation or retrieval fails. +func (e *WindsorEnvPrinter) generateBuildID() (string, error) { + now := time.Now() + yy := now.Year() % 100 + mm := int(now.Month()) + dd := now.Day() + datePart := fmt.Sprintf("%02d%02d%02d", yy, mm, dd) + + projectRoot, err := e.shell.GetProjectRoot() + if err != nil { + return "", fmt.Errorf("failed to get project root: %w", err) + } + + buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") + var existingBuildID string + + if _, err := e.shims.Stat(buildIDPath); os.IsNotExist(err) { + existingBuildID = "" + } else { + data, err := e.shims.ReadFile(buildIDPath) + if err != nil { + return "", fmt.Errorf("failed to read build ID file: %w", err) + } + existingBuildID = strings.TrimSpace(string(data)) + } + + if existingBuildID != "" { + return e.incrementBuildID(existingBuildID, datePart) + } + + random, err := rand.Int(rand.Reader, big.NewInt(1000)) + if err != nil { + return "", fmt.Errorf("failed to generate random number: %w", err) + } + counter := 1 + randomPart := fmt.Sprintf("%03d", random.Int64()) + counterPart := fmt.Sprintf("%d", counter) + + return fmt.Sprintf("%s.%s.%s", datePart, randomPart, counterPart), nil +} + +// incrementBuildID parses an existing build ID and increments its counter component. +// If the date component differs from the current date, generates a new random number and resets the counter to 1. +// Returns the incremented or reset build ID string, or an error if the input format is invalid. +func (e *WindsorEnvPrinter) incrementBuildID(existingBuildID, currentDate string) (string, error) { + parts := strings.Split(existingBuildID, ".") + if len(parts) != 3 { + return "", fmt.Errorf("invalid build ID format: %s", existingBuildID) + } + + existingDate := parts[0] + existingRandom := parts[1] + existingCounter, err := strconv.Atoi(parts[2]) + if err != nil { + return "", fmt.Errorf("invalid counter component: %s", parts[2]) + } + + if existingDate != currentDate { + random, err := rand.Int(rand.Reader, big.NewInt(1000)) + if err != nil { + return "", fmt.Errorf("failed to generate random number: %w", err) + } + return fmt.Sprintf("%s.%03d.1", currentDate, random.Int64()), nil } - return strings.TrimSpace(string(data)), nil + existingCounter++ + return fmt.Sprintf("%s.%s.%d", existingDate, existingRandom, existingCounter), nil } // Ensure WindsorEnvPrinter implements the EnvPrinter interface diff --git a/pkg/context/env/windsor_env_test.go b/pkg/context/env/windsor_env_test.go index bbe4fd472..4cbc75c90 100644 --- a/pkg/context/env/windsor_env_test.go +++ b/pkg/context/env/windsor_env_test.go @@ -1,4 +1,4 @@ -package envvars +package env import ( "fmt" @@ -433,8 +433,11 @@ contexts: // And no additional variables should be added t.Logf("Environment variables: %v", envVars) - if len(envVars) != 6 { - t.Errorf("Should have six base environment variables (BUILD_ID excluded when file doesn't exist)") + if len(envVars) != 7 { + t.Errorf("Should have seven base environment variables (including BUILD_ID which is generated if missing)") + } + if envVars["BUILD_ID"] == "" { + t.Errorf("BUILD_ID should be generated when file doesn't exist") } }) diff --git a/pkg/pipelines/init.go b/pkg/pipelines/init.go index a30f64543..babc03337 100644 --- a/pkg/pipelines/init.go +++ b/pkg/pipelines/init.go @@ -8,6 +8,9 @@ import ( "runtime" "strings" + "github.com/windsorcli/cli/pkg/composer/artifact" + "github.com/windsorcli/cli/pkg/composer/blueprint" + "github.com/windsorcli/cli/pkg/composer/terraform" "github.com/windsorcli/cli/pkg/constants" "github.com/windsorcli/cli/pkg/context/config" envvars "github.com/windsorcli/cli/pkg/context/env" @@ -16,9 +19,6 @@ import ( "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/generators" terraforminfra "github.com/windsorcli/cli/pkg/provisioner/terraform" - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/composer/terraform" "github.com/windsorcli/cli/pkg/workstation/network" "github.com/windsorcli/cli/pkg/workstation/services" "github.com/windsorcli/cli/pkg/workstation/virt" diff --git a/pkg/runtime/runtime_loaders.go b/pkg/runtime/runtime_loaders.go index aea6295a9..29bc09724 100644 --- a/pkg/runtime/runtime_loaders.go +++ b/pkg/runtime/runtime_loaders.go @@ -7,14 +7,14 @@ import ( "path/filepath" secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets" + "github.com/windsorcli/cli/pkg/composer/blueprint" "github.com/windsorcli/cli/pkg/context/config" envvars "github.com/windsorcli/cli/pkg/context/env" + "github.com/windsorcli/cli/pkg/context/secrets" + "github.com/windsorcli/cli/pkg/context/shell" "github.com/windsorcli/cli/pkg/provisioner/cluster" "github.com/windsorcli/cli/pkg/provisioner/kubernetes" k8sclient "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" - "github.com/windsorcli/cli/pkg/composer/blueprint" - "github.com/windsorcli/cli/pkg/context/secrets" - "github.com/windsorcli/cli/pkg/context/shell" ) // ============================================================================= From 36cfe85b151d0d93edfd2c079ebc95d5b80e82f8 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sun, 2 Nov 2025 20:03:22 -0500 Subject: [PATCH 2/4] Set projectRoot etc. on NewContext Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/context/context.go | 63 +++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/pkg/context/context.go b/pkg/context/context.go index 6c6b899ad..8312a3d5b 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -110,6 +110,14 @@ func NewContext(ctx *ExecutionContext) (*ExecutionContext, error) { } } + projectRoot, err := ctx.Shell.GetProjectRoot() + if err != nil { + return nil, fmt.Errorf("failed to get project root: %w", err) + } + ctx.ProjectRoot = projectRoot + ctx.ConfigRoot = filepath.Join(ctx.ProjectRoot, "contexts", ctx.ContextName) + ctx.TemplateRoot = filepath.Join(ctx.ProjectRoot, "contexts", "_template") + if ctx.ConfigHandler == nil { if existing := injector.Resolve("configHandler"); existing != nil { if configHandler, ok := existing.(config.ConfigHandler); ok { @@ -254,18 +262,26 @@ func (ctx *ExecutionContext) GetAliases() map[string]string { // If no build ID exists, a new one is generated, persisted, and returned. // Returns the build ID string or an error if retrieval or persistence fails. func (ctx *ExecutionContext) GetBuildID() (string, error) { - projectRoot, err := ctx.Shell.GetProjectRoot() + projectRoot := ctx.ProjectRoot + + if err := os.MkdirAll(projectRoot, 0750); err != nil { + return "", fmt.Errorf("failed to create project root directory: %w", err) + } + + root, err := os.OpenRoot(projectRoot) if err != nil { - return "", fmt.Errorf("failed to get project root: %w", err) + return "", fmt.Errorf("failed to open project root: %w", err) } + defer root.Close() + + buildIDPath := ".windsor/.build-id" - buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") var buildID string - if _, err := os.Stat(buildIDPath); os.IsNotExist(err) { + if _, err := root.Stat(buildIDPath); os.IsNotExist(err) { buildID = "" } else { - data, err := os.ReadFile(buildIDPath) + data, err := root.ReadFile(buildIDPath) if err != nil { return "", fmt.Errorf("failed to read build ID file: %w", err) } @@ -424,19 +440,26 @@ func (ctx *ExecutionContext) loadSecrets() error { // writeBuildIDToFile writes the provided build ID string to the .windsor/.build-id file in the project root. // Ensures the .windsor directory exists before writing. Returns an error if directory creation or file write fails. func (ctx *ExecutionContext) writeBuildIDToFile(buildID string) error { - projectRoot, err := ctx.Shell.GetProjectRoot() + projectRoot := ctx.ProjectRoot + + if err := os.MkdirAll(projectRoot, 0750); err != nil { + return fmt.Errorf("failed to create project root directory: %w", err) + } + + root, err := os.OpenRoot(projectRoot) if err != nil { - return fmt.Errorf("failed to get project root: %w", err) + return fmt.Errorf("failed to open project root: %w", err) } + defer root.Close() - buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") - buildIDDir := filepath.Dir(buildIDPath) + buildIDPath := ".windsor/.build-id" + buildIDDir := ".windsor" - if err := os.MkdirAll(buildIDDir, 0755); err != nil { + if err := root.MkdirAll(buildIDDir, 0750); err != nil { return fmt.Errorf("failed to create build ID directory: %w", err) } - return os.WriteFile(buildIDPath, []byte(buildID), 0644) + return root.WriteFile(buildIDPath, []byte(buildID), 0600) } // generateBuildID generates and returns a build ID string in the format YYMMDD.RANDOM.#. @@ -451,18 +474,26 @@ func (ctx *ExecutionContext) generateBuildID() (string, error) { dd := now.Day() datePart := fmt.Sprintf("%02d%02d%02d", yy, mm, dd) - projectRoot, err := ctx.Shell.GetProjectRoot() + projectRoot := ctx.ProjectRoot + + if err := os.MkdirAll(projectRoot, 0750); err != nil { + return "", fmt.Errorf("failed to create project root directory: %w", err) + } + + root, err := os.OpenRoot(projectRoot) if err != nil { - return "", fmt.Errorf("failed to get project root: %w", err) + return "", fmt.Errorf("failed to open project root: %w", err) } + defer root.Close() + + buildIDPath := ".windsor/.build-id" - buildIDPath := filepath.Join(projectRoot, ".windsor", ".build-id") var existingBuildID string - if _, err := os.Stat(buildIDPath); os.IsNotExist(err) { + if _, err := root.Stat(buildIDPath); os.IsNotExist(err) { existingBuildID = "" } else { - data, err := os.ReadFile(buildIDPath) + data, err := root.ReadFile(buildIDPath) if err != nil { return "", fmt.Errorf("failed to read build ID file: %w", err) } From f5d3434c2c174f2ec938a823467cfc4940f7a0d0 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sun, 2 Nov 2025 20:04:56 -0500 Subject: [PATCH 3/4] Get build ID directly in bluepritn handler Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/composer/blueprint/blueprint_handler.go | 22 +++++++++------------ 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/pkg/composer/blueprint/blueprint_handler.go b/pkg/composer/blueprint/blueprint_handler.go index 2bd914bef..9f340db5b 100644 --- a/pkg/composer/blueprint/blueprint_handler.go +++ b/pkg/composer/blueprint/blueprint_handler.go @@ -18,13 +18,12 @@ import ( _ "embed" "github.com/goccy/go-yaml" - contextpkg "github.com/windsorcli/cli/pkg/context" - "github.com/windsorcli/cli/pkg/context/config" + "github.com/windsorcli/cli/pkg/composer/artifact" "github.com/windsorcli/cli/pkg/constants" + "github.com/windsorcli/cli/pkg/context/config" + "github.com/windsorcli/cli/pkg/context/shell" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/context/shell" "github.com/briandowns/spinner" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" @@ -1684,14 +1683,12 @@ func (b *BaseBlueprintHandler) applyValuesConfigMaps() error { mergedCommonValues["REGISTRY_URL"] = registryURL mergedCommonValues["LOCAL_VOLUME_PATH"] = localVolumePath - execCtx := &contextpkg.ExecutionContext{ - Shell: b.shell, - ConfigHandler: b.configHandler, - Injector: b.injector, - } - buildID, err := execCtx.GetBuildID() - if err != nil { - return fmt.Errorf("failed to get build ID: %w", err) + buildID := os.Getenv("BUILD_ID") + if buildID == "" && b.projectRoot != "" { + buildIDPath := filepath.Join(b.projectRoot, ".windsor", ".build-id") + if data, err := b.shims.ReadFile(buildIDPath); err == nil { + buildID = strings.TrimSpace(string(data)) + } } if buildID != "" { mergedCommonValues["BUILD_ID"] = buildID @@ -1845,7 +1842,6 @@ func (b *BaseBlueprintHandler) flattenValuesToConfigMap(values map[string]any, p return nil } - // deepMergeMaps returns a new map from a deep merge of base and overlay maps. // Overlay values take precedence; nested maps merge recursively. Non-map overlay values replace base values. func (b *BaseBlueprintHandler) deepMergeMaps(base, overlay map[string]any) map[string]any { From 48b8a22ac5e9a681bc6811c34685f542774ffa4b Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 3 Nov 2025 07:45:40 -0500 Subject: [PATCH 4/4] unset build_id in tests Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- .../blueprint/blueprint_handler_public_test.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/composer/blueprint/blueprint_handler_public_test.go b/pkg/composer/blueprint/blueprint_handler_public_test.go index 040e711d4..72ac6d0de 100644 --- a/pkg/composer/blueprint/blueprint_handler_public_test.go +++ b/pkg/composer/blueprint/blueprint_handler_public_test.go @@ -15,12 +15,12 @@ import ( sourcev1 "github.com/fluxcd/source-controller/api/v1" "github.com/goccy/go-yaml" blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" - "github.com/windsorcli/cli/pkg/context/config" + "github.com/windsorcli/cli/pkg/composer/artifact" "github.com/windsorcli/cli/pkg/constants" + "github.com/windsorcli/cli/pkg/context/config" + "github.com/windsorcli/cli/pkg/context/shell" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/provisioner/kubernetes" - "github.com/windsorcli/cli/pkg/composer/artifact" - "github.com/windsorcli/cli/pkg/context/shell" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -297,6 +297,9 @@ func setupShims(t *testing.T) *Shims { func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { t.Helper() + // Unset BUILD_ID to ensure tests aren't affected by environment + os.Unsetenv("BUILD_ID") + // Create temporary directory for test tmpDir, err := os.MkdirTemp("", "blueprint-test-*") if err != nil { @@ -444,6 +447,7 @@ contexts: t.Cleanup(func() { os.Unsetenv("WINDSOR_PROJECT_ROOT") os.Unsetenv("WINDSOR_CONTEXT") + os.Unsetenv("BUILD_ID") os.Chdir(tmpDir) })