-
-
Notifications
You must be signed in to change notification settings - Fork 153
Expand file tree
/
Copy pathstack_processor_locals.go
More file actions
210 lines (179 loc) · 6.45 KB
/
stack_processor_locals.go
File metadata and controls
210 lines (179 loc) · 6.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package exec
import (
"fmt"
errUtils "github.com/cloudposse/atmos/errors"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/locals"
"github.com/cloudposse/atmos/pkg/perf"
"github.com/cloudposse/atmos/pkg/schema"
)
// ExtractAndResolveLocals extracts and resolves locals from a section.
// Returns resolved locals map (merged with parent locals) or nil if no locals section.
// If there's an error resolving locals, it returns the error.
func ExtractAndResolveLocals(
atmosConfig *schema.AtmosConfiguration,
section map[string]any,
parentLocals map[string]any,
filePath string,
) (map[string]any, error) {
defer perf.Track(atmosConfig, "exec.ExtractAndResolveLocals")()
if section == nil {
return copyParentLocals(parentLocals), nil
}
// Check for locals section.
localsRaw, exists := section[cfg.LocalsSectionName]
if !exists {
return copyParentLocals(parentLocals), nil
}
// Locals must be a map.
localsMap, ok := localsRaw.(map[string]any)
if !ok {
return nil, fmt.Errorf("%w in %s", errUtils.ErrLocalsInvalidType, filePath)
}
// Handle empty locals section.
if len(localsMap) == 0 {
return copyOrCreateParentLocals(parentLocals), nil
}
// Resolve locals with dependency ordering and cycle detection.
return resolveLocalsWithDependencies(localsMap, parentLocals, filePath)
}
// copyParentLocals creates a copy of parent locals or returns nil if no parent locals.
func copyParentLocals(parentLocals map[string]any) map[string]any {
if parentLocals == nil {
return nil
}
result := make(map[string]any, len(parentLocals))
for k, v := range parentLocals {
result[k] = v
}
return result
}
// copyOrCreateParentLocals creates a copy of parent locals or an empty map if nil.
func copyOrCreateParentLocals(parentLocals map[string]any) map[string]any {
if parentLocals == nil {
return make(map[string]any)
}
result := make(map[string]any, len(parentLocals))
for k, v := range parentLocals {
result[k] = v
}
return result
}
// resolveLocalsWithDependencies resolves locals using dependency ordering and cycle detection.
func resolveLocalsWithDependencies(localsMap, parentLocals map[string]any, filePath string) (map[string]any, error) {
resolver := locals.NewResolver(localsMap, filePath)
resolved, err := resolver.Resolve(parentLocals)
if err != nil {
return nil, err
}
return resolved, nil
}
// ProcessStackLocals extracts and resolves all locals from a stack config file.
// Returns a LocalsContext with resolved locals at each scope (global, terraform, helmfile, packer).
// Component-level locals are processed separately during component processing.
func ProcessStackLocals(
atmosConfig *schema.AtmosConfiguration,
stackConfigMap map[string]any,
filePath string,
) (*LocalsContext, error) {
defer perf.Track(atmosConfig, "exec.ProcessStackLocals")()
ctx := &LocalsContext{}
// Extract global locals (available to all sections).
globalLocals, err := ExtractAndResolveLocals(atmosConfig, stackConfigMap, nil, filePath)
if err != nil {
return nil, fmt.Errorf("failed to resolve global locals: %w", err)
}
ctx.Global = globalLocals
// Extract terraform section locals (inherit from global).
if terraformSection, ok := stackConfigMap[cfg.TerraformSectionName].(map[string]any); ok {
terraformLocals, err := ExtractAndResolveLocals(atmosConfig, terraformSection, ctx.Global, filePath)
if err != nil {
return nil, fmt.Errorf("failed to resolve terraform locals: %w", err)
}
ctx.Terraform = terraformLocals
} else {
ctx.Terraform = ctx.Global
}
// Extract helmfile section locals (inherit from global).
if helmfileSection, ok := stackConfigMap[cfg.HelmfileSectionName].(map[string]any); ok {
helmfileLocals, err := ExtractAndResolveLocals(atmosConfig, helmfileSection, ctx.Global, filePath)
if err != nil {
return nil, fmt.Errorf("failed to resolve helmfile locals: %w", err)
}
ctx.Helmfile = helmfileLocals
} else {
ctx.Helmfile = ctx.Global
}
// Extract packer section locals (inherit from global).
if packerSection, ok := stackConfigMap[cfg.PackerSectionName].(map[string]any); ok {
packerLocals, err := ExtractAndResolveLocals(atmosConfig, packerSection, ctx.Global, filePath)
if err != nil {
return nil, fmt.Errorf("failed to resolve packer locals: %w", err)
}
ctx.Packer = packerLocals
} else {
ctx.Packer = ctx.Global
}
return ctx, nil
}
// LocalsContext holds resolved locals at different scopes within a stack file.
// This is used to pass locals context during template processing.
type LocalsContext struct {
// Global holds locals defined at the stack file root level.
Global map[string]any
// Terraform holds locals from the terraform section (merged with global).
Terraform map[string]any
// Helmfile holds locals from the helmfile section (merged with global).
Helmfile map[string]any
// Packer holds locals from the packer section (merged with global).
Packer map[string]any
}
// GetForComponentType returns the appropriate locals for a given component type.
func (ctx *LocalsContext) GetForComponentType(componentType string) map[string]any {
defer perf.Track(nil, "exec.LocalsContext.GetForComponentType")()
if ctx == nil {
return nil
}
switch componentType {
case cfg.TerraformSectionName:
return ctx.Terraform
case cfg.HelmfileSectionName:
return ctx.Helmfile
case cfg.PackerSectionName:
return ctx.Packer
default:
return ctx.Global
}
}
// ResolveComponentLocals resolves locals for a specific component.
// It merges component-level locals with the parent scope (component-type or global).
func ResolveComponentLocals(
atmosConfig *schema.AtmosConfiguration,
componentConfig map[string]any,
parentLocals map[string]any,
filePath string,
) (map[string]any, error) {
defer perf.Track(atmosConfig, "exec.ResolveComponentLocals")()
return ExtractAndResolveLocals(atmosConfig, componentConfig, parentLocals, filePath)
}
// StripLocalsFromSection removes the locals section from a map.
// This is used to prevent locals from being merged across file boundaries
// and from appearing in the final component output.
func StripLocalsFromSection(section map[string]any) map[string]any {
defer perf.Track(nil, "exec.StripLocalsFromSection")()
if section == nil {
return nil
}
// If no locals section, return as-is.
if _, exists := section[cfg.LocalsSectionName]; !exists {
return section
}
// Create a copy without locals.
result := make(map[string]any, len(section)-1)
for k, v := range section {
if k != cfg.LocalsSectionName {
result[k] = v
}
}
return result
}