Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions pkg/blueprint/blueprint_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (

type BlueprintHandler interface {
Initialize() error
LoadBlueprint() error
LoadConfig() error
LoadData(data map[string]any, ociInfo ...*artifact.OCIArtifactInfo) error
Write(overwrite ...bool) error
Expand Down Expand Up @@ -131,6 +132,81 @@ func (b *BaseBlueprintHandler) Initialize() error {
return nil
}

// LoadBlueprint loads all blueprint data into memory, establishing defaults from either templates
// or OCI artifacts, then applies any local blueprint.yaml overrides to ensure the correct precedence.
// All sources are processed and merged into the in-memory runtime state.
// Returns an error if any required paths are inaccessible or any loading operation fails.
func (b *BaseBlueprintHandler) LoadBlueprint() error {
if _, err := b.shims.Stat(b.templateRoot); err == nil {
if _, err := b.GetLocalTemplateData(); err != nil {
return fmt.Errorf("failed to get local template data: %w", err)
}
} else {
effectiveBlueprintURL := constants.GetEffectiveBlueprintURL()
ociInfo, err := artifact.ParseOCIReference(effectiveBlueprintURL)
if err != nil {
return fmt.Errorf("failed to parse default blueprint reference: %w", err)
}
if ociInfo == nil {
return fmt.Errorf("invalid default blueprint reference: %s", effectiveBlueprintURL)
}
artifactBuilder := b.injector.Resolve("artifactBuilder")
if artifactBuilder == nil {
return fmt.Errorf("artifact builder not available")
}
ab, ok := artifactBuilder.(artifact.Artifact)
if !ok {
return fmt.Errorf("artifact builder has wrong type")
}
templateData, err := ab.GetTemplateData(ociInfo.URL)
if err != nil {
return fmt.Errorf("failed to get template data from default blueprint: %w", err)
}
blueprintData := make(map[string]any)
for key, value := range templateData {
blueprintData[key] = string(value)
}
if err := b.LoadData(blueprintData, ociInfo); err != nil {
return fmt.Errorf("failed to load default blueprint data: %w", err)
}
}

sources := b.GetSources()
if len(sources) > 0 {
artifactBuilder := b.injector.Resolve("artifactBuilder")
if artifactBuilder != nil {
if ab, ok := artifactBuilder.(artifact.Artifact); ok {
var ociURLs []string
for _, source := range sources {
if strings.HasPrefix(source.Url, "oci://") {
ociURLs = append(ociURLs, source.Url)
}
}
if len(ociURLs) > 0 {
_, err := ab.Pull(ociURLs)
if err != nil {
return fmt.Errorf("failed to load OCI sources: %w", err)
}
}
}
}
}

configRoot, err := b.configHandler.GetConfigRoot()
if err != nil {
return fmt.Errorf("error getting config root: %w", err)
}

blueprintPath := filepath.Join(configRoot, "blueprint.yaml")
if _, err := b.shims.Stat(blueprintPath); err == nil {
if err := b.LoadConfig(); err != nil {
return fmt.Errorf("failed to load blueprint config overrides: %w", err)
}
}

return nil
}

// LoadConfig reads blueprint configuration from blueprint.yaml file.
// Returns an error if blueprint.yaml does not exist.
// Template processing is now handled by the pkg/template package.
Expand Down
60 changes: 60 additions & 0 deletions pkg/blueprint/blueprint_handler_public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3792,6 +3792,66 @@ func TestBaseBlueprintHandler_SetRenderedKustomizeData(t *testing.T) {
})
}

