Skip to content

Commit 4ed1eb5

Browse files
authored
Merge branch 'main' into feat/workflow-tags-labels
2 parents 10a66ed + e299b51 commit 4ed1eb5

174 files changed

Lines changed: 6666 additions & 4184 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ website/** linguist-documentation
1111
# and mark them generated so GitHub collapses their diffs in PR review.
1212
*.cast text eol=lf diff whitespace linguist-generated=true
1313

14-
# Golden snapshots should be treated a raw output to prevent line-ending conversions
15-
tests/snapshots/**/*.golden linguist-generated=true -text
14+
# Golden snapshots should be treated as raw output to prevent line-ending conversions.
15+
# Some intentionally preserve a final blank line to keep tree output separated from
16+
# the next shell prompt, so Git should not flag that terminal boundary as whitespace.
17+
tests/snapshots/**/*.golden linguist-generated=true -text whitespace=-blank-at-eof
1618

1719
# Mark binary files to prevent normalization
1820
*.png binary

atmos.yaml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,19 @@ validate:
9191
# source, and flags the former as a tab-indentation violation. gofumpt (via
9292
# `atmos lint`) already fully owns real Go source formatting, so editorconfig
9393
# checking .go files is both redundant and produces false positives.
94+
#
95+
# tests/snapshots/**/*.golden captures exact CLI output (lipgloss padding,
96+
# trailing whitespace, and no-final-newline are all deliberate parts of
97+
# what's being asserted -- see CLAUDE.md's Golden Snapshots section).
98+
# .editorconfig has its own override block for this same path, but this
99+
# exclude list is the one that actually takes effect here: touching
100+
# .editorconfig itself is treated as a rule change that forces a full
101+
# non-affected-scoped repo scan (see affectedEditorConfigFiles in
102+
# cmd/validate_affected.go), which would surface unrelated pre-existing
103+
# violations far outside any single PR's scope.
94104
exclude:
95105
- "**/*.go"
106+
- "tests/snapshots/**/*.golden"
96107
# If set to true, the default ignore patterns (like .git/*) will not be applied.
97108
ignore_defaults: false
98109
# Runs the checker without making any changes or producing output, useful for testing configuration.
@@ -232,7 +243,8 @@ settings:
232243
list_merge_strategy: replace
233244
# Terminal settings for displaying content
234245
terminal:
235-
max_width: 120 # Maximum width for terminal output
246+
# max_width is unlimited by default (0): output uses the full detected
247+
# terminal width. Set a positive value only to force narrower wrapping.
236248
pager: false # Pager setting for all terminal output
237249
unicode: true # Use unicode characters
238250
syntax_highlighting:

cmd/describe_component.go

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,20 @@ import (
66
"os"
77

88
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
910

1011
errUtils "github.com/cloudposse/atmos/errors"
1112
e "github.com/cloudposse/atmos/internal/exec"
1213
"github.com/cloudposse/atmos/pkg/auth"
1314
comp "github.com/cloudposse/atmos/pkg/component"
1415
cfg "github.com/cloudposse/atmos/pkg/config"
16+
"github.com/cloudposse/atmos/pkg/flags"
1517
"github.com/cloudposse/atmos/pkg/schema"
18+
"github.com/cloudposse/atmos/pkg/store"
1619
)
1720

