Skip to content

Conversation

@carole-lavillonniere
Copy link
Collaborator

@carole-lavillonniere carole-lavillonniere commented Feb 12, 2026

The CLI now validates licenses upfront via the platform API, providing clear error messages if validation fails, rather than waiting for the container to fail during startup.
This should be further improved in the future by resolving the version for the latest tag without having to pull the images (DRG-504, DRG-508).

@coderabbitai
Copy link

coderabbitai bot commented Feb 12, 2026

📝 Walkthrough

Walkthrough

Adds a shared PlatformClient and license-validation flow: commands construct and pass a PlatformClient into auth and container startup; Start inspects image versions and calls PlatformAPI.GetLicense before starting containers. Tests and env examples updated for the new API endpoint and license checks.

Changes

Cohort / File(s) Summary
Command plumbing
cmd/logout.go, cmd/root.go, cmd/start.go, cmd/login.go
Create platformClient with api.NewPlatformClient() and pass it into auth.New(...) and container.Start(...); imports updated accordingly.
Platform API & types
internal/api/client.go
Added PlatformAPI.GetLicense(ctx,*LicenseRequest) error and implemented on PlatformClient; introduced exported types LicenseRequest, ProductInfo, CredentialsInfo, MachineInfo.
Auth integration
internal/auth/auth.go, internal/auth/login.go
auth.New(...) now accepts api.PlatformAPI; browser login constructor updated to accept injected platformClient.
Container startup & license flow
internal/container/start.go
Start signature updated to accept api.PlatformAPI; added validateLicense(...), perform image pulls, resolve image versions, and call GetLicense before starting containers.
Config image naming
internal/config/config.go
Added ContainerConfig.ProductName() and changed Image() to construct localstack/{product}:{tag}; emulator image mapping adjusted.
Runtime image introspection
internal/runtime/runtime.go, internal/runtime/docker.go
Added Runtime.GetImageVersion(ctx,imageName) (string,error); DockerRuntime inspects image ENV for LOCALSTACK_BUILD_VERSION.
Environment example
env.example
Added LOCALSTACK_API_ENDPOINT and LOCALSTACK_WEB_APP_URL.
Integration tests & helpers
test/integration/...
license_test.go, login_browser_flow_test.go, login_device_flow_test.go, main_test.go, start_test.go
Added license validation tests and mock license endpoints; updated mock API servers, env helpers (envWithout), and tests to set LOCALSTACK_API_ENDPOINT and assert license success/failure flows.
Other
internal/auth/*, internal/runtime/*
Signatures adjusted to accept the PlatformAPI where appropriate; small supporting imports and error handling additions.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI/Command
    participant Container as Container.Start()
    participant Platform as PlatformClient
    participant API as License API

    CLI->>Container: Start(ctx, rt, platformClient, onProgress)
    Container->>Container: Pull images
    Container->>Container: Resolve image versions (GetImageVersion)
    Container->>Platform: GetLicense(ctx, LicenseRequest)
    Platform->>API: POST /v1/license/request
    API-->>Platform: 200 / 400 / 403
    alt 200 OK
        Platform-->>Container: nil
        Container->>Container: Start containers
        Container-->>CLI: Initialization complete
    else 400/403
        Platform-->>Container: error
        Container-->>CLI: Initialization failed
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Validate license before starting container' directly and clearly describes the main change: adding license validation before container startup.
Description check ✅ Passed The description explains the license validation feature and relates to the changeset, providing context about platform API usage and error handling.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch carole/drg-444

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
internal/config/config.go (1)

19-21: Consider renaming emulatorImages to emulatorProductNames for clarity.

The map now stores product names (e.g., "localstack-pro") rather than full image references. The current name emulatorImages is misleading.

♻️ Suggested rename
-var emulatorImages = map[EmulatorType]string{
+var emulatorProductNames = map[EmulatorType]string{
 	EmulatorAWS: "localstack-pro",
 }

And update the reference in ProductName():

 func (c *ContainerConfig) ProductName() (string, error) {
-	productName, ok := emulatorImages[c.Type]
+	productName, ok := emulatorProductNames[c.Type]
 	if !ok {
 		return "", fmt.Errorf("%s emulator not supported yet by lstk", c.Type)
 	}
 	return productName, nil
 }
test/integration/license_test.go (1)

40-46: Missing validation of product["version"] field.

The validation checks product["name"] but not product["version"]. According to the API contract (as seen in internal/api/client.go), the ProductInfo struct includes a Version field. Consider adding a check to ensure it's not empty.

🛡️ Proposed fix to add version validation
 			// Validate with safe type assertions
 			product, ok := req["product"].(map[string]interface{})
-			if !ok || product["name"] != "localstack-pro" {
+			if !ok || product["name"] != "localstack-pro" || product["version"] == "" {
 				validationErrors <- fmt.Errorf("invalid product field")
 				http.Error(w, "invalid product", http.StatusBadRequest)
 				return
 			}

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@test/integration/license_test.go`:
- Around line 40-45: The HTTP handler uses assert calls and type assertions that
can panic in a separate goroutine; create a validationErrors channel (e.g.,
validationErrors := make(chan error, 1)) and inside the handler replace assert.*
calls and direct type assertions on req with safe checks (use the comma-ok form
or type switches for req["product"] and req["credentials"], compare values
against authToken, and on any mismatch send a descriptive error to
validationErrors instead of calling assert). After the request completes in the
test, perform a non-blocking select on validationErrors (e.g., select { case err
:= <-validationErrors: t.Fatalf("request validation failed: %v", err) default:
}) to fail the test if the handler reported an error.

In `@test/integration/login_device_flow_test.go`:
- Line 146: The test currently removes LOCALSTACK_API_ENDPOINT causing
NewPlatformClient() to hit the real https://api.localstack.cloud; instead create
a local mock HTTP server (e.g., httptest.Server) that returns {"confirmed":
false} for the platform check, then set cmd.Env to include
LOCALSTACK_API_ENDPOINT pointing to that mock server URL (rather than removing
it) so NewPlatformClient() uses the local endpoint; update the test to
start/close the mock server around the test and ensure the mock handler returns
the deterministic failure response referenced by NewPlatformClient().
🧹 Nitpick comments (5)
env.example (1)

2-3: Consider adding comments to document these variables.

The new environment variables lack documentation explaining their purpose and when they're needed. Adding brief comments would improve clarity for users setting up the environment.

📝 Suggested documentation enhancement
 export LOCALSTACK_AUTH_TOKEN=ls-...
+# Platform API endpoint for license validation
 export LOCALSTACK_API_ENDPOINT=https://api.staging.aws.localstack.cloud
+# Web application URL for the LocalStack platform
 export LOCALSTACK_WEB_APP_URL=https://app.staging.aws.localstack.cloud
internal/config/config.go (1)

67-73: Consider reusing ProductName() inside Image() to avoid duplicate lookups.

♻️ Suggested refactor
 func (c *ContainerConfig) Image() (string, error) {
-	productName, ok := emulatorImages[c.Type]
-	if !ok {
-		return "", fmt.Errorf("%s emulator not supported yet by lstk", c.Type)
-	}
+	productName, err := c.ProductName()
+	if err != nil {
+		return "", err
+	}
 	tag := c.Tag
 	if tag == "" {
 		tag = "latest"
 	}
 	return fmt.Sprintf("localstack/%s:%s", productName, tag), nil
 }
internal/auth/auth.go (1)

18-26: Guard against nil platformClient to avoid runtime panics.

🛡️ Suggested guard
 func New(platformClient api.PlatformAPI) (*Auth, error) {
+	if platformClient == nil {
+		return nil, errors.New("platform client is required")
+	}
 	kr, err := newSystemKeyring()
 	if err != nil {
 		return nil, err
 	}
test/integration/login_browser_flow_test.go (1)

63-64: Hardcoded sleep is fragile for synchronization.

Using time.Sleep(1 * time.Second) to wait for the callback server may cause flaky tests if startup is delayed. Consider polling the endpoint until it responds or using a ready signal.

♻️ Suggested improvement using polling
-	// Wait for callback server to be ready
-	time.Sleep(1 * time.Second)
+	// Wait for callback server to be ready by polling
+	for i := 0; i < 10; i++ {
+		conn, err := net.Dial("tcp", "127.0.0.1:45678")
+		if err == nil {
+			conn.Close()
+			break
+		}
+		time.Sleep(100 * time.Millisecond)
+	}
internal/api/client.go (1)

221-230: Consider including server error details in failure messages.

The hardcoded error messages for 400/403 don't include any details from the response body. If the server provides diagnostic information, it would be helpful for debugging.

♻️ Optional: Read error response body
 	switch resp.StatusCode {
 	case http.StatusOK:
 		return nil
 	case http.StatusBadRequest:
+		body, _ := io.ReadAll(resp.Body)
+		if len(body) > 0 {
+			return fmt.Errorf("license validation failed: %s", string(body))
+		}
 		return fmt.Errorf("license validation failed: invalid token format, missing license assignment, or missing subscription")
 	case http.StatusForbidden:
+		body, _ := io.ReadAll(resp.Body)
+		if len(body) > 0 {
+			return fmt.Errorf("license validation failed: %s", string(body))
+		}
 		return fmt.Errorf("license validation failed: invalid, inactive, or expired authentication token or subscription")
 	default:
 		return fmt.Errorf("license request failed with status %d", resp.StatusCode)
 	}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
test/integration/login_browser_flow_test.go (1)

73-76: ⚠️ Potential issue | 🟠 Major

Test design conflates login flow with container startup, causing CI failure.

The pipeline failure indicates the test fails because localstack/localstack-pro:latest cannot be pulled in the CI environment. The test expects both "Login successful" and "License activation failed" messages, but the actual failure point is during image pulling, not license validation.

If the goal is to test the browser login flow and token storage, consider stopping after verifying the token is stored, rather than letting the flow proceed to container startup. Alternatively, mock the runtime/container layer to avoid real image pulls.

🛠️ Suggested approach: Focus the test on login flow only

Consider restructuring the test to either:

  1. Mock the container runtime to avoid image pulls, or
  2. End the test after login succeeds and token is stored, before container startup begins
-	// Login should succeed, but container will fail with invalid token
-	assert.Contains(t, string(out), "Login successful")
-	assert.Contains(t, string(out), "License activation failed")
+	// Verify login succeeded (container startup may fail in CI without images)
+	assert.Contains(t, string(out), "Login successful")
test/integration/login_device_flow_test.go (1)

121-129: ⚠️ Potential issue | 🟠 Major

Add retry loop before asserting container is running. The container creation is asynchronous; the CLI command completes before the container is fully started, causing the "No such container: localstack-aws" failure. The context already has a 2-minute timeout available, so a short retry with 30-second deadline and 500ms polling is appropriate.

🔧 Suggested retry to reduce flakiness
-        inspect, err := dockerClient.ContainerInspect(ctx, containerName)
-        require.NoError(t, err)
-        assert.True(t, inspect.State.Running, "container should be running")
+        deadline := time.Now().Add(30 * time.Second)
+        for {
+            inspect, err := dockerClient.ContainerInspect(ctx, containerName)
+            if err == nil && inspect.State.Running {
+                break
+            }
+            if time.Now().After(deadline) {
+                t.Fatalf("container should be running; last error: %v", err)
+            }
+            time.Sleep(500 * time.Millisecond)
+        }
🤖 Fix all issues with AI agents
In `@test/integration/license_test.go`:
- Around line 21-84: Tests fail because images are pulled in the startup path
before license validation; move or gate the image-pull logic so license
validation occurs first. In the startup flow in start.go, change the sequence
where images are pulled (the pullImages / ensureImagesPulled call inside the
Start/StartContainers flow) to first call the license validation routine
(validateLicenseForTag or the existing license validation code path) for each
relevant image tag (including resolving 'latest' to a check without attempting a
full pull), or add a runtime flag/mocker hook that skips real pulls during
tests; update the start sequence to only pull images after licenses are
validated or when a skip-pull/test-mode flag is not set so tests can run without
network image pulls.
🧹 Nitpick comments (4)
internal/config/config.go (1)

67-73: Consider extracting the common lookup logic.

ProductName() and Image() both perform identical map lookups with the same error handling. This is minor duplication but could be consolidated.

♻️ Optional: Extract common lookup
+func (c *ContainerConfig) productName() (string, error) {
+	name, ok := emulatorImages[c.Type]
+	if !ok {
+		return "", fmt.Errorf("%s emulator not supported yet by lstk", c.Type)
+	}
+	return name, nil
+}
+
 func (c *ContainerConfig) Image() (string, error) {
-	productName, ok := emulatorImages[c.Type]
-	if !ok {
-		return "", fmt.Errorf("%s emulator not supported yet by lstk", c.Type)
+	productName, err := c.productName()
+	if err != nil {
+		return "", err
 	}
 	tag := c.Tag
 	if tag == "" {
 		tag = "latest"
 	}
 	return fmt.Sprintf("localstack/%s:%s", productName, tag), nil
 }
 
 func (c *ContainerConfig) ProductName() (string, error) {
-	productName, ok := emulatorImages[c.Type]
-	if !ok {
-		return "", fmt.Errorf("%s emulator not supported yet by lstk", c.Type)
-	}
-	return productName, nil
+	return c.productName()
 }
internal/container/start.go (1)

122-122: Silently ignoring os.Hostname() error.

The error from os.Hostname() is discarded, resulting in an empty hostname if it fails. This may be intentional for simplicity, but consider logging the error for debugging purposes.

♻️ Optional: Log hostname retrieval failure
-	hostname, _ := os.Hostname()
+	hostname, err := os.Hostname()
+	if err != nil {
+		log.Printf("Could not retrieve hostname: %v", err)
+	}
test/integration/login_browser_flow_test.go (1)

25-39: Mock server is incomplete for the browser login flow.

The mock server returns an exchange_token in the auth request response, but doesn't handle the token exchange endpoint (/v1/auth/request/{id}/exchange). The browser callback stores the token directly via the callback URL, bypassing this, but having a more complete mock would improve test reliability and documentation of the expected flow.

Additionally, the license endpoint returns StatusOK, which contradicts the test's expectation of "License activation failed" at line 75.

cmd/root.go (1)

22-38: Code duplication between rootCmd.Run and startCmd.Run.

The Run function here is nearly identical to startCmd.Run in cmd/start.go (lines 17-33). Both create a Docker runtime, platform client, progress callback, and call container.Start. Consider extracting this shared logic into a helper function to reduce duplication and ensure both entry points stay in sync.

♻️ Suggested refactor to extract shared startup logic
// In a shared location, e.g., cmd/helpers.go
func runStart(ctx context.Context) error {
    rt, err := runtime.NewDockerRuntime()
    if err != nil {
        return err
    }
    
    platformClient := api.NewPlatformClient()
    
    onProgress := func(msg string) {
        fmt.Println(msg)
    }
    
    return container.Start(ctx, rt, platformClient, onProgress)
}

Then both commands can call:

if err := runStart(cmd.Context()); err != nil {
    fmt.Fprintf(os.Stderr, "Error: %v\n", err)
    os.Exit(1)
}

@carole-lavillonniere carole-lavillonniere force-pushed the carole/drg-444 branch 3 times, most recently from 42b7cf1 to 8adb3bc Compare February 12, 2026 14:50
@carole-lavillonniere carole-lavillonniere marked this pull request as ready for review February 12, 2026 14:50
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@internal/auth/auth.go`:
- Around line 18-26: The constructor New should guard against a nil
platformClient to fail fast and avoid panics when browserLogin is later used;
update New to check if platformClient == nil and return a descriptive error
(instead of creating browserLogin) so callers get an immediate error,
referencing the New function, the Auth struct, browserLogin field, and
newBrowserLogin helper when implementing the check.

In `@internal/container/start.go`:
- Around line 122-135: The license request currently sets
Machine.PlatformRelease to stdruntime.GOARCH (CPU arch) instead of the OS
release; change it to call a helper that returns the OS release string (e.g.,
getOSRelease()) and assign that to api.MachineInfo.PlatformRelease when building
licenseReq. Implement getOSRelease() to detect runtime.GOOS and return the OS
release appropriately (for Unix-like systems run "uname -r" or use syscall/unix
uname, for Windows use the appropriate Windows API/registry call), and ensure
any errors fall back to an empty string or a safe default; update the
construction in start.go to use getOSRelease() instead of stdruntime.GOARCH.

In `@test/integration/login_browser_flow_test.go`:
- Around line 43-46: The child process may receive a duplicate
LOCALSTACK_API_ENDPOINT if the parent env already contains it; update the
envWithout call used to build cmd.Env so it removes both keys before appending
the single override. Replace envWithout("LOCALSTACK_AUTH_TOKEN") with
envWithout("LOCALSTACK_AUTH_TOKEN", "LOCALSTACK_API_ENDPOINT") so cmd.Env =
append(envWithout(...), "LOCALSTACK_API_ENDPOINT="+mockServer.URL) to ensure
only the intended endpoint is present.

@carole-lavillonniere carole-lavillonniere marked this pull request as draft February 12, 2026 15:04
@carole-lavillonniere carole-lavillonniere marked this pull request as ready for review February 12, 2026 15:25
Copy link
Member

@silv-io silv-io left a comment

Choose a reason for hiding this comment

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

Looks good, just some minor nits :)

tag = "latest"
}
return fmt.Sprintf("%s:%s", baseImage, tag), nil
return fmt.Sprintf("localstack/%s:%s", productName, tag), nil
Copy link
Member

Choose a reason for hiding this comment

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

nit: maybe lets put the "localstack" somewhere as a const named vendor or just localstack to avoid typos

return path, nil
}

func (c *ContainerConfig) ProductName() (string, error) {
Copy link
Member

Choose a reason for hiding this comment

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

q: did we check that the other products are actually binding their product name to the image name? I think for snowflake it might not be just snowflake

if err != nil {
log.Printf("Could not resolve version from image %s: %v", containerConfig.Image, err)
if version == "" {
version = "latest"
Copy link
Member

Choose a reason for hiding this comment

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

q: does this now work after all or is this just a placeholder until it does?

Comment on lines +23 to +24
authToken := os.Getenv("LOCALSTACK_AUTH_TOKEN")
require.NotEmpty(t, authToken, "LOCALSTACK_AUTH_TOKEN must be set to run this test")
Copy link
Member

Choose a reason for hiding this comment

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

nit: would swap those two lines to have all the preconditions together at the start :D

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

They cannot be swapped since line 24 relies on line 23, or did you mean something else?

validationErrors := make(chan error, 1)

// Mock platform API that returns success
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Copy link
Member

Choose a reason for hiding this comment

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

it's verbose but I like it

}

func envWithoutAuthToken() []string {
func envWithout(keys ...string) []string {
Copy link
Member

Choose a reason for hiding this comment

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

praise: nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants