Skip to content
Closed
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,15 @@ lstk --config /path/to/config.toml start
```toml
[[containers]]
type = "aws" # Emulator type. Currently supported: "aws"
tag = "latest" # Docker image tag, e.g. "latest", "2026.03"
tag = "stable" # Docker image tag, e.g. "stable", "latest", "2026.03"
port = "4566" # Host port the emulator will be accessible on
# volume = "" # Host directory for persistent state (default: OS cache dir)
# env = [] # Named environment profiles to apply (see [env.*] sections below)
```

**Fields:**
- `type`: emulator type; only `"aws"` is supported for now
- `tag`: Docker image tag for LocalStack (e.g. `"latest"`, `"4.14.0"`); useful for pinning a version
- `tag`: Docker image tag for LocalStack (e.g. `"latest"`, `"2026.03"`); useful for pinning a version
- `port`: port LocalStack listens on (default `4566`)
- `volume`: (optional) host directory for persistent emulator state (default: OS cache dir)
- `env`: (optional) list of named environment variable groups to inject into the container (see below)
Expand Down
4 changes: 2 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ var defaultConfigTemplate string
type Config struct {
Containers []ContainerConfig `mapstructure:"containers"`
Env map[string]map[string]string `mapstructure:"env"`
UpdatePrompt bool `mapstructure:"update_prompt"`
UpdatePrompt bool `mapstructure:"update_prompt"`
}

func setDefaults() {
viper.SetDefault("containers", []map[string]any{
{
"type": "aws",
"tag": "latest",
"tag": "stable",
"port": "4566",
},
})
Expand Down
4 changes: 2 additions & 2 deletions internal/config/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ func (c *ContainerConfig) Image() (string, error) {
return fmt.Sprintf("%s/%s:%s", dockerRegistry, productName, tag), nil
}

// Name returns the container name: "localstack-{type}" or "localstack-{type}-{tag}" if tag != latest
// Name returns the container name: "localstack-{type}" or "localstack-{type}-{tag}" if tag is a pinned version
func (c *ContainerConfig) Name() string {
tag := c.Tag
if tag == "" || tag == "latest" {
if tag == "" || tag == "latest" || tag == "stable" {
return fmt.Sprintf("localstack-%s", c.Type)
}
return fmt.Sprintf("localstack-%s-%s", c.Type, tag)
Expand Down
2 changes: 1 addition & 1 deletion internal/config/default_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# You can define multiple to run them side by side.
[[containers]]
type = "aws" # Emulator type. Currently supported: "aws"
tag = "latest" # Docker image tag, e.g. "latest", "2026.03"
tag = "stable" # Docker image tag, e.g. "stable", "latest", "2026.03"
port = "4566" # Host port the emulator will be accessible on
# volume = "" # Host directory for persistent state (default: OS cache dir)
# env = [] # Named environment profiles to apply (see [env.*] sections below)
Expand Down
4 changes: 2 additions & 2 deletions internal/container/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,12 @@ func pullImages(ctx context.Context, rt runtime.Runtime, sink output.Sink, tel *
}

// Validates licenses before pulling where the version is known.
// Pinned tags are validated immediately; "latest" tags are resolved via the catalog API.
// Pinned tags are validated immediately; "latest" and "stable" tags are resolved via the catalog API.
// Returns containers that couldn't be resolved (catalog unavailable) for post-pull validation.
func tryPrePullLicenseValidation(ctx context.Context, sink output.Sink, opts StartOptions, tel *telemetry.Client, containers []runtime.ContainerConfig, token string) ([]runtime.ContainerConfig, error) {
var needsPostPull []runtime.ContainerConfig
for _, c := range containers {
if c.Tag != "" && c.Tag != "latest" {
if c.Tag != "" && c.Tag != "latest" && c.Tag != "stable" {
if err := validateLicense(ctx, sink, opts, tel, c, token); err != nil {
return nil, err
}
Expand Down
5 changes: 4 additions & 1 deletion test/integration/version_resolution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func createVersionResolutionMockServer(t *testing.T, catalogVersion string, lice
}

// Verifies that when the catalog API returns a version, the license request uses
// that version (not "latest"), allowing license validation to happen before pulling the image.
// that version (not "stable" or "latest"), allowing license validation to happen before pulling the image.
func TestVersionResolvedViaCatalog(t *testing.T) {
requireDocker(t)
_ = env.Require(t, env.AuthToken)
Expand All @@ -71,6 +71,8 @@ func TestVersionResolvedViaCatalog(t *testing.T) {
"license request should carry the version returned by the catalog API")
assert.NotEqual(t, "latest", *capturedVersion,
"license request should not use the unresolved 'latest' tag")
assert.NotEqual(t, "stable", *capturedVersion,
"license request should not use the unresolved 'stable' tag")
}

// Verifies that when the catalog endpoint is unavailable, the version is resolved
Expand All @@ -91,6 +93,7 @@ func TestVersionFallsBackToImageInspectionWhenCatalogFails(t *testing.T) {

assert.NotEmpty(t, *capturedVersion, "license request should carry a version resolved from image inspection")
assert.NotEqual(t, "latest", *capturedVersion, "resolved version should not be the unresolved 'latest' tag")
assert.NotEqual(t, "stable", *capturedVersion, "resolved version should not be the unresolved 'stable' tag")
}

// Verifies that when both the catalog endpoint is unavailable and the license
Expand Down