Skip to content
Merged
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
42 changes: 37 additions & 5 deletions src/plugins/setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import os from "node:os";

import { commands, ProgressLocation, window } from "vscode";

import { createPlugin } from "../plugins.ts";
Expand All @@ -10,6 +8,11 @@ import {
} from "../utils/authenticate.ts";
import { configureAwsProfiles } from "../utils/configure-aws.ts";
import { runInstallProcess } from "../utils/install.ts";
import {
activateLicense,
checkIsLicenseValid,
activateLicenseUntilValid,
} from "../utils/license.ts";
import { minDelay } from "../utils/promises.ts";

export default createPlugin(
Expand Down Expand Up @@ -121,8 +124,6 @@ export default createPlugin(
progress.report({
message:
"Waiting for authentication response from the browser...",
// message: "Waiting for browser response...",
// message: "Waiting for authentication response...",
});
const { authToken } = await minDelay(
requestAuthentication(context, cancellationToken),
Expand All @@ -145,7 +146,6 @@ export default createPlugin(

/////////////////////////////////////////////////////////////////////
progress.report({
// message: "Authenticating...",
message: "Authenticating to file...",
});
await minDelay(saveAuthToken(authToken, outputChannel));
Expand All @@ -167,6 +167,38 @@ export default createPlugin(
}
}

/////////////////////////////////////////////////////////////////////
progress.report({ message: "Checking LocalStack license..." });

// If an auth token has just been obtained or LocalStack has never been started,
// then there will be no license info to be reported by `localstack license info`.
// Also, an expired license could be cached.
// Activating the license pre-emptively to know its state during the setup process.
const licenseIsValid = await minDelay(
activateLicense(outputChannel).then(() =>
checkIsLicenseValid(outputChannel),
),
);
if (!licenseIsValid) {
progress.report({
message:
"License is not valid or not assigned. Open License settings page to activate it.",
});

commands.executeCommand("localstack.openLicensePage");

await activateLicenseUntilValid(
outputChannel,
cancellationToken,
);
}

if (cancellationToken.isCancellationRequested) {
return;
}

//TODO add telemetry

/////////////////////////////////////////////////////////////////////
progress.report({
message: "Configuring AWS profiles...",
Expand Down
17 changes: 0 additions & 17 deletions src/utils/authenticate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createHash } from "node:crypto";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
Expand All @@ -11,7 +10,6 @@ import type {
import { env, Uri, window } from "vscode";

import { assertIsError } from "./assert.ts";
import { execLocalStack } from "./cli.ts";

/**
* Registers a {@link UriHandler} that waits for an authentication token from the browser,
Expand Down Expand Up @@ -114,21 +112,6 @@ export async function saveAuthToken(
}
}

const LICENSE_VALIDITY_MARKER = "license validity: valid";

export async function checkIsLicenseValid(outputChannel: LogOutputChannel) {
try {
const licenseInfoResponse = await execLocalStack(["license", "info"], {
outputChannel,
});
return licenseInfoResponse.stdout.includes(LICENSE_VALIDITY_MARKER);
} catch (error) {
outputChannel.error(error instanceof Error ? error : String(error));

return undefined;
}
}

/**
* Checks if the user is authenticated by validating the stored auth token.
*
Expand Down
46 changes: 46 additions & 0 deletions src/utils/license.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { CancellationToken, LogOutputChannel } from "vscode";

import { execLocalStack } from "./cli.ts";

const LICENSE_VALIDITY_MARKER = "license validity: valid";

export async function checkIsLicenseValid(outputChannel: LogOutputChannel) {
try {
const licenseInfoResponse = await execLocalStack(["license", "info"], {
outputChannel,
});
return licenseInfoResponse.stdout.includes(LICENSE_VALIDITY_MARKER);
} catch (error) {
outputChannel.error(error instanceof Error ? error : String(error));

return false;
}
}

export async function activateLicense(outputChannel: LogOutputChannel) {
try {
await execLocalStack(["license", "activate"], {
outputChannel,
});
} catch (error) {
outputChannel.error(error instanceof Error ? error : String(error));
}
}

export async function activateLicenseUntilValid(
outputChannel: LogOutputChannel,
cancellationToken: CancellationToken,
): Promise<void> {
while (true) {
if (cancellationToken.isCancellationRequested) {
break;
}
const licenseIsValid = await checkIsLicenseValid(outputChannel);
if (licenseIsValid) {
break;
}
await activateLicense(outputChannel);
// Wait before trying again
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
9 changes: 7 additions & 2 deletions src/utils/manage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { v7 as uuidv7 } from "uuid";
import type { ExtensionContext, LogOutputChannel, MessageItem } from "vscode";
import { commands, env, Uri, window } from "vscode";

import { checkIsLicenseValid } from "./authenticate.ts";
import { spawnLocalStack } from "./cli.ts";
import { exec } from "./exec.ts";
import { checkIsLicenseValid } from "./license.ts";
import type { Telemetry } from "./telemetry.ts";

export type LocalstackStatus = "running" | "starting" | "stopping" | "stopped";
Expand Down Expand Up @@ -183,7 +183,12 @@ export async function stopLocalStack(

export async function openLicensePage() {
const url = new URL("https://app.localstack.cloud/settings/auth-tokens");
await env.openExternal(Uri.parse(url.toString()));
const openSuccessful = await env.openExternal(Uri.parse(url.toString()));
if (!openSuccessful) {
window.showErrorMessage(
`Open LocalStack License page in browser by entering the URL manually: ${url.toString()}`,
);
}
}

async function showInformationMessage(
Expand Down
4 changes: 2 additions & 2 deletions src/utils/promises.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pMinDelay from "p-min-delay";

/**
* Setting up a minimum wait time of 1s allows users
* Setting up a minimum wait time allows users
* to visually grasp the text before it goes away, if
* the task was fast (less than 0.5s).
* the task was faster than the minimum wait time.
*/
const MIN_TIME_BETWEEN_STEPS_MS = 500;
Comment on lines -4 to 8
Copy link
Collaborator

Choose a reason for hiding this comment

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

❤️ thank you for making it make sense 😆


Expand Down
13 changes: 8 additions & 5 deletions src/utils/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import type { LogOutputChannel } from "vscode";
import { checkIsAuthenticated } from "./authenticate.ts";
import { checkIsProfileConfigured } from "./configure-aws.ts";
import { checkLocalstackInstalled } from "./install.ts";
import { checkIsLicenseValid } from "./license.ts";

export async function checkIsSetupRequired(
outputChannel: LogOutputChannel,
): Promise<boolean> {
const [isInstalled, isAuthenticated, isProfileConfigured] = await Promise.all(
[
const [isInstalled, isAuthenticated, isLicenseValid, isProfileConfigured] =
await Promise.all([
checkLocalstackInstalled(outputChannel),
checkIsAuthenticated(),
checkIsLicenseValid(outputChannel),
checkIsProfileConfigured(),
],
);
]);

return !isInstalled || !isAuthenticated || !isProfileConfigured;
return (
!isInstalled || !isAuthenticated || !isLicenseValid || !isProfileConfigured
);
}