Skip to content
Open
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 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 && LOCALSTACK_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/spf13/cobra"
Expand All @@ -18,6 +19,7 @@ var rootCmd = &cobra.Command{
Short: "LocalStack CLI",
Long: "lstk is the command-line interface for LocalStack.",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
env.Init()
return config.Init()
},
Run: func(cmd *cobra.Command, args []string) {
Expand Down
10 changes: 7 additions & 3 deletions env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Authentication token (optional - will trigger device flow login if not set)
export LOCALSTACK_AUTH_TOKEN=ls-...

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

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

# Force file-based keyring backend instead of system keychain (optional)
# export LOCALSTACK_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
4 changes: 2 additions & 2 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 Down Expand Up @@ -35,7 +35,7 @@ func (a *Auth) GetToken(ctx context.Context) (string, error) {
return token, nil
}

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

Expand Down
8 changes: 2 additions & 6 deletions internal/auth/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import (
"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"
const loginCallbackURL = "127.0.0.1:45678"

type LoginProvider interface {
Expand Down Expand Up @@ -121,11 +121,7 @@ func (b *browserLogin) 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 (b *browserLogin) completeDeviceFlow(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 newAuthTokenStorage() (*authTokenStorage, error) {
},
}

// Force file backend if KEYRING env var is set to "file"
if os.Getenv("KEYRING") == "file" {
// Force file backend if LOCALSTACK_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("LOCALSTACK")
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"),
}
}
68 changes: 68 additions & 0 deletions test/integration/env/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package env

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

// Key is a declared environment variable name.
type Key string

const (
AuthToken Key = "LOCALSTACK_AUTH_TOKEN"
APIEndpoint Key = "LOCALSTACK_API_ENDPOINT"
Keyring Key = "LOCALSTACK_KEYRING"
CI Key = "CI"
)

// Get returns the value of the given environment variable.
func Get(key Key) string {
return os.Getenv(string(key))
}

// Require returns the value of the given environment variable, failing the test if it is not set.
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
}

// Environ is a slice of "KEY=value" environment variable strings.
type Environ []string

// Without returns the current process environment excluding the given keys.
func Without(keys ...Key) Environ {
return Environ(os.Environ()).Without(keys...)
}

// With returns the current process environment with key=value appended.
func With(key Key, value string) Environ {
return Environ(os.Environ()).With(key, value)
}

// Without returns a copy of e excluding any variable whose key matches one of the given keys.
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
}

// With returns a copy of e with key=value appended.
func (e Environ) With(key Key, value string) Environ {
return append(e, string(key)+"="+value)
}
15 changes: 4 additions & 11 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,10 +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,
)
cmd.Env = env.With(env.APIEndpoint, mockServer.URL)
output, err := cmd.CombinedOutput()

require.Error(t, err, "expected lstk start to fail with forbidden license")
Expand Down
6 changes: 2 additions & 4 deletions test/integration/login_browser_flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"
"time"

"github.com/localstack/lstk/test/integration/env"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -39,10 +40,7 @@ func TestBrowserFlowStoresToken(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)

// Keep stdin open so ENTER listener doesn't trigger immediately
stdinPipe, err := cmd.StdinPipe()
Expand Down
15 changes: 4 additions & 11 deletions test/integration/login_device_flow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"testing"
"time"

"github.com/localstack/lstk/test/integration/env"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -69,8 +69,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 @@ -80,10 +79,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)

// Keep stdin open and get the pipe to simulate ENTER
stdinPipe, err := cmd.StdinPipe()
Expand Down Expand Up @@ -137,10 +133,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)

// Keep stdin open and get the pipe to simulate ENTER
stdinPipe, err := cmd.StdinPipe()
Expand Down
Loading