From 7a0384c9ca8ee7580d215c9ecc89b7f8b45a5b9b Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Sun, 7 Sep 2025 00:51:55 +0200 Subject: [PATCH 01/10] Add pull docker image functions --- src/utils/setup.ts | 69 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 721b79c..62410ec 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -1,9 +1,12 @@ import type { LogOutputChannel } from "vscode"; +import * as z from "zod/v4-mini"; import { checkIsAuthenticated } from "./authenticate.ts"; import { checkIsProfileConfigured } from "./configure-aws.ts"; +import { exec } from "./exec.ts"; import { checkLocalstackInstalled } from "./install.ts"; import { checkIsLicenseValid } from "./license.ts"; +import { spawn } from "./spawn.ts"; export async function checkSetupStatus(outputChannel: LogOutputChannel) { const [isInstalled, isAuthenticated, isLicenseValid, isProfileConfigured] = @@ -21,3 +24,69 @@ export async function checkSetupStatus(outputChannel: LogOutputChannel) { isProfileConfigured, }; } + +const LOCALSTACK_DOCKER_IMAGE = "localstack/localstack-pro"; + +export async function updateDockerImage( + outputChannel: LogOutputChannel, +): Promise { + const imageVersion = await getDockerImageSemverVersion(outputChannel); + if (!imageVersion) { + await pullDockerImage(outputChannel); + } +} + +const InspectSchema = z.array( + z.object({ + Config: z.object({ + Env: z.array(z.string()), + }), + }), +); + +async function inspectDockerImage( + outputChannel: LogOutputChannel, +): Promise { + try { + const { stdout } = await exec(`docker inspect ${LOCALSTACK_DOCKER_IMAGE}`); + const data: unknown = JSON.parse(stdout); + const parsed = InspectSchema.safeParse(data); + if (!parsed.success) { + throw new Error( + `Could not parse "docker inspect" output: ${JSON.stringify(z.treeifyError(parsed.error))}`, + ); + } + const env = parsed.data[0]?.Config.Env ?? []; + const version = env + .find((line) => line.startsWith("LOCALSTACK_BUILD_VERSION=")) + ?.slice("LOCALSTACK_BUILD_VERSION=".length); + return version; + } catch (error) { + outputChannel.error("Could not inspect LocalStack docker image"); + outputChannel.error(error instanceof Error ? error : String(error)); + return undefined; + } +} + +async function getDockerImageSemverVersion( + outputChannel: LogOutputChannel, +): Promise { + const imageVersion = await inspectDockerImage(outputChannel); + if (!imageVersion) { + return; + } + + return imageVersion; +} + +async function pullDockerImage(outputChannel: LogOutputChannel): Promise { + try { + await spawn("docker", ["pull", LOCALSTACK_DOCKER_IMAGE], { + outputChannel, + outputLabel: "docker.pull", + }); + } catch (error) { + outputChannel.error("Could not pull LocalStack docker image"); + outputChannel.error(error instanceof Error ? error : String(error)); + } +} From f5a266731da46ef88e5dca27942e638af48ffbae Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Sun, 7 Sep 2025 01:41:29 +0200 Subject: [PATCH 02/10] Start pulling docker image after CLI install If we start pulling docker image as a first thing in the setup process then `docker pull` consumes all the bandwidth and makes CLI download an order of magnitude longer. If we start pulling image right after the install step it still takes advantage of time that user spends in the rest of setup steps. Options considered: start `docker pull` right after CLI is downloaded. This way we could take advantage of time spent in another modal - global/local selection. Trade off is that passing image pull progress from/to runInstallProcess is adding complexity that is not justified with 2-3 seconds saved. --- src/plugins/setup.ts | 17 +++++++++++++++++ src/utils/setup.ts | 11 ++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/plugins/setup.ts b/src/plugins/setup.ts index 70f65fd..87fe7d3 100644 --- a/src/plugins/setup.ts +++ b/src/plugins/setup.ts @@ -14,6 +14,7 @@ import { activateLicenseUntilValid, } from "../utils/license.ts"; import { minDelay } from "../utils/promises.ts"; +import { updateDockerImage } from "../utils/setup.ts"; export default createPlugin( "setup", @@ -93,6 +94,14 @@ export default createPlugin( } } + let imagePulled = false; + const pullImageProcess = updateDockerImage( + outputChannel, + cancellationToken, + ).then(() => { + imagePulled = true; + }); + ///////////////////////////////////////////////////////////////////// progress.report({ message: "Verifying authentication...", @@ -227,6 +236,14 @@ export default createPlugin( }); await minDelay(Promise.resolve()); + if (!imagePulled) { + progress.report({ + message: + "Waiting for the LocalStack docker image to finish downloading...", + }); + await minDelay(pullImageProcess); + } + ///////////////////////////////////////////////////////////////////// if (localStackStatusTracker.status() === "running") { window diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 62410ec..630d549 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -1,4 +1,4 @@ -import type { LogOutputChannel } from "vscode"; +import type { CancellationToken, LogOutputChannel } from "vscode"; import * as z from "zod/v4-mini"; import { checkIsAuthenticated } from "./authenticate.ts"; @@ -29,10 +29,11 @@ const LOCALSTACK_DOCKER_IMAGE = "localstack/localstack-pro"; export async function updateDockerImage( outputChannel: LogOutputChannel, + cancellationToken: CancellationToken, ): Promise { const imageVersion = await getDockerImageSemverVersion(outputChannel); if (!imageVersion) { - await pullDockerImage(outputChannel); + await pullDockerImage(outputChannel, cancellationToken); } } @@ -79,11 +80,15 @@ async function getDockerImageSemverVersion( return imageVersion; } -async function pullDockerImage(outputChannel: LogOutputChannel): Promise { +async function pullDockerImage( + outputChannel: LogOutputChannel, + cancellationToken: CancellationToken, +): Promise { try { await spawn("docker", ["pull", LOCALSTACK_DOCKER_IMAGE], { outputChannel, outputLabel: "docker.pull", + cancellationToken: cancellationToken, }); } catch (error) { outputChannel.error("Could not pull LocalStack docker image"); From 2653a5c70b294be58c0436996eda5ea1b02f78f6 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Sun, 7 Sep 2025 01:51:00 +0200 Subject: [PATCH 03/10] Don't show Start Localstack when a user cancels docker pull --- src/plugins/setup.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/plugins/setup.ts b/src/plugins/setup.ts index 87fe7d3..05671a3 100644 --- a/src/plugins/setup.ts +++ b/src/plugins/setup.ts @@ -244,6 +244,18 @@ export default createPlugin( await minDelay(pullImageProcess); } + if (cancellationToken.isCancellationRequested) { + telemetry.track({ + name: "setup_ended", + payload: { + namespace: "onboarding", + steps: [1, 2, 3], + status: "CANCELLED", + }, + }); + return; + } + ///////////////////////////////////////////////////////////////////// if (localStackStatusTracker.status() === "running") { window From 15bc2b75e4099bf79c9dc3974dc7e0025b70ace6 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Sun, 7 Sep 2025 01:02:28 +0200 Subject: [PATCH 04/10] Add missing awaits --- src/plugins/setup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/setup.ts b/src/plugins/setup.ts index 05671a3..53d95a0 100644 --- a/src/plugins/setup.ts +++ b/src/plugins/setup.ts @@ -204,7 +204,7 @@ export default createPlugin( "License is not valid or not assigned. Open License settings page to activate it.", }); - commands.executeCommand("localstack.openLicensePage"); + await commands.executeCommand("localstack.openLicensePage"); await activateLicenseUntilValid( outputChannel, @@ -229,7 +229,7 @@ export default createPlugin( }), ); - commands.executeCommand("localstack.refreshStatusBar"); + await commands.executeCommand("localstack.refreshStatusBar"); progress.report({ message: 'Finished configuring "localstack" AWS profiles.', From ecca7d67ca26ed3c38e3b26cefa0301d02c8d3b6 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 8 Sep 2025 13:01:09 +0200 Subject: [PATCH 05/10] Merge related functions inspectDockerImage was returning only a limited version data. If we need a generic inspect function later then we could extract it, right now there is no need as there is no other usage of inspect. --- src/utils/setup.ts | 52 ++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 630d549..00f6922 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -45,39 +45,31 @@ const InspectSchema = z.array( }), ); -async function inspectDockerImage( - outputChannel: LogOutputChannel, -): Promise { - try { - const { stdout } = await exec(`docker inspect ${LOCALSTACK_DOCKER_IMAGE}`); - const data: unknown = JSON.parse(stdout); - const parsed = InspectSchema.safeParse(data); - if (!parsed.success) { - throw new Error( - `Could not parse "docker inspect" output: ${JSON.stringify(z.treeifyError(parsed.error))}`, - ); - } - const env = parsed.data[0]?.Config.Env ?? []; - const version = env - .find((line) => line.startsWith("LOCALSTACK_BUILD_VERSION=")) - ?.slice("LOCALSTACK_BUILD_VERSION=".length); - return version; - } catch (error) { - outputChannel.error("Could not inspect LocalStack docker image"); - outputChannel.error(error instanceof Error ? error : String(error)); - return undefined; - } -} - async function getDockerImageSemverVersion( outputChannel: LogOutputChannel, ): Promise { - const imageVersion = await inspectDockerImage(outputChannel); - if (!imageVersion) { - return; - } - - return imageVersion; + try { + const { stdout } = await exec(`docker inspect ${LOCALSTACK_DOCKER_IMAGE}`); + const data: unknown = JSON.parse(stdout); + const parsed = InspectSchema.safeParse(data); + if (!parsed.success) { + throw new Error( + `Could not parse "docker inspect" output: ${JSON.stringify(z.treeifyError(parsed.error))}`, + ); + } + const env = parsed.data[0]?.Config.Env ?? []; + const imageVersion = env + .find((line) => line.startsWith("LOCALSTACK_BUILD_VERSION=")) + ?.slice("LOCALSTACK_BUILD_VERSION=".length); + if (!imageVersion) { + return; + } + return imageVersion; + } catch (error) { + outputChannel.error("Could not inspect LocalStack docker image"); + outputChannel.error(error instanceof Error ? error : String(error)); + return undefined; + } } async function pullDockerImage( From 878347d66f4a97da853c55551496b3793296d748 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 8 Sep 2025 13:10:21 +0200 Subject: [PATCH 06/10] Shorten docker download message --- src/plugins/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/setup.ts b/src/plugins/setup.ts index 53d95a0..27e53f2 100644 --- a/src/plugins/setup.ts +++ b/src/plugins/setup.ts @@ -239,7 +239,7 @@ export default createPlugin( if (!imagePulled) { progress.report({ message: - "Waiting for the LocalStack docker image to finish downloading...", + "Downloading LocalStack docker image...", }); await minDelay(pullImageProcess); } From f1114568219581635c83d381e3cf8ccff4ff9520 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 8 Sep 2025 13:12:17 +0200 Subject: [PATCH 07/10] Fix formatting --- src/plugins/setup.ts | 35 +++++++++++++++++------------------ src/utils/setup.ts | 44 ++++++++++++++++++++++---------------------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/src/plugins/setup.ts b/src/plugins/setup.ts index 27e53f2..85f1853 100644 --- a/src/plugins/setup.ts +++ b/src/plugins/setup.ts @@ -236,25 +236,24 @@ export default createPlugin( }); await minDelay(Promise.resolve()); - if (!imagePulled) { - progress.report({ - message: - "Downloading LocalStack docker image...", - }); - await minDelay(pullImageProcess); - } + if (!imagePulled) { + progress.report({ + message: "Downloading LocalStack docker image...", + }); + await minDelay(pullImageProcess); + } - if (cancellationToken.isCancellationRequested) { - telemetry.track({ - name: "setup_ended", - payload: { - namespace: "onboarding", - steps: [1, 2, 3], - status: "CANCELLED", - }, - }); - return; - } + if (cancellationToken.isCancellationRequested) { + telemetry.track({ + name: "setup_ended", + payload: { + namespace: "onboarding", + steps: [1, 2, 3], + status: "CANCELLED", + }, + }); + return; + } ///////////////////////////////////////////////////////////////////// if (localStackStatusTracker.status() === "running") { diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 00f6922..5a2fbbf 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -48,28 +48,28 @@ const InspectSchema = z.array( async function getDockerImageSemverVersion( outputChannel: LogOutputChannel, ): Promise { - try { - const { stdout } = await exec(`docker inspect ${LOCALSTACK_DOCKER_IMAGE}`); - const data: unknown = JSON.parse(stdout); - const parsed = InspectSchema.safeParse(data); - if (!parsed.success) { - throw new Error( - `Could not parse "docker inspect" output: ${JSON.stringify(z.treeifyError(parsed.error))}`, - ); - } - const env = parsed.data[0]?.Config.Env ?? []; - const imageVersion = env - .find((line) => line.startsWith("LOCALSTACK_BUILD_VERSION=")) - ?.slice("LOCALSTACK_BUILD_VERSION=".length); - if (!imageVersion) { - return; - } - return imageVersion; - } catch (error) { - outputChannel.error("Could not inspect LocalStack docker image"); - outputChannel.error(error instanceof Error ? error : String(error)); - return undefined; - } + try { + const { stdout } = await exec(`docker inspect ${LOCALSTACK_DOCKER_IMAGE}`); + const data: unknown = JSON.parse(stdout); + const parsed = InspectSchema.safeParse(data); + if (!parsed.success) { + throw new Error( + `Could not parse "docker inspect" output: ${JSON.stringify(z.treeifyError(parsed.error))}`, + ); + } + const env = parsed.data[0]?.Config.Env ?? []; + const imageVersion = env + .find((line) => line.startsWith("LOCALSTACK_BUILD_VERSION=")) + ?.slice("LOCALSTACK_BUILD_VERSION=".length); + if (!imageVersion) { + return; + } + return imageVersion; + } catch (error) { + outputChannel.error("Could not inspect LocalStack docker image"); + outputChannel.error(error instanceof Error ? error : String(error)); + return undefined; + } } async function pullDockerImage( From a8f425cde74d686fbb5431d7e8f046c01ba8f8b3 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 8 Sep 2025 14:47:07 +0200 Subject: [PATCH 08/10] Move docker image name to global constants module --- src/constants.ts | 2 ++ src/utils/cli.ts | 4 ++-- src/utils/setup.ts | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index e7af742..4a94541 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,3 +33,5 @@ const CLI_WINDOWS_PATHS = [ export const CLI_PATHS = platform() === "win32" ? CLI_WINDOWS_PATHS : CLI_UNIX_PATHS; + +export const LOCALSTACK_DOCKER_IMAGE_NAME = "localstack/localstack-pro"; diff --git a/src/utils/cli.ts b/src/utils/cli.ts index 99b740c..3db15a1 100644 --- a/src/utils/cli.ts +++ b/src/utils/cli.ts @@ -4,13 +4,13 @@ import { access } from "node:fs/promises"; import type { CancellationToken, LogOutputChannel } from "vscode"; import { workspace } from "vscode"; -import { CLI_PATHS } from "../constants.ts"; +import { CLI_PATHS, LOCALSTACK_DOCKER_IMAGE_NAME } from "../constants.ts"; import { exec } from "./exec.ts"; import { spawn } from "./spawn.ts"; import type { SpawnOptions } from "./spawn.ts"; -const IMAGE_NAME = "localstack/localstack-pro"; +const IMAGE_NAME = LOCALSTACK_DOCKER_IMAGE_NAME; // not using the import directly as the constant name should match the env var const LOCALSTACK_LDM_PREVIEW = "1"; const findLocalStack = async (): Promise => { diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 5a2fbbf..e699399 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -1,6 +1,8 @@ import type { CancellationToken, LogOutputChannel } from "vscode"; import * as z from "zod/v4-mini"; +import {LOCALSTACK_DOCKER_IMAGE_NAME} from "../constants.ts"; + import { checkIsAuthenticated } from "./authenticate.ts"; import { checkIsProfileConfigured } from "./configure-aws.ts"; import { exec } from "./exec.ts"; @@ -25,8 +27,6 @@ export async function checkSetupStatus(outputChannel: LogOutputChannel) { }; } -const LOCALSTACK_DOCKER_IMAGE = "localstack/localstack-pro"; - export async function updateDockerImage( outputChannel: LogOutputChannel, cancellationToken: CancellationToken, @@ -49,7 +49,7 @@ async function getDockerImageSemverVersion( outputChannel: LogOutputChannel, ): Promise { try { - const { stdout } = await exec(`docker inspect ${LOCALSTACK_DOCKER_IMAGE}`); + const { stdout } = await exec(`docker inspect ${LOCALSTACK_DOCKER_IMAGE_NAME}`); const data: unknown = JSON.parse(stdout); const parsed = InspectSchema.safeParse(data); if (!parsed.success) { @@ -77,7 +77,7 @@ async function pullDockerImage( cancellationToken: CancellationToken, ): Promise { try { - await spawn("docker", ["pull", LOCALSTACK_DOCKER_IMAGE], { + await spawn("docker", ["pull", LOCALSTACK_DOCKER_IMAGE_NAME], { outputChannel, outputLabel: "docker.pull", cancellationToken: cancellationToken, From 1836fa310ceb0ca423151015f87a425cb182da9c Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 8 Sep 2025 14:50:12 +0200 Subject: [PATCH 09/10] Don't await refresh status bar --- src/plugins/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/setup.ts b/src/plugins/setup.ts index 85f1853..bb6b826 100644 --- a/src/plugins/setup.ts +++ b/src/plugins/setup.ts @@ -229,7 +229,7 @@ export default createPlugin( }), ); - await commands.executeCommand("localstack.refreshStatusBar"); + void commands.executeCommand("localstack.refreshStatusBar"); progress.report({ message: 'Finished configuring "localstack" AWS profiles.', From 035ece401a91b6cced8e45146144fd3ca767a797 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Mon, 8 Sep 2025 14:52:02 +0200 Subject: [PATCH 10/10] Fix formatting --- src/utils/setup.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/setup.ts b/src/utils/setup.ts index e699399..7589003 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -1,7 +1,7 @@ import type { CancellationToken, LogOutputChannel } from "vscode"; import * as z from "zod/v4-mini"; -import {LOCALSTACK_DOCKER_IMAGE_NAME} from "../constants.ts"; +import { LOCALSTACK_DOCKER_IMAGE_NAME } from "../constants.ts"; import { checkIsAuthenticated } from "./authenticate.ts"; import { checkIsProfileConfigured } from "./configure-aws.ts"; @@ -49,7 +49,9 @@ async function getDockerImageSemverVersion( outputChannel: LogOutputChannel, ): Promise { try { - const { stdout } = await exec(`docker inspect ${LOCALSTACK_DOCKER_IMAGE_NAME}`); + const { stdout } = await exec( + `docker inspect ${LOCALSTACK_DOCKER_IMAGE_NAME}`, + ); const data: unknown = JSON.parse(stdout); const parsed = InspectSchema.safeParse(data); if (!parsed.success) {