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
25 changes: 13 additions & 12 deletions docs/guides/terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ To ensure compatibility with Windsor, your Terraform project should adhere to a

```plaintext
.windsor/
└── .tf_modules/
├── cluster/
│ └── talos/
│ ├── main.tf
│ └── variables.tf
└── gitops/
└── flux/
├── main.tf
└── variables.tf
└── contexts/
└── local/
├── cluster/
│ └── talos/
│ ├── main.tf
│ └── variables.tf
└── gitops/
└── flux/
├── main.tf
└── variables.tf
contexts/
└── local/
├── .terraform/
Expand Down Expand Up @@ -69,7 +70,7 @@ kustomize:
path: ""
```

Modules like `cluster/talos` and `gitops/flux` are remote, with shims in `.windsor/.tf_modules/cluster/talos` and `.windsor/.tf_modules/gitops/flux`. Running `windsor up` applies these modules sequentially.
Modules like `cluster/talos` and `gitops/flux` are remote, with shims in `.windsor/contexts/<context>/cluster/talos` and `.windsor/contexts/<context>/gitops/flux`. Running `windsor up` applies these modules sequentially.

Store your Terraform code in a `terraform/` folder within your project. To reference it in `blueprint.yaml`, add a section without a `source` field. For example, if your code is in `terraform/example/my-app`, add:

Expand All @@ -85,9 +86,9 @@ terraform:
Now, running `windsor up` will execute your module after the `gitops/flux` module.

## Importing Resources
The Windsor CLI offers a unique method for importing and using remote Terraform modules. Running `windsor init local` unpacks shims that reference basic modules from Windsor's [core blueprint](https://github.com/windsorcli/core), stored in `.windsor/.tf_modules`.
The Windsor CLI offers a unique method for importing and using remote Terraform modules. Running `windsor init local` unpacks shims that reference basic modules from Windsor's [core blueprint](https://github.com/windsorcli/core), stored in `.windsor/contexts/<context>/`.

Think of the `tf_modules` folder as the remote module counterpart to the local `terraform` folder. Variables for these modules are located in `contexts/<context-name>/terraform/path/to/module.tfvars`.
Think of the `contexts/<context>/` folder as the remote module counterpart to the local `terraform` folder. Variables for these modules are located in `.windsor/contexts/<context>/path/to/module/terraform.tfvars`.

## Terraform CLI Assistance

Expand Down
2 changes: 1 addition & 1 deletion pkg/composer/blueprint/blueprint_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -1266,7 +1266,7 @@ func (b *BaseBlueprintHandler) resolveComponentPaths(blueprint *blueprintv1alpha
componentCopy := component

if b.isValidTerraformRemoteSource(componentCopy.Source) || b.isOCISource(componentCopy.Source) || strings.HasPrefix(componentCopy.Source, "file://") {
componentCopy.FullPath = filepath.Join(projectRoot, ".windsor", ".tf_modules", componentCopy.Path)
componentCopy.FullPath = filepath.Join(projectRoot, ".windsor", "contexts", b.runtime.ContextName, "terraform", componentCopy.Path)
} else {
componentCopy.FullPath = filepath.Join(projectRoot, "terraform", componentCopy.Path)
}
Expand Down
10 changes: 6 additions & 4 deletions pkg/composer/blueprint/blueprint_handler_private_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,7 @@ func TestBaseBlueprintHandler_resolveComponentPaths(t *testing.T) {
// Given a handler with components using remote sources
handler := setup(t)
handler.runtime.ProjectRoot = "/test/project"
handler.runtime.ContextName = "local"
blueprint := &blueprintv1alpha1.Blueprint{
TerraformComponents: []blueprintv1alpha1.TerraformComponent{
{
Expand All @@ -548,8 +549,8 @@ func TestBaseBlueprintHandler_resolveComponentPaths(t *testing.T) {
// When resolving component paths
handler.resolveComponentPaths(blueprint)

// Then remote source components should use .windsor/.tf_modules path
if blueprint.TerraformComponents[0].FullPath != filepath.Join("/test/project", ".windsor", ".tf_modules", "test-module") {
// Then remote source components should use .windsor/contexts/<context> path
if blueprint.TerraformComponents[0].FullPath != filepath.Join("/test/project", ".windsor", "contexts", "local", "terraform", "test-module") {
t.Errorf("Expected FullPath for remote source, got: %s", blueprint.TerraformComponents[0].FullPath)
}
})
Expand All @@ -558,6 +559,7 @@ func TestBaseBlueprintHandler_resolveComponentPaths(t *testing.T) {
// Given a handler with components using OCI sources
handler := setup(t)
handler.runtime.ProjectRoot = "/test/project"
handler.runtime.ContextName = "local"
handler.blueprint = blueprintv1alpha1.Blueprint{
Sources: []blueprintv1alpha1.Source{
{
Expand All @@ -578,8 +580,8 @@ func TestBaseBlueprintHandler_resolveComponentPaths(t *testing.T) {
// When resolving component paths
handler.resolveComponentPaths(blueprint)

// Then OCI source components should use .windsor/.tf_modules path
if blueprint.TerraformComponents[0].FullPath != filepath.Join("/test/project", ".windsor", ".tf_modules", "test-module") {
// Then OCI source components should use .windsor/contexts/<context> path
if blueprint.TerraformComponents[0].FullPath != filepath.Join("/test/project", ".windsor", "contexts", "local", "terraform", "test-module") {
t.Errorf("Expected FullPath for OCI source, got: %s", blueprint.TerraformComponents[0].FullPath)
}
})
Expand Down
8 changes: 5 additions & 3 deletions pkg/composer/blueprint/blueprint_handler_public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ func setupBlueprintMocks(t *testing.T, opts ...func(*BlueprintTestMocks)) *Bluep
TemplateRoot: filepath.Join(tmpDir, "contexts", "_template"),
ConfigHandler: configHandler,
Shell: mockShell,
ContextName: "local",
}

// Create mocks struct
Expand Down Expand Up @@ -605,7 +606,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) {
}

// And the component should have the correct full path
expectedPath := filepath.FromSlash(filepath.Join(projectRoot, ".windsor", ".tf_modules", "path/to/module"))
expectedPath := filepath.FromSlash(filepath.Join(projectRoot, ".windsor", "contexts", "local", "terraform", "path/to/module"))
if resolvedComponents[0].FullPath != expectedPath {
t.Errorf("Expected path %q, got %q", expectedPath, resolvedComponents[0].FullPath)
}
Expand Down Expand Up @@ -679,7 +680,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) {
}

// And the component should have the correct full path with backslashes preserved
expectedPath := filepath.Join(projectRoot, ".windsor", ".tf_modules", "path\\to\\module")
expectedPath := filepath.Join(projectRoot, ".windsor", "contexts", "local", "terraform", "path\\to\\module")
if resolvedComponents[0].FullPath != expectedPath {
t.Errorf("Expected path %q, got %q", expectedPath, resolvedComponents[0].FullPath)
}
Expand All @@ -692,6 +693,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) {
// And a project root directory
projectRoot := "/test/project"
handler.runtime.ProjectRoot = projectRoot
handler.runtime.ContextName = "local"

// And an OCI source
sources := []blueprintv1alpha1.Source{
Expand Down Expand Up @@ -729,7 +731,7 @@ func TestBlueprintHandler_GetTerraformComponents(t *testing.T) {
}

// And the component should have the correct full path
expectedPath := filepath.FromSlash(filepath.Join(projectRoot, ".windsor", ".tf_modules", "cluster/talos"))
expectedPath := filepath.FromSlash(filepath.Join(projectRoot, ".windsor", "contexts", "local", "terraform", "cluster/talos"))
if resolvedComponents[0].FullPath != expectedPath {
t.Errorf("Expected path %q, got %q", expectedPath, resolvedComponents[0].FullPath)
}
Expand Down
16 changes: 8 additions & 8 deletions pkg/composer/terraform/archive_module_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func TestArchiveModuleResolver_ProcessModules(t *testing.T) {
{
Path: "test-module",
Source: "file://" + archivePath + "//terraform/test-module",
FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module"),
FullPath: filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module"),
},
}
}
Expand All @@ -134,7 +134,7 @@ func TestArchiveModuleResolver_ProcessModules(t *testing.T) {
}

// And the module directory should be created
moduleDir := filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module")
moduleDir := filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module")
if _, err := os.Stat(moduleDir); err != nil {
t.Errorf("Expected module directory to be created, got error: %v", err)
}
Expand Down Expand Up @@ -180,7 +180,7 @@ func TestArchiveModuleResolver_ProcessModules(t *testing.T) {
{
Path: "test-module",
Source: "file:///invalid/path.tar.gz//terraform/test-module",
FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module"),
FullPath: filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module"),
},
}
}
Expand Down Expand Up @@ -1130,7 +1130,7 @@ func TestArchiveModuleResolver_processComponent(t *testing.T) {
component := blueprintv1alpha1.TerraformComponent{
Path: "test-module",
Source: "file:///invalid/path.tar.gz//terraform/test-module",
FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module"),
FullPath: filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module"),
}

// Override MkdirAll to succeed
Expand Down Expand Up @@ -1159,7 +1159,7 @@ func TestArchiveModuleResolver_processComponent(t *testing.T) {
component := blueprintv1alpha1.TerraformComponent{
Path: "test-module",
Source: "file://" + archivePath + "//terraform/test-module",
FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module"),
FullPath: filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module"),
}

// Override shims for real file operations
Expand Down Expand Up @@ -1200,7 +1200,7 @@ func TestArchiveModuleResolver_processComponent(t *testing.T) {
component := blueprintv1alpha1.TerraformComponent{
Path: "test-module",
Source: "file://" + archivePath + "//terraform/test-module",
FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module"),
FullPath: filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module"),
}

// Override shims for real file operations
Expand Down Expand Up @@ -1246,7 +1246,7 @@ func TestArchiveModuleResolver_processComponent(t *testing.T) {
component := blueprintv1alpha1.TerraformComponent{
Path: "test-module",
Source: "file://" + archivePath + "//terraform/test-module",
FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module"),
FullPath: filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module"),
}

// Override shims for real file operations
Expand Down Expand Up @@ -1293,7 +1293,7 @@ func TestArchiveModuleResolver_processComponent(t *testing.T) {
component := blueprintv1alpha1.TerraformComponent{
Path: "test-module",
Source: "file://" + archivePath + "//terraform/test-module",
FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module"),
FullPath: filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module"),
}

// Override shims for real file operations
Expand Down
8 changes: 5 additions & 3 deletions pkg/composer/terraform/composite_module_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestCompositeModuleResolver_ProcessModules(t *testing.T) {
{
Path: "oci-module",
Source: "oci://registry.example.com/module:latest//terraform/oci-module",
FullPath: filepath.Join(tmpDir, ".windsor", ".tf_modules", "oci-module"),
FullPath: filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "oci-module"),
},
{
Path: "standard-module",
Expand Down Expand Up @@ -287,8 +287,9 @@ func TestCompositeModuleResolver_GenerateTfvars(t *testing.T) {
resolver, mocks := setup(t)
tmpDir := t.TempDir()
mocks.Runtime.ProjectRoot = tmpDir
mocks.Runtime.ContextName = "local"

moduleDir := filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module")
moduleDir := filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module")
if err := os.MkdirAll(moduleDir, 0755); err != nil {
t.Fatalf("Failed to create module directory: %v", err)
}
Expand Down Expand Up @@ -332,8 +333,9 @@ func TestCompositeModuleResolver_GenerateTfvars(t *testing.T) {
resolver, mocks := setup(t)
tmpDir := t.TempDir()
mocks.Runtime.ProjectRoot = tmpDir
mocks.Runtime.ContextName = "local"

moduleDir := filepath.Join(tmpDir, ".windsor", ".tf_modules", "test-module")
moduleDir := filepath.Join(tmpDir, ".windsor", "contexts", "local", "terraform", "test-module")
if err := os.MkdirAll(moduleDir, 0755); err != nil {
t.Fatalf("Failed to create module directory: %v", err)
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/composer/terraform/module_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func (h *BaseModuleResolver) parseVariablesFile(variablesTfPath string, protecte
}

// generateComponentTfvars generates tfvars files for a single Terraform component.
// All components write tfvars files to .windsor/.tf_modules/<component.Path>/terraform.tfvars,
// All components write tfvars files to .windsor/contexts/<context>/terraform/<component.Path>/terraform.tfvars,
// regardless of whether they have a Source (remote) or not (local). This unifies the behavior
// between local templates and OCI artifacts, preventing writes to the contexts folder.
// Returns an error if variables.tf cannot be found or if tfvars file generation fails.
Expand All @@ -200,7 +200,7 @@ func (h *BaseModuleResolver) generateComponentTfvars(projectRoot string, compone
return fmt.Errorf("failed to find variables.tf for component %s: %w", component.Path, err)
}

moduleTfvarsPath := filepath.Join(projectRoot, ".windsor", ".tf_modules", component.Path, "terraform.tfvars")
moduleTfvarsPath := filepath.Join(projectRoot, ".windsor", "contexts", h.runtime.ContextName, "terraform", component.Path, "terraform.tfvars")
if err := h.removeTfvarsFiles(filepath.Dir(moduleTfvarsPath)); err != nil {
return fmt.Errorf("failed cleaning existing .tfvars in module dir %s: %w", filepath.Dir(moduleTfvarsPath), err)
}
Expand All @@ -212,14 +212,14 @@ func (h *BaseModuleResolver) generateComponentTfvars(projectRoot string, compone
}

// findVariablesTfFileForComponent returns the path to the variables.tf file for the specified Terraform component.
// If the component has a non-empty Source, the path is .windsor/.tf_modules/<component.Path>/variables.tf under the project root.
// If the component has a non-empty Source, the path is .windsor/contexts/<context>/terraform/<component.Path>/variables.tf under the project root.
// If the component has an empty Source, the path is terraform/<component.Path>/variables.tf under the project root.
// Returns the variables.tf file path if it exists, or an error if not found.
func (h *BaseModuleResolver) findVariablesTfFileForComponent(projectRoot string, component blueprintv1alpha1.TerraformComponent) (string, error) {
var variablesTfPath string

if component.Source != "" {
variablesTfPath = filepath.Join(projectRoot, ".windsor", ".tf_modules", component.Path, "variables.tf")
variablesTfPath = filepath.Join(projectRoot, ".windsor", "contexts", h.runtime.ContextName, "terraform", component.Path, "variables.tf")
} else {
variablesTfPath = filepath.Join(projectRoot, "terraform", component.Path, "variables.tf")
}
Expand Down
Loading
Loading