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/plugins/setup.ts b/src/plugins/setup.ts index 70f65fd..bb6b826 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...", @@ -195,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, @@ -220,13 +229,32 @@ export default createPlugin( }), ); - commands.executeCommand("localstack.refreshStatusBar"); + void commands.executeCommand("localstack.refreshStatusBar"); progress.report({ message: 'Finished configuring "localstack" AWS profiles.', }); await minDelay(Promise.resolve()); + 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 (localStackStatusTracker.status() === "running") { window 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 721b79c..7589003 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -1,9 +1,14 @@ -import type { LogOutputChannel } from "vscode"; +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"; 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 +26,66 @@ export async function checkSetupStatus(outputChannel: LogOutputChannel) { isProfileConfigured, }; } + +export async function updateDockerImage( + outputChannel: LogOutputChannel, + cancellationToken: CancellationToken, +): Promise { + const imageVersion = await getDockerImageSemverVersion(outputChannel); + if (!imageVersion) { + await pullDockerImage(outputChannel, cancellationToken); + } +} + +const InspectSchema = z.array( + z.object({ + Config: z.object({ + Env: z.array(z.string()), + }), + }), +); + +async function getDockerImageSemverVersion( + outputChannel: LogOutputChannel, +): Promise { + try { + const { stdout } = await exec( + `docker inspect ${LOCALSTACK_DOCKER_IMAGE_NAME}`, + ); + 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( + outputChannel: LogOutputChannel, + cancellationToken: CancellationToken, +): Promise { + try { + await spawn("docker", ["pull", LOCALSTACK_DOCKER_IMAGE_NAME], { + outputChannel, + outputLabel: "docker.pull", + cancellationToken: cancellationToken, + }); + } catch (error) { + outputChannel.error("Could not pull LocalStack docker image"); + outputChannel.error(error instanceof Error ? error : String(error)); + } +}