Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
run: make test-integration
env:
CREATE_JUNIT_REPORT: "true"
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
LSTK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}

- name: Upload test results
uses: actions/upload-artifact@v6
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test:
test-integration: $(BUILD_DIR)/$(BINARY_NAME)
@JUNIT=""; [ -n "$$CREATE_JUNIT_REPORT" ] && JUNIT="--junitfile ../../test-integration-results.xml"; \
if [ "$$(uname)" = "Darwin" ]; then \
cd test/integration && KEYRING=file go run gotest.tools/gotestsum@latest --format testdox $$JUNIT -- -count=1 ./...; \
cd test/integration && LSTK_KEYRING=file go run gotest.tools/gotestsum@latest --format testdox $$JUNIT -- -count=1 ./...; \
else \
cd test/integration && go run gotest.tools/gotestsum@latest --format testdox $$JUNIT -- -count=1 ./...; \
fi
Expand Down
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/localstack/lstk/internal/api"
"github.com/localstack/lstk/internal/config"
"github.com/localstack/lstk/internal/container"
"github.com/localstack/lstk/internal/env"
"github.com/localstack/lstk/internal/output"
"github.com/localstack/lstk/internal/runtime"
"github.com/localstack/lstk/internal/ui"
Expand All @@ -27,6 +28,7 @@ var rootCmd = &cobra.Command{
if cmd.Name() == "version" {
return nil
}
env.Init()
return config.Init()
},
Run: func(cmd *cobra.Command, args []string) {
Expand Down
16 changes: 10 additions & 6 deletions env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
export LOCALSTACK_AUTH_TOKEN=ls-...
# Authentication token (optional - will trigger device flow login if not set)
export LSTK_AUTH_TOKEN=ls-...

# Force file-based keyring backend (instead of system keychain)
# export KEYRING=file
#
export LOCALSTACK_API_ENDPOINT=https://api.staging.aws.localstack.cloud
export LOCALSTACK_WEB_APP_URL=https://app.staging.aws.localstack.cloud
# API endpoint (defaults to https://api.localstack.cloud)
export LSTK_API_ENDPOINT=https://api.staging.aws.localstack.cloud

# Web app URL (defaults to https://app.localstack.cloud)
export LSTK_WEB_APP_URL=https://app.staging.aws.localstack.cloud

# Force file-based keyring backend instead of system keychain (optional)
# export LSTK_KEYRING=file
9 changes: 3 additions & 6 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
"fmt"
"log"
"net/http"
"os"
"time"

"github.com/localstack/lstk/internal/env"
)

