Skip to content

Commit 00ff25a

Browse files
authored
feat: pre-fetch docker image (#36)
Adds start pulling docker image if doesn't exist after CLI install. This saves user time as image is pulled anyway and first LocalStack run. Reasoning: 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. But 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.
1 parent 452be25 commit 00ff25a

File tree

4 files changed

+103
-5
lines changed

4 files changed

+103
-5
lines changed

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ const CLI_WINDOWS_PATHS = [
3333

3434
export const CLI_PATHS =
3535
platform() === "win32" ? CLI_WINDOWS_PATHS : CLI_UNIX_PATHS;
36+
37+
export const LOCALSTACK_DOCKER_IMAGE_NAME = "localstack/localstack-pro";

src/plugins/setup.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
activateLicenseUntilValid,
1515
} from "../utils/license.ts";
1616
import { minDelay } from "../utils/promises.ts";
17+
import { updateDockerImage } from "../utils/setup.ts";
1718

1819
export default createPlugin(
1920
"setup",
@@ -93,6 +94,14 @@ export default createPlugin(
9394
}
9495
}
9596

97+
let imagePulled = false;
98+
const pullImageProcess = updateDockerImage(
99+
outputChannel,
100+
cancellationToken,
101+
).then(() => {
102+
imagePulled = true;
103+
});
104+
96105
/////////////////////////////////////////////////////////////////////
97106
progress.report({
98107
message: "Verifying authentication...",
@@ -195,7 +204,7 @@ export default createPlugin(
195204
"License is not valid or not assigned. Open License settings page to activate it.",
196205
});
197206

198-
commands.executeCommand("localstack.openLicensePage");
207+
await commands.executeCommand("localstack.openLicensePage");
199208

200209
await activateLicenseUntilValid(
201210
outputChannel,
@@ -220,13 +229,32 @@ export default createPlugin(
220229
}),
221230
);
222231

223-
commands.executeCommand("localstack.refreshStatusBar");
232+
void commands.executeCommand("localstack.refreshStatusBar");
224233

225234
progress.report({
226235
message: 'Finished configuring "localstack" AWS profiles.',
227236
});
228237
await minDelay(Promise.resolve());
229238

239+
if (!imagePulled) {
240+
progress.report({
241+
message: "Downloading LocalStack docker image...",
242+
});
243+
await minDelay(pullImageProcess);
244+
}
245+
246+
if (cancellationToken.isCancellationRequested) {
247+
telemetry.track({
248+
name: "setup_ended",
249+
payload: {
250+
namespace: "onboarding",
251+
steps: [1, 2, 3],
252+
status: "CANCELLED",
253+
},
254+
});
255+
return;
256+
}
257+
230258
/////////////////////////////////////////////////////////////////////
231259
if (localStackStatusTracker.status() === "running") {
232260
window

src/utils/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { access } from "node:fs/promises";
44
import type { CancellationToken, LogOutputChannel } from "vscode";
55
import { workspace } from "vscode";
66

7-
import { CLI_PATHS } from "../constants.ts";
7+
import { CLI_PATHS, LOCALSTACK_DOCKER_IMAGE_NAME } from "../constants.ts";
88

99
import { exec } from "./exec.ts";
1010
import { spawn } from "./spawn.ts";
1111
import type { SpawnOptions } from "./spawn.ts";
1212

13-
const IMAGE_NAME = "localstack/localstack-pro";
13+
const IMAGE_NAME = LOCALSTACK_DOCKER_IMAGE_NAME; // not using the import directly as the constant name should match the env var
1414
const LOCALSTACK_LDM_PREVIEW = "1";
1515

1616
const findLocalStack = async (): Promise<string> => {

src/utils/setup.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import type { LogOutputChannel } from "vscode";
1+
import type { CancellationToken, LogOutputChannel } from "vscode";
2+
import * as z from "zod/v4-mini";
3+
4+
import { LOCALSTACK_DOCKER_IMAGE_NAME } from "../constants.ts";
25

36
import { checkIsAuthenticated } from "./authenticate.ts";
47
import { checkIsProfileConfigured } from "./configure-aws.ts";
8+
import { exec } from "./exec.ts";
59
import { checkLocalstackInstalled } from "./install.ts";
610
import { checkIsLicenseValid } from "./license.ts";
11+
import { spawn } from "./spawn.ts";
712

813
export async function checkSetupStatus(outputChannel: LogOutputChannel) {
914
const [isInstalled, isAuthenticated, isLicenseValid, isProfileConfigured] =
@@ -21,3 +26,66 @@ export async function checkSetupStatus(outputChannel: LogOutputChannel) {
2126
isProfileConfigured,
2227
};
2328
}
29+
30+
export async function updateDockerImage(
31+
outputChannel: LogOutputChannel,
32+
cancellationToken: CancellationToken,
33+
): Promise<void> {
34+
const imageVersion = await getDockerImageSemverVersion(outputChannel);
35+
if (!imageVersion) {
36+
await pullDockerImage(outputChannel, cancellationToken);
37+
}
38+
}
39+
40+
const InspectSchema = z.array(
41+
z.object({
42+
Config: z.object({
43+
Env: z.array(z.string()),
44+
}),
45+
}),
46+
);
47+
48+
async function getDockerImageSemverVersion(
49+
outputChannel: LogOutputChannel,
50+
): Promise<string | undefined> {
51+
try {
52+
const { stdout } = await exec(
53+
`docker inspect ${LOCALSTACK_DOCKER_IMAGE_NAME}`,
54+
);
55+
const data: unknown = JSON.parse(stdout);
56+
const parsed = InspectSchema.safeParse(data);
57+
if (!parsed.success) {
58+
throw new Error(
59+
`Could not parse "docker inspect" output: ${JSON.stringify(z.treeifyError(parsed.error))}`,
60+
);
61+
}
62+
const env = parsed.data[0]?.Config.Env ?? [];
63+
const imageVersion = env
64+
.find((line) => line.startsWith("LOCALSTACK_BUILD_VERSION="))
65+
?.slice("LOCALSTACK_BUILD_VERSION=".length);
66+
if (!imageVersion) {
67+
return;
68+
}
69+
return imageVersion;
70+
} catch (error) {
71+
outputChannel.error("Could not inspect LocalStack docker image");
72+
outputChannel.error(error instanceof Error ? error : String(error));
73+
return undefined;
74+
}
75+
}
76+
77+
async function pullDockerImage(
78+
outputChannel: LogOutputChannel,
79+
cancellationToken: CancellationToken,
80+
): Promise<void> {
81+
try {
82+
await spawn("docker", ["pull", LOCALSTACK_DOCKER_IMAGE_NAME], {
83+
outputChannel,
84+
outputLabel: "docker.pull",
85+
cancellationToken: cancellationToken,
86+
});
87+
} catch (error) {
88+
outputChannel.error("Could not pull LocalStack docker image");
89+
outputChannel.error(error instanceof Error ? error : String(error));
90+
}
91+
}

0 commit comments

Comments
 (0)