func TestBlueprintHandler_LoadBlueprint(t *testing.T) {
t.Run("LoadsBlueprintSuccessfullyWithLocalTemplates", func(t *testing.T) {
// Given a blueprint handler with local templates
mocks := setupMocks(t)
handler := NewBlueprintHandler(mocks.Injector)
if err := handler.Initialize(); err != nil {
t.Fatalf("Initialize() failed: %v", err)
}
// Set up shims after initialization
handler.shims = mocks.Shims

// Set up project root and create template root directory
tmpDir := t.TempDir()
mocks.Shell.GetProjectRootFunc = func() (string, error) {
return tmpDir, nil
}
templateRoot := filepath.Join(tmpDir, "contexts", "_template")
if err := os.MkdirAll(templateRoot, 0755); err != nil {
t.Fatalf("Failed to create template root: %v", err)
}

// Create a basic blueprint.yaml in templates
blueprintContent := `apiVersion: v1alpha1
kind: Blueprint
metadata:
name: test-blueprint
description: Test blueprint
sources: []
terraformComponents: []
kustomizations: []`

blueprintPath := filepath.Join(templateRoot, "blueprint.yaml")
if err := os.WriteFile(blueprintPath, []byte(blueprintContent), 0644); err != nil {
t.Fatalf("Failed to create blueprint.yaml: %v", err)
}

// Mock config handler to return empty context values
mocks.ConfigHandler.(*config.MockConfigHandler).GetContextValuesFunc = func() (map[string]any, error) {
return map[string]any{}, nil
}
mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string {
return "test-context"
}

// When loading blueprint
err := handler.LoadBlueprint()

// Then should succeed
if err != nil {
t.Errorf("Expected no error, got %v", err)
}

// And blueprint should be loaded
metadata := handler.GetMetadata()
if metadata.Name != "test-blueprint" {
t.Errorf("Expected blueprint name 'test-blueprint', got %s", metadata.Name)
}
})
}

