-
-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathenumerate.go
More file actions
176 lines (158 loc) · 6.71 KB
/
Copy pathenumerate.go
File metadata and controls
176 lines (158 loc) · 6.71 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
package secret
import (
"fmt"
"sort"
"github.com/spf13/cobra"
"github.com/spf13/viper"
errUtils "github.com/cloudposse/atmos/errors"
e "github.com/cloudposse/atmos/internal/exec"
cfg "github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/perf"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/secrets"
)
// scopeEntry is a single (stack, component) instance that declares one or more secrets, paired
// with its resolved component section (declarations carry their derived scope after stack merge).
type scopeEntry struct {
Stack string
Component string
ComponentType string
Section map[string]any
}
// enumerateScopesFn is a seam so tests can inject scope entries without real stack processing.
var enumerateScopesFn = enumerateSecretScopes
// enumerateSecretScopes lists every (stack, component) instance that declares secrets, narrowed by
// the given facets (an empty Stack/Component means "all"). It resolves the stack manifests once via
// describe-stacks with auth disabled and all credentialed read functions skipped (see
// credentialFreeSkip) — declarations and their derived scope are available without retrieving any
// secret values or reading any remote backend.
func enumerateSecretScopes(facet secretScope) ([]scopeEntry, *schema.AtmosConfiguration, error) {
defer perf.Track(nil, "secret.enumerateSecretScopes")()
atmosConfig, err := cfg.InitCliConfig(schema.ConfigAndStacksInfo{Stack: facet.Stack}, true)
if err != nil {
return nil, nil, fmt.Errorf("%w: %w", errUtils.ErrFailedToInitConfig, err)
}
var components []string
if facet.Component != "" {
components = []string{facet.Component}
}
// Listing only reads the static `secrets.vars` declarations (see collectSecretScopeEntries →
// secrets.ExtractDeclarations); it never retrieves a secret value, so per-component auth is
// pure overhead. Disable it explicitly so a 72-component stack doesn't run 72 auth cycles
// (credentials-file rewrite + keyring rebuild) just to enumerate declarations.
//
// With auth disabled, every credentialed read function must also be skipped: an evaluated
// `!terraform.state`/`!terraform.output`/`!store` would fall back to the default AWS chain and
// fail (e.g. an unreachable EC2 IMDS endpoint) even though enumeration never needs the resolved
// value. See credentialFreeSkip.
stacksMap, err := e.ExecuteDescribeStacksWithAuthDisabled(
&atmosConfig, facet.Stack, components, nil, nil,
false, true, true, false, credentialFreeSkip(), nil, true,
)
if err != nil {
return nil, nil, err
}
return collectSecretScopeEntries(stacksMap, facet.Component), &atmosConfig, nil
}
// collectSecretScopeEntries traverses the describe-stacks map
// (stack -> components -> <type> -> component -> section) and keeps the instances that declare
// secrets, optionally narrowed to a single component. Entries are sorted by stack, component,
// then component type so a name shared across component types has deterministic ordering.
func collectSecretScopeEntries(stacksMap map[string]any, componentFilter string) []scopeEntry {
var entries []scopeEntry
for stackName, raw := range stacksMap {
stackMap, ok := raw.(map[string]any)
if !ok {
continue
}
entries = append(entries, secretEntriesInStack(stackName, stackMap, componentFilter)...)
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].Stack != entries[j].Stack {
return entries[i].Stack < entries[j].Stack
}
if entries[i].Component != entries[j].Component {
return entries[i].Component < entries[j].Component
}
return entries[i].ComponentType < entries[j].ComponentType
})
return entries
}
// secretEntriesInStack returns the secret-declaring instances within a single stack's describe map
// (components -> <type> -> component -> section), optionally narrowed to componentFilter.
func secretEntriesInStack(stackName string, stackMap map[string]any, componentFilter string) []scopeEntry {
comps, ok := stackMap[cfg.ComponentsSectionName].(map[string]any)
if !ok {
return nil
}
var entries []scopeEntry
for componentType, typeRaw := range comps {
typeMap, ok := typeRaw.(map[string]any)
if !ok {
continue
}
for compName, secRaw := range typeMap {
if componentFilter != "" && compName != componentFilter {
continue
}
section, ok := secRaw.(map[string]any)
if !ok {
continue
}
if len(secrets.ExtractDeclarations(section)) == 0 {
continue
}
entries = append(entries, scopeEntry{Stack: stackName, Component: compName, ComponentType: componentType, Section: section})
}
}
return entries
}
// stackCompletion returns the distinct stacks that declare secrets. It backs both shell completion
// and the interactive prompt for a missing --stack.
func stackCompletion(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
entries, _, err := enumerateScopesFn(secretScope{})
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return distinct(entries, func(e scopeEntry) string { return e.Stack }), cobra.ShellCompDirectiveNoFileComp
}
// componentCompletion returns the distinct components that declare secrets in the currently
// selected --stack (read from viper, so it reflects a value just chosen via the stack prompt).
func componentCompletion(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
entries, _, err := enumerateScopesFn(secretScope{Stack: viper.GetString("stack")})
if err != nil {
return nil, cobra.ShellCompDirectiveNoFileComp
}
return distinct(entries, func(e scopeEntry) string { return e.Component }), cobra.ShellCompDirectiveNoFileComp
}
// checkStackSopsCollisions enumerates a stack's secret-declaring instances and verifies their SOPS
// files don't collide across scopes (distinct instances sharing a file, or a stack-scoped secret
// resolving per-component). It is a write-time guardrail for the advanced `spec.file` template path.
func checkStackSopsCollisions(stack string) error {
defer perf.Track(nil, "secret.checkStackSopsCollisions")()
entries, atmosConfig, err := enumerateScopesFn(secretScope{Stack: stack})
if err != nil {
return err
}
var placements []secrets.SopsPlacement
for _, entry := range entries {
svc := secrets.NewService(atmosConfig, entry.Stack, entry.Component, entry.Section)
placements = append(placements, svc.SopsPlacements()...)
}
return secrets.DetectSopsCollisions(placements)
}
// distinct returns the sorted, de-duplicated values produced by key over the entries.
func distinct(entries []scopeEntry, key func(scopeEntry) string) []string {
seen := make(map[string]bool)
var out []string
for _, entry := range entries {
v := key(entry)
if v == "" || seen[v] {
continue
}
seen[v] = true
out = append(out, v)
}
sort.Strings(out)
return out
}