Skip to content

Commit 341e6f5

Browse files
committed
feat(emulator): add local emulator support
1 parent a622ff6 commit 341e6f5

185 files changed

Lines changed: 12254 additions & 930 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.

.github/workflows/native-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
persist-credentials: false
4949

5050
- name: Set up Go
51-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
51+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
5252
with:
5353
go-version-file: "go.mod"
5454

@@ -138,7 +138,7 @@ jobs:
138138
persist-credentials: false
139139

140140
- name: Set up Go
141-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
141+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
142142
with:
143143
go-version-file: "go.mod"
144144

.github/workflows/planfile-artifacts-e2e.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
persist-credentials: false
6161

6262
- name: Set up Go
63-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
63+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
6464
with:
6565
go-version-file: "go.mod"
6666

@@ -91,7 +91,7 @@ jobs:
9191
persist-credentials: false
9292

9393
- name: Set up Go
94-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
94+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
9595
with:
9696
go-version-file: "go.mod"
9797

.github/workflows/planfile-verify-e2e.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
with:
6565
persist-credentials: false
6666
- name: Set up Go
67-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
67+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
6868
with:
6969
go-version-file: "go.mod"
7070
- name: Build atmos
@@ -102,7 +102,7 @@ jobs:
102102
with:
103103
persist-credentials: false
104104
- name: Set up Go
105-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
105+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
106106
with:
107107
go-version-file: "go.mod"
108108
- name: Build atmos
@@ -132,7 +132,7 @@ jobs:
132132
with:
133133
persist-credentials: false
134134
- name: Set up Go
135-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
135+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
136136
with:
137137
go-version-file: "go.mod"
138138
- name: Build atmos

.github/workflows/test.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979
- name: Set up Go
8080
if: ${{ ! ( matrix.target == 'windows' && github.event.pull_request.draft ) }}
8181
# setup-go v6 requires runner v2.327.1+ and can affect toolchain handling.
82-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
82+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
8383
with:
8484
go-version-file: "go.mod"
8585
id: go
@@ -212,7 +212,7 @@ jobs:
212212
213213
- name: Set up Go
214214
# setup-go v6 requires runner v2.327.1+ and can affect toolchain handling.
215-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
215+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
216216
if: ${{ ! ( matrix.flavor.target == 'windows' && github.event.pull_request.draft ) }}
217217
with:
218218
go-version-file: "go.mod"
@@ -338,8 +338,8 @@ jobs:
338338

339339
# Run the AWS-emulator demo tests against Floci (a free, LocalStack-compatible emulator;
340340
# LocalStack Community Edition was EOL'd in March 2026).
341-
# NOTE: "[floci] demo-floci" must be kept in sync with the required status checks in
342-
# branch protection on main (previously "[localstack] demo-localstack").
341+
# NOTE: the "[floci] emulator-aws" status check must be kept in sync with the required
342+
# status checks configured in branch protection on main.
343343
floci:
344344
name: "[floci] ${{ matrix.demo-folder }}"
345345
needs: build
@@ -356,7 +356,7 @@ jobs:
356356
fail-fast: false
357357
matrix:
358358
demo-folder:
359-
- demo-floci
359+
- emulator-aws
360360

361361
timeout-minutes: 30
362362
steps:
@@ -437,7 +437,7 @@ jobs:
437437

438438
- name: Set up Go
439439
# setup-go v6 requires runner v2.327.1+ and can affect toolchain handling.
440-
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
440+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
441441
with:
442442
go-version-file: "go.mod"
443443
cache: false
@@ -706,7 +706,7 @@ jobs:
706706
matrix:
707707
demo-folder:
708708
- demo-context
709-
- demo-floci
709+
- emulator-aws
710710
- demo-stacks
711711
- demo-helmfile
712712
- quick-start-advanced

cmd/describe_component.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func resolveAuthManager(p *resolveAuthManagerParams) (auth.AuthManager, error) {
151151
}
152152
}
153153

