Skip to content

Commit b3b4457

Browse files
create aws localstack profile
1 parent af71c79 commit b3b4457

File tree

13 files changed

+819
-16
lines changed

13 files changed

+819
-16
lines changed

cmd/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ func runStart(ctx context.Context, rt runtime.Runtime, cfg *env.Env, tel *teleme
8181

8282
platformClient := api.NewPlatformClient(cfg.APIEndpoint)
8383
if ui.IsInteractive() {
84-
return ui.Run(ctx, rt, version.Version(), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL)
84+
return ui.Run(ctx, rt, version.Version(), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL, cfg.LocalStackHost)
8585
}
86-
return container.Start(ctx, rt, output.NewPlainSink(os.Stdout), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL, false)
86+
return container.Start(ctx, rt, output.NewPlainSink(os.Stdout), platformClient, cfg.AuthToken, cfg.ForceFileKeyring, cfg.WebAppURL, false, cfg.LocalStackHost)
8787
}
8888

8989
func initConfig(cmd *cobra.Command, _ []string) error {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/stretchr/testify v1.11.1
2020
go.uber.org/mock v0.6.0
2121
golang.org/x/term v0.40.0
22+
gopkg.in/ini.v1 v1.67.1
2223
gotest.tools/v3 v3.5.2
2324
)
2425

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3
4747
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4848
github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ=
4949
github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs=
50+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5051
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5152
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5253
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -151,8 +152,14 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
151152
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
152153
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
153154
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
155+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
156+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
157+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
154158
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
155159
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
160+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
161+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
162+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
156163
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
157164
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
158165
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -210,6 +217,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
210217
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
211218
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
212219
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
220+
gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=
221+
gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
222+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
213223
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
214224
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
215225
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=