21+
var describeComponentErrorModeParser *flags.StandardParser
22+
1823
// describeComponentCmd describes configuration for components.
1924
var describeComponentCmd = &cobra.Command{
2025
Use: "component",
@@ -53,6 +58,8 @@ type describeComponentFlags struct {
5358
query string
5459
skip []string
5560
provenance bool
61+
provenanceExplicit bool
62+
errorMode string
5663
}
5764

5865
// parseDescribeComponentFlags extracts all flag values from the command.
@@ -88,6 +95,8 @@ func parseDescribeComponentFlags(cmd *cobra.Command) (describeComponentFlags, er
8895
if f.provenance, err = flags.GetBool("provenance"); err != nil {
8996
return f, err
9097
}
98+
// An explicit --provenance beats the `describe.provenance` config default.
99+
f.provenanceExplicit = flags.Changed("provenance")
91100
return f, nil
92101
}
93102

@@ -124,10 +133,13 @@ type resolveAuthManagerParams struct {
124133
processYamlFunctions bool
125134
}
126135

127-
// resolveAuthManager creates an AuthManager when YAML functions are enabled or identity
128-
// is explicitly requested via CLI flag.
136+
// resolveAuthManager creates an AuthManager when identity is explicitly requested or when
137+
// enabled YAML functions can read an identity-backed store. The latter manager is deliberately
138+
// unauthenticated: the store resolver authenticates its configured identity only if the store
139+
// is actually read, preserving describe component's non-eager inspection behavior.
129140
func resolveAuthManager(p *resolveAuthManagerParams) (auth.AuthManager, error) {
130-
if !p.processYamlFunctions && !p.identityExplicit {
141+
needsStoreAuth := p.processYamlFunctions && hasIdentityBackedStore(p.atmosConfig)
142+
if !p.identityExplicit && !needsStoreAuth {
131143
return nil, nil
132144
}
133145

@@ -155,9 +167,32 @@ func resolveAuthManager(p *resolveAuthManagerParams) (auth.AuthManager, error) {
155167
}
156168
}
157169

170+
if !p.identityExplicit {
171+
return auth.CreateManagerWithAtmosConfigForStack(mergedAuthConfig, p.atmosConfig, p.stack)
172+
}
173+
158174
return CreateAuthManagerFromIdentityWithAtmosConfig(p.identityName, mergedAuthConfig, p.atmosConfig, p.stack)
159175
}
160176

177+
// hasIdentityBackedStore reports whether a configured store requires Atmos identity
178+
// resolution. Stores without an identity retain their ambient SDK credential behavior.
179+
func hasIdentityBackedStore(atmosConfig *schema.AtmosConfiguration) bool {
180+
if atmosConfig == nil {
181+
return false
182+
}
183+
184+
for name, storeConfig := range atmosConfig.StoresConfig {
185+
if storeConfig.Identity == "" {
186+
continue
187+
}
188+
if _, ok := atmosConfig.Stores[name].(store.IdentityAwareStore); ok {
189+
return true
190+
}
191+
}
192+
193+
return false
194+
}
195+
161196
func getRunnableDescribeComponentCmd(
162197
g getRunnableDescribeComponentCmdProps,
163198
) func(cmd *cobra.Command, args []string) error {
@@ -187,6 +222,18 @@ func getRunnableDescribeComponentCmd(
187222
if err != nil {
188223
return handleConfigError(err, needsPathResolution, component, f.stack)
189224
}
225+
if cmd.Flags().Lookup(describeErrorModeFlagName) != nil {
226+
if err = resolveDescribeErrorModeFlag(cmd, viper.GetViper(), describeComponentErrorModeParser); err != nil {
227+
return err
228+
}
229+
if f.errorMode, err = cmd.Flags().GetString(describeErrorModeFlagName); err != nil {
230+
return err
231+
}
232+
}
233+
f.errorMode = e.ResolveErrorMode(f.errorMode, atmosConfig.Describe.ErrorMode)
234+
if f.errorMode != "strict" && f.errorMode != "warn" && f.errorMode != "silent" {
235+
return fmt.Errorf("%w: %q", e.ErrInvalidErrorMode, f.errorMode)
236+
}
190237

191238
component, err = resolveComponentFromPathIfNeeded(&g, &atmosConfig, component, f.stack, needsPathResolution)
192239
if err != nil {
@@ -219,6 +266,8 @@ func getRunnableDescribeComponentCmd(
219266
Format: f.format,
220267
File: f.file,
221268
Provenance: f.provenance,
269+
ProvenanceExplicit: f.provenanceExplicit,
270+
ErrorMode: f.errorMode,
222271
AuthManager: authManager,
223272
})
224273
}
@@ -256,7 +305,12 @@ func init() {
256305
describeComponentCmd.PersistentFlags().Bool("process-functions", true, "Enable/disable YAML functions processing in Atmos stack manifests when executing the command")
257306
describeComponentCmd.PersistentFlags().Bool("use-mocks", false, "Resolve Terraform state/output YAML functions from component mocks instead of remote state. Supported only by plan and describe commands")
258307
describeComponentCmd.PersistentFlags().StringSlice("skip", nil, "Skip executing a YAML function in the Atmos stack manifests when executing the command")
259-
describeComponentCmd.PersistentFlags().Bool("provenance", false, "Enable provenance tracking to show where configuration values originated")
308+
describeComponentCmd.PersistentFlags().Bool("provenance", false, "Show where configuration values originated (enabled by default; disable with --provenance=false or describe.provenance in atmos.yaml)")
309+
describeComponentErrorModeParser = newDescribeErrorModeParser()
310+
describeComponentErrorModeParser.RegisterPersistentFlags(describeComponentCmd)
311+
if err := describeComponentErrorModeParser.BindToViper(viper.GetViper()); err != nil {
312+
errUtils.CheckErrorPrintAndExit(err, "", "")
313+
}
260314

261315
err := describeComponentCmd.MarkPersistentFlagRequired("stack")
262316
if err != nil {

cmd/describe_component_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ import (
55
"path/filepath"
66
"testing"
77

8+
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
810
"github.com/stretchr/testify/assert"
911
"github.com/stretchr/testify/require"
12+
"go.uber.org/mock/gomock"
13+
14+
"github.com/cloudposse/atmos/internal/exec"
15+
"github.com/cloudposse/atmos/pkg/schema"
16+
"github.com/cloudposse/atmos/pkg/store"
1017
)
1118

1219
func TestDescribeComponentCmd_Error(t *testing.T) {
@@ -27,6 +34,164 @@ func TestDescribeComponentCmd_ProvenanceFlag(t *testing.T) {
2734
assert.Equal(t, "false", provenanceFlag.DefValue, "provenance flag should default to false")
2835
}
2936

37+
func TestHasIdentityBackedStore(t *testing.T) {
38+
ctrl := gomock.NewController(t)
39+
identityAware := store.NewMockIdentityAwareStore(ctrl)
40+
plain := store.NewMockStore(ctrl)
41+
42+
tests := []struct {
43+
name string
44+
atmosConfig *schema.AtmosConfiguration
45+
want bool
46+
}{
47+
{name: "nil configuration", atmosConfig: nil, want: false},
48+
{
49+
name: "plain store with identity",
50+
atmosConfig: &schema.AtmosConfiguration{
51+
StoresConfig: store.StoresConfig{"plain": {Identity: "platform"}},
52+
Stores: store.StoreRegistry{"plain": plain},
53+
},
54+
want: false,
55+
},
56+
{
57+
name: "identity-aware store without identity",
58+
atmosConfig: &schema.AtmosConfiguration{
59+
StoresConfig: store.StoresConfig{"cloud": {}},
60+
Stores: store.StoreRegistry{"cloud": identityAware},
61+
},
62+
want: false,
63+
},
64+
{
65+
name: "identity-aware store with identity",
66+
atmosConfig: &schema.AtmosConfiguration{
67+
StoresConfig: store.StoresConfig{"cloud": {Identity: "platform"}},
68+
Stores: store.StoreRegistry{"cloud": identityAware},
69+
},
70+
want: true,
71+
},
72+
}
73+
for _, tt := range tests {
74+
t.Run(tt.name, func(t *testing.T) {
75+
assert.Equal(t, tt.want, hasIdentityBackedStore(tt.atmosConfig))
76+
})
77+
}
78+
}
79+
80+
// TestGetRunnableDescribeComponentCmd_InvalidErrorMode covers the dispatch call site
81+
// inside getRunnableDescribeComponentCmd that rejects a resolved --error-mode value that
82+
// isn't one of "strict", "warn", or "silent" once resolved against atmos.yaml's
83+
// describe.error_mode: an invalid resolved value must short-circuit before the describe
84+
// component executor ever runs. Mirrors describe_stacks_test.go's and
85+
// describe_dependents_test.go's InvalidErrorMode tests for the same shared --error-mode
86+
// flag resolution path (cmd/describe_error_mode_flag.go).
87+
//
88+
// Unlike those siblings, the value is set via ParseFlags rather than by reaching into the
89+
// registered flag's Value directly, since describeComponentCmd's --error-mode is a
90+
// PersistentFlag, and cobra only merges persistent flags into the command's own flag set
91+
// on the first ParseFlags/Execute call, not on registration. Its siblings happen to get
92+
// that merge for free from an unrelated earlier test's real dispatch call, but
93+
// describeComponentCmd does not, so looking up the flag directly would return nil here
94+
// depending on test order. ParseFlags both triggers the merge and sets the value in one
95+
// deterministic step.
96+
func TestGetRunnableDescribeComponentCmd_InvalidErrorMode(t *testing.T) {
97+
tk := NewTestKit(t)
98+
99+
viper.Reset()
100+
tk.Setenv("ATMOS_IDENTITY", "")
101+
tk.Setenv("IDENTITY", "")
102+
103+
errorModeFlag := describeComponentCmd.PersistentFlags().Lookup(describeErrorModeFlagName)
104+
require.NotNil(t, errorModeFlag, "error-mode flag must be registered on describeComponentCmd")
105+
origValue := errorModeFlag.Value.String()
106+
origChanged := errorModeFlag.Changed
107+
t.Cleanup(func() {
108+
_ = errorModeFlag.Value.Set(origValue)
109+
errorModeFlag.Changed = origChanged
110+
})
111+
require.NoError(t, describeComponentCmd.ParseFlags([]string{"--error-mode=bogus"}))
112+
113+
ctrl := gomock.NewController(t)
114+
defer ctrl.Finish()
115+
116+
mockExec := exec.NewMockDescribeComponentCmdExec(ctrl)
117+
mockExec.EXPECT().ExecuteDescribeComponentCmd(gomock.Any()).Times(0)
118+
119+
run := getRunnableDescribeComponentCmd(getRunnableDescribeComponentCmdProps{
120+
checkAtmosConfigE: func(opts ...AtmosValidateOption) error { return nil },
121+
initCliConfig: func(info schema.ConfigAndStacksInfo, processStacks bool) (schema.AtmosConfiguration, error) {
122+
return schema.AtmosConfiguration{}, nil
123+
},
124+
isExplicitComponentPath: func(component string) bool { return false },
125+
resolveComponentFromPath: func(atmosConfig *schema.AtmosConfiguration, component, stack string) (string, error) {
126+
return component, nil
127+
},
128+
executeDescribeComponent: func(params *exec.ExecuteDescribeComponentParams) (map[string]any, error) {
129+
return nil, nil
130+
},
131+
newDescribeComponentExec: mockExec,
132+
})
133+
134+
err := run(describeComponentCmd, []string{"vpc"})
135+
136+
require.ErrorIs(t, err, exec.ErrInvalidErrorMode, "invalid error-mode should be rejected before executing")
137+
}
138+
139+
// TestGetRunnableDescribeComponentCmd_ErrorModeWrongType covers the genuinely-forceable
140+
// return-err branch on cmd.Flags().GetString(describeErrorModeFlagName) inside
141+
// getRunnableDescribeComponentCmd: registering "error-mode" as a Bool flag (instead of the
142+
// real String flag) reproduces a type mismatch without needing to touch any
143+
// BindFlagsToViper-adjacent code path. Mirrors describe_dependents_test.go's
144+
// TestSetFlagsForDescribeDependentsCmd_ErrorModeWrongType and
145+
// describe_edition_test.go's TestDescribeEditionCmd_FormatFlagWrongType.
146+
//
147+
// Note: resolveDescribeErrorModeFlag itself still succeeds here (binding a Bool pflag to
148+
// Viper doesn't error, and Viper's GetString on the bound Bool value round-trips to
149+
// "false", which cmd.Flags().Set("error-mode", "false") happily accepts on a Bool flag)
150+
// -- it's the subsequent cmd.Flags().GetString call that fails, because the flag is
151+
// genuinely a Bool.
152+
func TestGetRunnableDescribeComponentCmd_ErrorModeWrongType(t *testing.T) {
153+
tk := NewTestKit(t)
154+
viper.Reset()
155+
156+
testCmd := &cobra.Command{Use: "component"}
157+
testCmd.Flags().String("stack", "", "")
158+
testCmd.Flags().String("format", "yaml", "")
159+
testCmd.Flags().String("file", "", "")
160+
testCmd.Flags().Bool("process-templates", true, "")
161+
testCmd.Flags().Bool("process-functions", true, "")
162+
testCmd.Flags().String("query", "", "")
163+
testCmd.Flags().StringSlice("skip", nil, "")
164+
testCmd.Flags().Bool("provenance", false, "")
165+
testCmd.Flags().Bool("error-mode", false, "")
166+
require.NoError(t, testCmd.Flags().Set("error-mode", "true"))
167+
168+
ctrl := gomock.NewController(t)
169+
defer ctrl.Finish()
170+
171+
mockExec := exec.NewMockDescribeComponentCmdExec(ctrl)
172+
mockExec.EXPECT().ExecuteDescribeComponentCmd(gomock.Any()).Times(0)
173+
174+
run := getRunnableDescribeComponentCmd(getRunnableDescribeComponentCmdProps{
175+
checkAtmosConfigE: func(opts ...AtmosValidateOption) error { return nil },
176+
initCliConfig: func(info schema.ConfigAndStacksInfo, processStacks bool) (schema.AtmosConfiguration, error) {
177+
return schema.AtmosConfiguration{}, nil
178+
},
179+
isExplicitComponentPath: func(component string) bool { return false },
180+
resolveComponentFromPath: func(atmosConfig *schema.AtmosConfiguration, component, stack string) (string, error) {
181+
return component, nil
182+
},
183+
executeDescribeComponent: func(params *exec.ExecuteDescribeComponentParams) (map[string]any, error) {
184+
return nil, nil
185+
},
186+
newDescribeComponentExec: mockExec,
187+
})
188+
189+
err := run(testCmd, []string{"vpc"})
190+
191+
require.Error(tk, err, "GetString on a Bool-typed error-mode flag must return an error")
192+
assert.NotErrorIs(tk, err, exec.ErrInvalidErrorMode, "the failure must come from GetString, not error-mode validation")
193+
}
194+
30195
// TestDescribeComponentCmd_ProvenanceWithFormatJSON tests that provenance and format flags
31196
// are correctly parsed and accepted. This is a flag parsing test, not a functional test.
32197
func TestDescribeComponentCmd_ProvenanceWithFormatJSON(t *testing.T) {

0 commit comments

Comments
 (0)