func TestBaseBlueprintHandler_GetLocalTemplateData(t *testing.T) {
t.Run("CollectsBlueprintAndFeatureFiles", func(t *testing.T) {
projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT")
Expand Down
9 changes: 9 additions & 0 deletions pkg/blueprint/mock_blueprint_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
// MockBlueprintHandler is a mock implementation of BlueprintHandler interface for testing
type MockBlueprintHandler struct {
InitializeFunc func() error
LoadBlueprintFunc func() error
LoadConfigFunc func() error
LoadDataFunc func(data map[string]any, ociInfo ...*artifact.OCIArtifactInfo) error
WriteFunc func(overwrite ...bool) error
Expand Down Expand Up @@ -48,6 +49,14 @@ func (m *MockBlueprintHandler) Initialize() error {
return nil
}

// LoadBlueprint calls the mock LoadBlueprintFunc if set, otherwise returns nil
func (m *MockBlueprintHandler) LoadBlueprint() error {
if m.LoadBlueprintFunc != nil {
return m.LoadBlueprintFunc()
}
return nil
}

// LoadConfig calls the mock LoadConfigFunc if set, otherwise returns nil
func (m *MockBlueprintHandler) LoadConfig() error {
if m.LoadConfigFunc != nil {
Expand Down
66 changes: 53 additions & 13 deletions pkg/runtime/runtime_loaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"path/filepath"

secretsConfigType "github.com/windsorcli/cli/api/v1alpha1/secrets"
"github.com/windsorcli/cli/pkg/artifact"
"github.com/windsorcli/cli/pkg/blueprint"
"github.com/windsorcli/cli/pkg/cluster"
"github.com/windsorcli/cli/pkg/config"
"github.com/windsorcli/cli/pkg/env"
Expand All @@ -25,8 +27,8 @@ func (r *Runtime) LoadShell() *Runtime {

if r.Shell == nil {
r.Shell = shell.NewDefaultShell(r.Injector)
r.Injector.Register("shell", r.Shell)
}
r.Injector.Register("shell", r.Shell)
return r
}

Expand All @@ -42,10 +44,11 @@ func (r *Runtime) LoadConfigHandler() *Runtime {

if r.ConfigHandler == nil {
r.ConfigHandler = config.NewConfigHandler(r.Injector)
if err := r.ConfigHandler.Initialize(); err != nil {
r.err = fmt.Errorf("failed to initialize config handler: %w", err)
return r
}
}
r.Injector.Register("configHandler", r.ConfigHandler)
if err := r.ConfigHandler.Initialize(); err != nil {
r.err = fmt.Errorf("failed to initialize config handler: %w", err)
return r
}
return r
}
Expand Down Expand Up @@ -143,11 +146,7 @@ func (r *Runtime) LoadSecretsProviders() *Runtime {
return r
}

// LoadKubernetes loads and initializes Kubernetes and cluster client dependencies for the Runtime.
// It creates and registers the Kubernetes client, cluster client, and Kubernetes manager in the Injector,
// then initializes the Kubernetes manager to establish connections to Kubernetes API and provider APIs.
// If any dependency is missing, it is constructed and registered. If initialization fails, it sets r.err and returns.
// Returns the Runtime instance with updated dependencies and error state.
// LoadKubernetes loads and initializes Kubernetes and cluster client dependencies.
func (r *Runtime) LoadKubernetes() *Runtime {
if r.err != nil {
return r
Expand All @@ -161,23 +160,64 @@ func (r *Runtime) LoadKubernetes() *Runtime {
r.Injector.Register("kubernetesClient", kubernetesClient)
}

if r.ConfigHandler.GetString("cluster.driver") == "talos" {
driver := r.ConfigHandler.GetString("cluster.driver")
if driver == "" {
return r
}
if driver == "talos" {
if r.ClusterClient == nil {
r.ClusterClient = cluster.NewTalosClusterClient(r.Injector)
r.Injector.Register("clusterClient", r.ClusterClient)
}
} else {
r.err = fmt.Errorf("unsupported cluster driver: %s", r.ConfigHandler.GetString("cluster.driver"))
r.err = fmt.Errorf("unsupported cluster driver: %s", driver)
return r
}

if r.K8sManager == nil {
r.K8sManager = kubernetes.NewKubernetesManager(r.Injector)
r.Injector.Register("kubernetesManager", r.K8sManager)
}
r.Injector.Register("kubernetesManager", r.K8sManager)
if err := r.K8sManager.Initialize(); err != nil {
r.err = fmt.Errorf("failed to initialize kubernetes manager: %w", err)
return r
}
return r
}

// LoadBlueprint initializes and configures all runtime dependencies necessary for blueprint processing.
// It creates and registers the blueprint handler and artifact builder if they do not already exist,
// then initializes each component to provide template processing, OCI artifact loading, and blueprint
// data management. All dependencies are injected and registered as needed. If any error occurs during
// initialization, the error is set in the runtime and the method returns. Returns the Runtime instance
// with updated dependencies and error state.
func (r *Runtime) LoadBlueprint() *Runtime {
if r.err != nil {
return r
}
if r.ConfigHandler == nil {
r.err = fmt.Errorf("config handler not loaded - call LoadConfigHandler() first")
return r
}
if r.BlueprintHandler == nil {
r.BlueprintHandler = blueprint.NewBlueprintHandler(r.Injector)
r.Injector.Register("blueprintHandler", r.BlueprintHandler)
}
if r.ArtifactBuilder == nil {
r.ArtifactBuilder = artifact.NewArtifactBuilder()
r.Injector.Register("artifactBuilder", r.ArtifactBuilder)
}
if err := r.BlueprintHandler.Initialize(); err != nil {
r.err = fmt.Errorf("failed to initialize blueprint handler: %w", err)
return r
}
if err := r.ArtifactBuilder.Initialize(r.Injector); err != nil {
r.err = fmt.Errorf("failed to initialize artifact builder: %w", err)
return r
}
if err := r.BlueprintHandler.LoadBlueprint(); err != nil {
r.err = fmt.Errorf("failed to load blueprint data: %w", err)
return r
}
return r
}
Loading
Loading