type PlatformAPI interface {
Expand Down Expand Up @@ -65,12 +66,8 @@ type PlatformClient struct {
}

func NewPlatformClient() *PlatformClient {
baseURL := os.Getenv("LOCALSTACK_API_ENDPOINT")
if baseURL == "" {
baseURL = "https://api.localstack.cloud"
}
return &PlatformClient{
baseURL: baseURL,
baseURL: env.Vars.APIEndpoint,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
Expand Down
8 changes: 4 additions & 4 deletions internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"context"
"errors"
"fmt"
"os"

"github.com/99designs/keyring"
"github.com/localstack/lstk/internal/api"
"github.com/localstack/lstk/internal/env"
"github.com/localstack/lstk/internal/output"
)

Expand All @@ -27,18 +27,18 @@ func New(sink output.Sink, platform api.PlatformAPI, storage AuthTokenStorage, a
}
}

// GetToken tries in order: 1) keyring 2) LOCALSTACK_AUTH_TOKEN env var 3) device flow login
// GetToken tries in order: 1) keyring 2) LSTK_AUTH_TOKEN env var 3) device flow login
func (a *Auth) GetToken(ctx context.Context) (string, error) {
if token, err := a.tokenStorage.GetAuthToken(); err == nil && token != "" {
return token, nil
}

if token := os.Getenv("LOCALSTACK_AUTH_TOKEN"); token != "" {
if token := env.Vars.AuthToken; token != "" {
return token, nil
}

if !a.allowLogin {
return "", fmt.Errorf("authentication required: set LOCALSTACK_AUTH_TOKEN or run in interactive mode")
return "", fmt.Errorf("authentication required: set LSTK_AUTH_TOKEN or run in interactive mode")
}

output.EmitLog(a.sink, "No existing credentials found. Please log in:")
Expand Down
2 changes: 1 addition & 1 deletion internal/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

func TestMain(m *testing.M) {
_ = os.Unsetenv("LOCALSTACK_AUTH_TOKEN")
_ = os.Unsetenv("LSTK_AUTH_TOKEN")
m.Run()
}

Expand Down
10 changes: 2 additions & 8 deletions internal/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ package auth
import (
"context"
"fmt"
"os"

"github.com/localstack/lstk/internal/api"
"github.com/localstack/lstk/internal/env"
"github.com/localstack/lstk/internal/output"
"github.com/pkg/browser"
)

const webAppURL = "https://app.localstack.cloud"

type LoginProvider interface {
Login(ctx context.Context) (string, error)
}
Expand Down Expand Up @@ -82,11 +80,7 @@ func (l *loginProvider) Login(ctx context.Context) (string, error) {
}

func getWebAppURL() string {
// allows overriding the URL for testing
if url := os.Getenv("LOCALSTACK_WEB_APP_URL"); url != "" {
return url
}
return webAppURL
return env.Vars.WebAppURL
}

func (l *loginProvider) completeAuth(ctx context.Context, authReq *api.AuthRequest) (string, error) {
Expand Down
5 changes: 3 additions & 2 deletions internal/auth/token_storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/99designs/keyring"
"github.com/localstack/lstk/internal/config"
"github.com/localstack/lstk/internal/env"
)

const (
Expand Down Expand Up @@ -44,8 +45,8 @@ func NewTokenStorage() (AuthTokenStorage, error) {
},
}

// Force file backend if KEYRING env var is set to "file"
if os.Getenv("KEYRING") == "file" {
// Force file backend if LSTK_KEYRING env var is set to "file"
if env.Vars.Keyring == "file" {
keyringConfig.AllowedBackends = []keyring.BackendType{keyring.FileBackend}
}

Expand Down
33 changes: 33 additions & 0 deletions internal/env/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package env

import (
"strings"

"github.com/spf13/viper"
)

type Env struct {
AuthToken string
APIEndpoint string
WebAppURL string
Keyring string
}

var Vars = &Env{}

// Init initializes environment variable configuration
func Init() {
viper.SetEnvPrefix("LSTK")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()

viper.SetDefault("api_endpoint", "https://api.localstack.cloud")
viper.SetDefault("web_app_url", "https://app.localstack.cloud")

Vars = &Env{
AuthToken: viper.GetString("auth_token"),
APIEndpoint: viper.GetString("api_endpoint"),
WebAppURL: viper.GetString("web_app_url"),
Keyring: viper.GetString("keyring"),
}
}
9 changes: 7 additions & 2 deletions internal/ui/run_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/charmbracelet/x/exp/teatest"
"github.com/localstack/lstk/internal/api"
"github.com/localstack/lstk/internal/auth"
"github.com/localstack/lstk/internal/env"
"github.com/localstack/lstk/internal/output"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -83,7 +84,9 @@ func TestLoginFlow_DeviceFlowSuccess(t *testing.T) {
mockServer := createMockAPIServer(t, "test-license-token", true)
defer mockServer.Close()

t.Setenv("LOCALSTACK_API_ENDPOINT", mockServer.URL)
t.Setenv("LSTK_API_ENDPOINT", mockServer.URL)
t.Setenv("LSTK_AUTH_TOKEN", "")
env.Init()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand Down Expand Up @@ -139,7 +142,9 @@ func TestLoginFlow_DeviceFlowFailure_NotConfirmed(t *testing.T) {
mockServer := createMockAPIServer(t, "", false)
defer mockServer.Close()

t.Setenv("LOCALSTACK_API_ENDPOINT", mockServer.URL)
t.Setenv("LSTK_API_ENDPOINT", mockServer.URL)
t.Setenv("LSTK_AUTH_TOKEN", "")
env.Init()

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
Expand Down
60 changes: 60 additions & 0 deletions test/integration/env/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package env

import (
"os"
"strings"
"testing"
)

type Key string

const (
AuthToken Key = "LSTK_AUTH_TOKEN"
APIEndpoint Key = "LSTK_API_ENDPOINT"
Keyring Key = "LSTK_KEYRING"
CI Key = "CI"
)

func Get(key Key) string {
return os.Getenv(string(key))
}

func Require(t testing.TB, key Key) string {
t.Helper()
v := os.Getenv(string(key))
if v == "" {
t.Fatalf("%s must be set to run this test", key)
}
return v
}

type Environ []string

func Without(keys ...Key) Environ {
return Environ(os.Environ()).Without(keys...)
}

func With(key Key, value string) Environ {
return Environ(os.Environ()).With(key, value)
}

func (e Environ) Without(keys ...Key) Environ {
var result Environ
for _, entry := range e {
excluded := false
for _, key := range keys {
if strings.HasPrefix(entry, string(key)+"=") {
excluded = true
break
}
}
if !excluded {
result = append(result, entry)
}
}
return result
}

func (e Environ) With(key Key, value string) Environ {
return append(e, string(key)+"="+value)
Comment on lines +58 to +59
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ensure With overrides deterministically by removing existing keys first.
Appending without dedup can leave multiple entries for the same key; depending on the consumer, earlier entries might win, causing flaky behavior.

🔧 Suggested fix
 func (e Environ) With(key Key, value string) Environ {
-	return append(e, string(key)+"="+value)
+	e = e.Without(key)
+	return append(e, string(key)+"="+value)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/env/env.go` around lines 58 - 59, The With method on Environ
currently appends a new "key=value" string which can leave earlier entries for
the same key and cause non-deterministic behavior; modify Environ.With (the With
function on type Environ that accepts Key) to first remove any existing entries
whose prefix equals string(key)+"=" (filter the slice), then append the new
string(key)+"="+value so the environment deterministically contains only the
updated value for that key.

}
16 changes: 4 additions & 12 deletions test/integration/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"testing"
"time"

"github.com/docker/docker/api/types/container"
"github.com/localstack/lstk/test/integration/env"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -20,8 +20,7 @@ const licenseContainerName = "localstack-aws"

func TestLicenseValidationSuccess(t *testing.T) {
requireDocker(t)
authToken := os.Getenv("LOCALSTACK_AUTH_TOKEN")
require.NotEmpty(t, authToken, "LOCALSTACK_AUTH_TOKEN must be set to run this test")
authToken := env.Require(t, env.AuthToken)

cleanupLicense()
t.Cleanup(cleanupLicense)
Expand Down Expand Up @@ -63,10 +62,7 @@ func TestLicenseValidationSuccess(t *testing.T) {
defer cancel()

cmd := exec.CommandContext(ctx, binaryPath(), "start")
cmd.Env = append(
os.Environ(),
"LOCALSTACK_API_ENDPOINT="+mockServer.URL,
)
cmd.Env = env.With(env.APIEndpoint, mockServer.URL)
output, err := cmd.CombinedOutput()

// Check for validation errors from handler
Expand Down Expand Up @@ -95,11 +91,7 @@ func TestLicenseValidationFailure(t *testing.T) {
defer cancel()

cmd := exec.CommandContext(ctx, binaryPath(), "start")
cmd.Env = append(
os.Environ(),
"LOCALSTACK_API_ENDPOINT="+mockServer.URL,
"LOCALSTACK_AUTH_TOKEN=test-token-for-license-validation",
)
cmd.Env = env.With(env.APIEndpoint, mockServer.URL).With(env.AuthToken, "test-token-for-license-validation")
output, err := cmd.CombinedOutput()

require.Error(t, err, "expected lstk start to fail with forbidden license")
Expand Down
15 changes: 4 additions & 11 deletions test/integration/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ import (
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"runtime"
"testing"
"time"

"github.com/creack/pty"
"github.com/localstack/lstk/test/integration/env"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -77,8 +77,7 @@ func TestDeviceFlowSuccess(t *testing.T) {
t.Cleanup(cleanup)

// Require valid token from environment
licenseToken := os.Getenv("LOCALSTACK_AUTH_TOKEN")
require.NotEmpty(t, licenseToken, "LOCALSTACK_AUTH_TOKEN must be set to run this test")
licenseToken := env.Require(t, env.AuthToken)

// Create mock API server that returns the real token
mockServer := createMockAPIServer(t, licenseToken, true)
Expand All @@ -88,10 +87,7 @@ func TestDeviceFlowSuccess(t *testing.T) {
defer cancel()

cmd := exec.CommandContext(ctx, binaryPath(), "login")
cmd.Env = append(
envWithout("LOCALSTACK_AUTH_TOKEN"),
"LOCALSTACK_API_ENDPOINT="+mockServer.URL,
)
cmd.Env = env.Without(env.AuthToken).With(env.APIEndpoint, mockServer.URL)

ptmx, err := pty.Start(cmd)
require.NoError(t, err, "failed to start command in PTY")
Expand Down Expand Up @@ -153,10 +149,7 @@ func TestDeviceFlowFailure_RequestNotConfirmed(t *testing.T) {
defer cancel()

cmd := exec.CommandContext(ctx, binaryPath(), "login")
cmd.Env = append(
envWithout("LOCALSTACK_AUTH_TOKEN"),
"LOCALSTACK_API_ENDPOINT="+mockServer.URL,
)
cmd.Env = env.Without(env.AuthToken).With(env.APIEndpoint, mockServer.URL)

ptmx, err := pty.Start(cmd)
require.NoError(t, err, "failed to start command in PTY")
Expand Down
Loading