154-
return CreateAuthManagerFromIdentityWithAtmosConfig(p.identityName, mergedAuthConfig, p.atmosConfig)
154+
return CreateAuthManagerFromIdentityWithAtmosConfig(p.identityName, mergedAuthConfig, p.atmosConfig, p.stack)
155155
}
156156

157157
func getRunnableDescribeComponentCmd(

cmd/emulator/completions.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package emulator
2+
3+
import (
4+
"sort"
5+
6+
"github.com/spf13/cobra"
7+
"github.com/spf13/viper"
8+
9+
e "github.com/cloudposse/atmos/internal/exec"
10+
cfg "github.com/cloudposse/atmos/pkg/config"
11+
"github.com/cloudposse/atmos/pkg/flags"
12+
l "github.com/cloudposse/atmos/pkg/list"
13+
"github.com/cloudposse/atmos/pkg/perf"
14+
"github.com/cloudposse/atmos/pkg/schema"
15+
)
16+
17+
// globalInfoForCompletion builds a minimal ConfigAndStacksInfo from global flags
18+
// so completion honors config-selection flags.
19+
func globalInfoForCompletion(cmd *cobra.Command) schema.ConfigAndStacksInfo {
20+
globalFlags := flags.ParseGlobalFlags(cmd, viper.GetViper())
21+
return schema.ConfigAndStacksInfo{
22+
AtmosBasePath: globalFlags.BasePath,
23+
AtmosConfigFilesFromArg: globalFlags.Config,
24+
AtmosConfigDirsFromArg: globalFlags.ConfigPath,
25+
ProfilesFromArg: globalFlags.Profile,
26+
}
27+
}
28+
29+
// stackFlagCompletion provides completion values for the --stack flag.
30+
func stackFlagCompletion(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
31+
defer perf.Track(nil, "emulator.stackFlagCompletion")()
32+
33+
atmosConfig, err := cfg.InitCliConfig(globalInfoForCompletion(cmd), true)
34+
if err != nil {
35+
return nil, cobra.ShellCompDirectiveNoFileComp
36+
}
37+
stacks, err := stackNamesForCompletion(&atmosConfig)
38+
if err != nil {
39+
return nil, cobra.ShellCompDirectiveNoFileComp
40+
}
41+
return stacks, cobra.ShellCompDirectiveNoFileComp
42+
}
43+
44+
func stackNamesForCompletion(atmosConfig *schema.AtmosConfiguration) ([]string, error) {
45+
stacksMap, _, err := e.FindStacksMap(atmosConfig, false)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
names := make(map[string]struct{}, len(stacksMap))
51+
for stackFileName, stackSection := range stacksMap {
52+
stackMap, ok := stackSection.(map[string]any)
53+
if !ok {
54+
continue
55+
}
56+
name, ok := stackNameForCompletion(atmosConfig, stackFileName, stackMap)
57+
if ok {
58+
names[name] = struct{}{}
59+
}
60+
}
61+
62+
stacks := make([]string, 0, len(names))
63+
for name := range names {
64+
stacks = append(stacks, name)
65+
}
66+
sort.Strings(stacks)
67+
return stacks, nil
68+
}
69+
70+
func stackNameForCompletion(atmosConfig *schema.AtmosConfiguration, stackFileName string, stackMap map[string]any) (string, bool) {
71+
if name, ok := stackMap["name"].(string); ok && name != "" {
72+
return name, true
73+
}
74+
75+
if atmosConfig.Stacks.NameTemplate != "" {
76+
name, err := e.ProcessTmpl(atmosConfig, "emulator-stack-completion-name-template", atmosConfig.Stacks.NameTemplate, stackMap, atmosConfig.Templates.Settings.IgnoreMissingTemplateValues)
77+
if err == nil && name != "" {
78+
return name, true
79+
}
80+
return stackNameFromComponentTemplates(atmosConfig, stackMap)
81+
}
82+
83+
if pattern := e.GetStackNamePattern(atmosConfig); pattern != "" {
84+
vars, _ := stackMap[cfg.VarsSectionName].(map[string]any)
85+
context := cfg.GetContextFromVars(vars)
86+
name, err := cfg.GetContextPrefix(stackFileName, context, pattern, stackFileName)
87+
if err == nil && name != "" {
88+
return name, true
89+
}
90+
}
91+
92+
return stackFileName, stackFileName != ""
93+
}
94+
95+
func stackNameFromComponentTemplates(atmosConfig *schema.AtmosConfiguration, stackMap map[string]any) (string, bool) {
96+
componentsSection, ok := stackMap[cfg.ComponentsSectionName].(map[string]any)
97+
if !ok {
98+
return "", false
99+
}
100+
for _, componentsByType := range componentsSection {
101+
componentMap, ok := componentsByType.(map[string]any)
102+
if !ok {
103+
continue
104+
}
105+
for _, componentSection := range componentMap {
106+
componentData, ok := componentSection.(map[string]any)
107+
if !ok {
108+
continue
109+
}
110+
name, err := e.ProcessTmpl(atmosConfig, "emulator-stack-completion-component-name-template", atmosConfig.Stacks.NameTemplate, componentData, atmosConfig.Templates.Settings.IgnoreMissingTemplateValues)
111+
if err == nil && name != "" {
112+
return name, true
113+
}
114+
}
115+
}
116+
return "", false
117+
}
118+
119+
// componentArgCompletion provides completion values for the component argument,
120+
// restricted to emulator components in the selected stack.
121+
func componentArgCompletion(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
122+
defer perf.Track(nil, "emulator.componentArgCompletion")()
123+
124+
if len(args) > 0 {
125+
return nil, cobra.ShellCompDirectiveNoFileComp
126+
}
127+
stack := ""
128+
if stackFlag := cmd.Flag("stack"); stackFlag != nil {
129+
stack = stackFlag.Value.String()
130+
}
131+
atmosConfig, err := cfg.InitCliConfig(globalInfoForCompletion(cmd), true)
132+
if err != nil {
133+
return nil, cobra.ShellCompDirectiveNoFileComp
134+
}
135+
stacksMap, err := e.ExecuteDescribeStacksWithAuthDisabled(&atmosConfig, stack, nil, []string{cfg.EmulatorComponentType}, nil, false, false, false, false, nil, nil, true)
136+
if err != nil {
137+
return nil, cobra.ShellCompDirectiveNoFileComp
138+
}
139+
components, err := l.FilterAndListComponents(stack, stacksMap)
140+
if err != nil {
141+
return nil, cobra.ShellCompDirectiveNoFileComp
142+
}
143+
return components, cobra.ShellCompDirectiveNoFileComp
144+
}
145+
146+
// RegisterEmulatorCompletions registers completion functions for the emulator
147+
// command. Every subcommand takes a component argument.
148+
func RegisterEmulatorCompletions(cmd *cobra.Command) {
149+
defer perf.Track(nil, "emulator.RegisterEmulatorCompletions")()
150+
151+
for _, subCmd := range cmd.Commands() {
152+
subCmd.ValidArgsFunction = componentArgCompletion
153+
}
154+
}

cmd/emulator/completions_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package emulator
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/spf13/cobra"
9+
"github.com/spf13/viper"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// withViperBasePath points config discovery at an isolated directory so
15+
// completion runs deterministically, and restores the prior value afterwards.
16+
func withViperBasePath(t *testing.T, path string) {
17+
t.Helper()
18+
v := viper.GetViper()
19+
orig := v.GetString("base-path")
20+
t.Cleanup(func() { v.Set("base-path", orig) })
21+
v.Set("base-path", path)
22+
}
23+
24+
// exampleProjectPath resolves the bundled emulator-aws example, a valid local
25+
// project (no cloud credentials required) used to exercise the completion happy
26+
// paths end-to-end.
27+
func exampleProjectPath(t *testing.T) string {
28+
t.Helper()
29+
wd, err := os.Getwd()
30+
require.NoError(t, err)
31+
path, err := filepath.Abs(filepath.Join(wd, "..", "..", "examples", "emulator-aws"))
32+
require.NoError(t, err)
33+
if _, statErr := os.Stat(filepath.Join(path, "atmos.yaml")); statErr != nil {
34+
t.Skipf("example project not found at %s", path)
35+
}
36+
return path
37+
}
38+
39+
func TestGlobalInfoForCompletion(t *testing.T) {
40+
withViperBasePath(t, "some/base")
41+
info := globalInfoForCompletion(&cobra.Command{Use: "emulator"})
42+
assert.Equal(t, "some/base", info.AtmosBasePath)
43+
}
44+
45+
func TestComponentArgCompletion_AlreadyHasArg(t *testing.T) {
46+
// With a component already provided, completion offers nothing more.
47+
values, directive := componentArgCompletion(&cobra.Command{Use: "up"}, []string{"aws"}, "")
48+
assert.Nil(t, values)
49+
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
50+
}
51+
52+
func TestStackFlagCompletion_NoProjectDegradesGracefully(t *testing.T) {
53+
// Pointed at an empty directory there is no project to complete from; the
54+
// function still returns the NoFileComp directive rather than erroring.
55+
withViperBasePath(t, t.TempDir())
56+
values, directive := stackFlagCompletion(&cobra.Command{Use: "emulator"}, nil, "")
57+
assert.Nil(t, values)
58+
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
59+
}
60+
61+
func TestComponentArgCompletion_NoProjectDegradesGracefully(t *testing.T) {
62+
withViperBasePath(t, t.TempDir())
63+
values, directive := componentArgCompletion(&cobra.Command{Use: "up"}, nil, "")
64+
assert.Nil(t, values)
65+
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
66+
}
67+
68+
func TestStackFlagCompletion_ListsStacks(t *testing.T) {
69+
// Run from the example project dir so config discovery finds its atmos.yaml.
70+
t.Chdir(exampleProjectPath(t))
71+
withViperBasePath(t, "")
72+
73+
stacks, directive := stackFlagCompletion(&cobra.Command{Use: "emulator"}, nil, "")
74+
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
75+
assert.Contains(t, stacks, "dev")
76+
}
77+
78+
func TestComponentArgCompletion_ResolvesProjectPipeline(t *testing.T) {
79+
t.Chdir(exampleProjectPath(t))
80+
withViperBasePath(t, "")
81+
82+
// componentArgCompletion honors the --stack flag to scope suggestions; with a
83+
// real project it resolves config and runs the describe->filter pipeline (vs.
84+
// the graceful-degrade path tested elsewhere), always returning NoFileComp.
85+
//
86+
// Note: the suggestion list is empty here because the underlying
87+
// list.FilterAndListComponents extracts only terraform/helmfile/packer/ansible
88+
// components, not `emulator` components. Exercising the live pipeline is what
89+
// drives coverage; a positive contents assertion would require a production
90+
// change to the list extractor.
91+
c := &cobra.Command{Use: "up"}
92+
c.Flags().String("stack", "dev", "")
93+
94+
_, directive := componentArgCompletion(c, nil, "")
95+
assert.Equal(t, cobra.ShellCompDirectiveNoFileComp, directive)
96+
}
97+
98+
func TestRegisterEmulatorCompletions(t *testing.T) {
99+
// Every subcommand gets the component-arg completion function attached.
100+
parent := &cobra.Command{Use: "emulator"}
101+
child := &cobra.Command{Use: "up"}
102+
parent.AddCommand(child)
103+
104+
RegisterEmulatorCompletions(parent)
105+
require.NotNil(t, child.ValidArgsFunction)
106+
}

0 commit comments

Comments
 (0)