Skip to content

Commit 48d1d95

Browse files
authored
Merge branch 'main' into fix/concurrent-terminal-output
2 parents f9953e5 + 814fbdd commit 48d1d95

107 files changed

Lines changed: 7920 additions & 1214 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.

.atmos.d/dev.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,11 @@ commands:
144144
content: NOTICE regenerated.
145145

146146
- name: mocks
147-
description: Regenerate auth/http mocks using go:generate directives
147+
description: Regenerate auth/http/store mocks using go:generate directives
148148
env: *go_auto_env
149149
steps:
150150
- type: atmos
151-
command: test generate-auth-http-mocks
151+
command: test generate-auth-http-store-mocks
152152

153153
- name: snapshots
154154
description: Regenerate CLI golden snapshots

.atmos.d/test.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ commands:
2020
default: false
2121
- name: generate-mocks
2222
type: bool
23-
description: Regenerate auth/http mocks using go:generate directives
23+
description: Regenerate auth/http/store mocks using go:generate directives
2424
default: false
2525
steps:
2626
- type: shell
@@ -44,7 +44,7 @@ commands:
4444
{{- if .Flags.full }} acceptance
4545
{{- else if .Flags.coverage }} acc-coverage
4646
{{- else if .Flags.race }} race
47-
{{- else if index .Flags "generate-mocks" }} generate-auth-http-mocks
47+
{{- else if index .Flags "generate-mocks" }} generate-auth-http-store-mocks
4848
{{- else }} short
4949
{{- end }}
5050
commands:
@@ -155,11 +155,13 @@ commands:
155155
- type: shell
156156
command: go test -race -shuffle=on ${TEST:-$(go list ./...)} ${TESTARGS:-} -timeout 10m
157157

158-
- name: generate-auth-http-mocks
159-
description: Regenerate auth/http mocks using go:generate directives
158+
- name: generate-auth-http-store-mocks
159+
description: Regenerate auth/http/store mocks using go:generate directives
160160
env: *go_auto_env
161161
steps:
162162
- type: shell
163163
command: go generate ./pkg/auth/types/...
164164
- type: shell
165165
command: go generate ./pkg/http/...
166+
- type: shell
167+
command: go generate ./pkg/store/...

agent-skills/skills/atmos-hooks/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ Use before/after events for component operations, for example:
6060
- `before.terraform.apply`, `after.terraform.apply`
6161
- `before.terraform.deploy`, `after.terraform.deploy`
6262
- `before.terraform.test`, `after.terraform.test`
63+
- `before.terraform.output`, `after.terraform.output` — fires for `atmos terraform output`,
64+
useful for backfilling a store from already-deployed infrastructure without an `apply`
65+
- `before.terraform.refresh`, `after.terraform.refresh`
6366

6467
Kubernetes provides `before`/`after` events for `render`, `diff`/`plan`, `apply`/`deploy`,
6568
`delete`, and `validate`. Native Helm provides `template`, `diff`, `apply`/`deploy`, and