internal/awsconfig/awsconfig.go

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package awsconfig
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"gopkg.in/ini.v1"
12+
13+
"github.com/localstack/lstk/internal/endpoint"
14+
"github.com/localstack/lstk/internal/output"
15+
)
16+
17+
const (
18+
profileName = "localstack"
19+
configSectionName = "profile localstack" // ~/.aws/config uses "profile <name>" as section header
20+
credsSectionName = "localstack" // ~/.aws/credentials uses just the profile name
21+
// TODO: make region configurable (e.g. from container env or lstk config)
22+
defaultRegion = "us-east-1"
23+
)
24+
25+
var credentialsKeys = map[string]string{
26+
"aws_access_key_id": "test",
27+
"aws_secret_access_key": "test",
28+
}
29+
30+
// isValidLocalStackEndpoint returns true if the URL points to a known LocalStack host with a port.
31+
func isValidLocalStackEndpoint(url string) bool {
32+
for _, prefix := range []string{
33+
"http://" + endpoint.Hostname + ":",
34+
"https://" + endpoint.Hostname + ":",
35+
"http://127.0.0.1:",
36+
"https://127.0.0.1:",
37+
"http://localhost:",
38+
"https://localhost:",
39+
} {
40+
if strings.HasPrefix(url, prefix) {
41+
return true
42+
}
43+
}
44+
return false
45+
}
46+
47+
func awsPaths() (configPath, credentialsPath string, err error) {
48+
home, err := os.UserHomeDir()
49+
if err != nil {
50+
return "", "", err
51+
}
52+
return filepath.Join(home, ".aws", "config"), filepath.Join(home, ".aws", "credentials"), nil
53+
}
54+
55+
// profileStatus holds which AWS profile files need to be written or updated.
56+
type profileStatus struct {
57+
configNeeded bool
58+
credsNeeded bool
59+
}
60+
61+
func (s profileStatus) anyNeeded() bool {
62+
return s.configNeeded || s.credsNeeded
63+
}
64+
65+
func (s profileStatus) filesToModify() []string {
66+
var files []string
67+
if s.configNeeded {
68+
files = append(files, "~/.aws/config")
69+
}
70+
if s.credsNeeded {
71+
files = append(files, "~/.aws/credentials")
72+
}
73+
return files
74+
}
75+
76+
// checkProfileStatus determines which AWS profile files need to be written or updated.
77+
func checkProfileStatus(configPath, credsPath string) (profileStatus, error) {
78+
configNeeded, err := configNeedsWrite(configPath)
79+
if err != nil {
80+
return profileStatus{}, err
81+
}
82+
credsNeeded, err := credsNeedWrite(credsPath)
83+
if err != nil {
84+
return profileStatus{}, err
85+
}
86+
return profileStatus{configNeeded: configNeeded, credsNeeded: credsNeeded}, nil
87+
}
88+
89+
func configNeedsWrite(path string) (bool, error) {
90+
f, err := ini.Load(path)
91+
if errors.Is(err, os.ErrNotExist) {
92+
return true, nil
93+
}
94+
if err != nil {
95+
return false, err
96+
}
97+
section, err := f.GetSection(configSectionName)
98+
if err != nil {
99+
return true, nil // section doesn't exist
100+
}
101+
endpointKey, err := section.GetKey("endpoint_url")
102+
if err != nil || !isValidLocalStackEndpoint(endpointKey.Value()) {
103+
return true, nil
104+
}
105+
if !section.HasKey("region") {
106+
return true, nil
107+
}
108+
return false, nil
109+
}
110+
111+
func credsNeedWrite(path string) (bool, error) {
112+
f, err := ini.Load(path)
113+
if errors.Is(err, os.ErrNotExist) {
114+
return true, nil
115+
}
116+
if err != nil {
117+
return false, err
118+
}
119+
section, err := f.GetSection(credsSectionName)
120+
if err != nil {
121+
return true, nil // section doesn't exist
122+
}
123+
for k, expected := range credentialsKeys {
124+
key, err := section.GetKey(k)
125+
if err != nil || key.Value() != expected {
126+
return true, nil
127+
}
128+
}
129+
return false, nil
130+
}
131+
132+
// profileExists reports whether the localstack profile section is present in both
133+
// ~/.aws/config and ~/.aws/credentials.
134+
func profileExists() (bool, error) {
135+
configPath, credsPath, err := awsPaths()
136+
if err != nil {
137+
return false, err
138+
}
139+
configOK, err := sectionExists(configPath, configSectionName)
140+
if err != nil {
141+
return false, err
142+
}
143+
credsOK, err := sectionExists(credsPath, credsSectionName)
144+
if err != nil {
145+
return false, err
146+
}
147+
return configOK && credsOK, nil
148+
}
149+
150+
// writeProfile writes the localstack profile to ~/.aws/config and ~/.aws/credentials,
151+
// creating or updating sections as needed.
152+
func writeProfile(host string) error {
153+
configPath, credsPath, err := awsPaths()
154+
if err != nil {
155+
return err
156+
}
157+
configKeys := map[string]string{
158+
"region": defaultRegion,
159+
"output": "json",
160+
"endpoint_url": "http://" + host,
161+
}
162+
if err := upsertSection(configPath, configSectionName, configKeys); err != nil {
163+
return fmt.Errorf("failed to write %s: %w", configPath, err)
164+
}
165+
if err := upsertSection(credsPath, credsSectionName, credentialsKeys); err != nil {
166+
return fmt.Errorf("failed to write %s: %w", credsPath, err)
167+
}
168+
return nil
169+
}
170+
171+
func writeConfigProfile(configPath, host string) error {
172+
keys := map[string]string{
173+
"region": defaultRegion,
174+
"output": "json",
175+
"endpoint_url": "http://" + host,
176+
}
177+
return upsertSection(configPath, configSectionName, keys)
178+
}
179+
180+
func writeCredsProfile(credsPath string) error {
181+
return upsertSection(credsPath, credsSectionName, credentialsKeys)
182+
}
183+
184+
// Setup checks for the localstack AWS profile and prompts to create or update it if needed.
185+
// In non-interactive mode, emits a note instead of prompting.
186+
func Setup(ctx context.Context, sink output.Sink, interactive bool, port string, localStackHost string) error {
187+
configPath, credsPath, err := awsPaths()
188+
if err != nil {
189+
output.EmitWarning(sink, fmt.Sprintf("could not determine AWS config paths: %v", err))
190+
return nil
191+
}
192+
193+
var dnsOK bool
194+
localStackHost, dnsOK = endpoint.ResolveHost(port, localStackHost)
195+
if !dnsOK {
196+
output.EmitNote(sink, `Could not resolve "localhost.localstack.cloud" — your system may have DNS rebind protection enabled. Using 127.0.0.1 as the endpoint.`)
197+
}
198+
199+
status, err := checkProfileStatus(configPath, credsPath)
200+
if err != nil {
201+
output.EmitWarning(sink, fmt.Sprintf("could not check AWS profile: %v", err))
202+
return nil
203+
}
204+
if !status.anyNeeded() {
205+
return nil
206+
}
207+
208+
if !interactive {
209+
output.EmitNote(sink, fmt.Sprintf("No complete LocalStack AWS profile found. Run lstk interactively to configure one, or add a [profile %s] section to ~/.aws/config manually.", profileName))
210+
return nil
211+
}
212+
213+
files := strings.Join(status.filesToModify(), " and ")
214+
responseCh := make(chan output.InputResponse, 1)
215+
output.EmitUserInputRequest(sink, output.UserInputRequestEvent{
216+
Prompt: fmt.Sprintf("Set up LocalStack AWS profile in %s?", files),
217+
Options: []output.InputOption{{Key: "y", Label: "Y"}, {Key: "n", Label: "n"}},
218+
ResponseCh: responseCh,
219+
})
220+
221+
select {
222+
case resp := <-responseCh:
223+
if resp.Cancelled || resp.SelectedKey == "n" {
224+
return nil
225+
}
226+
if status.configNeeded {
227+
if err := writeConfigProfile(configPath, localStackHost); err != nil {
228+
output.EmitWarning(sink, fmt.Sprintf("could not update ~/.aws/config: %v", err))
229+
return nil
230+
}
231+
}
232+
if status.credsNeeded {
233+
if err := writeCredsProfile(credsPath); err != nil {
234+
output.EmitWarning(sink, fmt.Sprintf("could not update ~/.aws/credentials: %v", err))
235+
return nil
236+
}
237+
}
238+
output.EmitSuccess(sink, fmt.Sprintf("LocalStack AWS profile written to %s", files))
239+
output.EmitNote(sink, fmt.Sprintf("Try: aws s3 mb s3://test --profile %s", profileName))
240+
case <-ctx.Done():
241+
return ctx.Err()
242+
}
243+
244+
return nil
245+
}
246+

0 commit comments

Comments
 (0)