agent-skills/skills/atmos-steps/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ the type-specific files under `website/docs/workflows/workflows/workflow/steps/t
9393
as the canonical reference. Current canonical step types include:
9494

9595
- Command and integration: `atmos`, `shell`, `script`, `exec`, `container`,
96-
`emulator`, `http`, `archive`, `require`, `workdir`, `cast`.
96+
`emulator`, `http`, `archive`, `require`, `workdir`, `cast`, `store`.
9797
- Orchestration: `parallel`, `matrix`, `wait`, `wait-all`, `cancel`.
9898
- Interactive: `input`, `confirm`, `choose`, `filter`, `file`, `write`.
9999
- UI and output: `toast`, `markdown`, `spin`, `table`, `pager`, `format`,

agent-skills/skills/atmos-stores/SKILL.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: atmos-stores
3-
description: "Store backends: AWS SSM, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, Redis, Artifactory configuration, hooks integration, cross-component data sharing"
3+
description: "Store backends: AWS SSM, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, Redis, Artifactory configuration, hooks integration, cross-component data sharing, atmos store CLI CRUD, type: store workflow step"
44
metadata:
55
copyright: Copyright Cloud Posse, LLC 2026
66
version: "1.0.0"
@@ -324,6 +324,47 @@ components:
324324

325325
Atmos merges these into a complete hook definition at resolution time.
326326

327+
## Write to Stores with the CLI or a Workflow Step
328+
329+
For raw CRUD access to any configured store, use the `atmos store` CLI command family or the
330+
`type: store` workflow step. This access works for any store, not only Terraform outputs. Neither
331+
method requires a declaration. Both operate directly on any store configured under `stores:`, by
332+
name.
333+
334+
```shell
335+
# CLI: set, get, delete, list -- scope to a stack and component, or omit for a global value
336+
atmos store set app-metadata image_tag sha256:abc123 --stack=prod --component=ecs-service
337+
atmos store get app-metadata image_tag --stack=prod --component=ecs-service
338+
atmos store list
339+
atmos store list app-metadata --stack=prod --component=ecs-service
340+
```
341+
342+
Passing a store name to `atmos store list` lists the key/value pairs stored under a scope
343+
(instead of the configured backends themselves), for backends that support key enumeration. Most
344+
backends support it. 1Password and the default system keychain backend do not, because their
345+
underlying APIs do not support enumeration. Check the `Listable` column in a bare
346+
`atmos store list` before relying on it for a given store. Values are masked the same way
347+
`atmos store get` masks a single value.
348+
349+
```yaml
350+
# Workflow, custom-command, or hook step: write a value, for example an image tag from a build step
351+
- name: record-tag
352+
type: store
353+
action: write
354+
with:
355+
store: app-metadata
356+
key: image_tag
357+
value: "{{ .steps.push.metadata.digest }}"
358+
stack: prod
359+
component: ecs-service
360+
```
361+
362+
Atmos allows you to write to a `secret: true` store this way, for example to write a generated
363+
password. But this write skips the `atmos secret` declaration and scope system. When a value must
364+
be tracked as a formal secret, use `secrets.vars` and `atmos secret set` instead. See the
365+
`atmos-secrets` skill for that system. See the `atmos-steps` skill and the
366+
`/workflows/steps/type/store` docs for the step type.
367+
327368
## Cross-Account and Cross-Region Access
328369

329370
### AWS Cross-Account via Role Assumption

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import (
103103
_ "github.com/cloudposse/atmos/cmd/scaffold"
104104
_ "github.com/cloudposse/atmos/cmd/secret"
105105
stackcmd "github.com/cloudposse/atmos/cmd/stack"
106+
_ "github.com/cloudposse/atmos/cmd/store"
106107
_ "github.com/cloudposse/atmos/cmd/terraform"
107108
"github.com/cloudposse/atmos/cmd/terraform/backend"
108109
terraformcache "github.com/cloudposse/atmos/cmd/terraform/cache"

cmd/store/delete.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package store
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/cloudposse/atmos/pkg/flags"
7+
"github.com/cloudposse/atmos/pkg/perf"
8+
"github.com/cloudposse/atmos/pkg/ui"
9+
)
10+
11+
var deleteParser *flags.StandardParser
12+
13+
var deleteCmd = &cobra.Command{
14+
Use: "delete STORE KEY",
15+
Aliases: []string{"rm", "unset"},
16+
Short: "Remove a value from a store.",
17+
Long: "Remove a value from a configured store backend. Returns an error if the backend does not support deletion.",
18+
Args: cobra.ExactArgs(2),
19+
RunE: runStoreDelete,
20+
}
21+
22+
func init() {
23+
deleteParser = flags.NewStandardParser(
24+
flags.WithBoolFlag("force", "f", false, "Delete without confirmation"),
25+
)
26+
deleteParser.RegisterFlags(deleteCmd)
27+
}
28+
29+
func runStoreDelete(cmd *cobra.Command, args []string) error {
30+
defer perf.Track(nil, "store.runStoreDelete")()
31+
32+
scope, err := parseStoreScope(cmd)
33+
if err != nil {
34+
return err
35+
}
36+
storeName, key := args[0], args[1]
37+
38+
force, _ := cmd.Flags().GetBool("force")
39+
if !force {
40+
confirmed, confErr := confirmActionFn("Delete `" + key + "` from store `" + storeName + "`?")
41+
if confErr != nil {
42+
return confErr
43+
}
44+
if !confirmed {
45+
ui.Warning("Aborted")
46+
return nil
47+
}
48+
}
49+
50+
svc, err := loadServiceFn(scope)
51+
if err != nil {
52+
return err
53+
}
54+
55+
if err := svc.Delete(storeName, scope.Stack, scope.Component, key); err != nil {
56+
return err
57+
}
58+
59+
ui.Successf("Deleted `%s` from store `%s`", key, storeName)
60+
return nil
61+
}

cmd/store/delete_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package store
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestRunStoreDelete_Force(t *testing.T) {
11+
setupIO(t)
12+
svc := newFakeStoreService()
13+
installService(t, svc, nil)
14+
15+
err := runStoreSubcommand(t, "delete", "app-metadata", "image_tag", "--force", "--stack", "dev", "--component", "vpc")
16+
require.NoError(t, err)
17+
18+
require.Len(t, svc.deleteCalls, 1)
19+
assert.Equal(t, "app-metadata", svc.deleteCalls[0].name)
20+
assert.Equal(t, "dev", svc.deleteCalls[0].stack)
21+
assert.Equal(t, "vpc", svc.deleteCalls[0].component)
22+
assert.Equal(t, "image_tag", svc.deleteCalls[0].key)
23+
}
24+
25+
func TestRunStoreDelete_ConfirmedPrompt(t *testing.T) {
26+
setupIO(t)
27+
svc := newFakeStoreService()
28+
installService(t, svc, nil)
29+
titles := overrideConfirmAction(t, true, nil)
30+
31+
err := runStoreSubcommand(t, "delete", "app-metadata", "key")
32+
require.NoError(t, err)
33+
34+
require.Len(t, svc.deleteCalls, 1)
35+
require.Len(t, *titles, 1)
36+
}
37+
38+
func TestRunStoreDelete_AbortedPrompt(t *testing.T) {
39+
setupIO(t)
40+
svc := newFakeStoreService()
41+
installService(t, svc, nil)
42+
overrideConfirmAction(t, false, nil)
43+
44+
err := runStoreSubcommand(t, "delete", "app-metadata", "key")
45+
require.NoError(t, err)
46+
assert.Empty(t, svc.deleteCalls)
47+
}
48+
49+
func TestRunStoreDelete_ConfirmError(t *testing.T) {
50+
setupIO(t)
51+
svc := newFakeStoreService()
52+
installService(t, svc, nil)
53+
overrideConfirmAction(t, false, assert.AnError)
54+
55+
err := runStoreSubcommand(t, "delete", "app-metadata", "key")
56+
require.ErrorIs(t, err, assert.AnError)
57+
assert.Empty(t, svc.deleteCalls)
58+
}
59+
60+
func TestRunStoreDelete_NotSupported(t *testing.T) {
61+
setupIO(t)
62+
svc := newFakeStoreService()
63+
svc.deleteErr = assert.AnError
64+
installService(t, svc, nil)
65+
66+
err := runStoreSubcommand(t, "delete", "app-metadata", "key", "--force")
67+
require.ErrorIs(t, err, assert.AnError)
68+
}

cmd/store/deps.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package store
2+
3+
import (
4+
"github.com/cloudposse/atmos/pkg/io"
5+
pstore "github.com/cloudposse/atmos/pkg/store"
6+
)
7+
8+
// storeService is the subset of *pstore.Service the command handlers depend on. It exists so
9+
// handlers operate against an interface, letting tests inject a fake without constructing real
10+
// config/auth. *pstore.Service satisfies it structurally -- no change to pkg/store is required.
11+
type storeService interface {
12+
Set(name, stack, component, key string, value any) error
13+
Get(name, stack, component, key string) (any, error)
14+
Delete(name, stack, component, key string) error
15+
ListKeyValues(name, stack, component string) ([]pstore.KeyValue, error)
16+
List() []pstore.Descriptor
17+
IsSecret(name string) bool
18+
}
19+
20+
// Seam variables wrap the real implementations so tests can override behavior. Each defaults to
21+
// the production function; handlers call the variable, never the underlying function directly.
22+
// Restore any override with t.Cleanup in tests.
23+
var (
24+
// Loads a store service (config + auth) for the given scope.
25+
loadServiceFn = func(scope storeScope) (storeService, error) {
26+
svc, err := loadService(scope)
27+
if err != nil {
28+
return nil, err
29+
}
30+
return svc, nil
31+
}
32+
33+
// Loads a store service for `store list`, with no authentication attempt.
34+
loadServiceForListFn = func(scope storeScope) (storeService, error) {
35+
svc, err := loadServiceForList(scope)
36+
if err != nil {
37+
return nil, err
38+
}
39+
return svc, nil
40+
}
41+
42+
// Interactively reads a value (masked input).
43+
promptForValueFn = promptForStoreValue
44+
45+
// Interactively confirms a destructive action.
46+
confirmActionFn = confirmAction
47+
48+
// Registers a value with the output masker. Overridden in tests to record calls, since the
49+
// real masker lives on a package-global io.Context that these lightweight test harnesses
50+
// (which construct their own iolib.Context for data/ui output) never wire up as global.
51+
registerSecretValueFn = io.RegisterSecretValue
52+
)

cmd/store/errors.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package store
2+
3+
import "errors"
4+
5+
// ErrStoreNameRequired indicates a store NAME argument is required.
6+
var ErrStoreNameRequired = errors.New("a store NAME is required")
7+
8+
// ErrStoreKeyRequired indicates a KEY argument is required.
9+
var ErrStoreKeyRequired = errors.New("a KEY is required")
10+
11+
// ErrRawFormatConflict indicates --raw was combined with a non-text --format.
12+
var ErrRawFormatConflict = errors.New("--raw is text-only and cannot be combined with --format=json or --format=env")

0 commit comments

Comments
 (0)