diff --git a/src/plugins/setup.ts b/src/plugins/setup.ts index 68c6f92..6557eec 100644 --- a/src/plugins/setup.ts +++ b/src/plugins/setup.ts @@ -1,5 +1,3 @@ -import os from "node:os"; - import { commands, ProgressLocation, window } from "vscode"; import { createPlugin } from "../plugins.ts"; @@ -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( @@ -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), @@ -145,7 +146,6 @@ export default createPlugin( ///////////////////////////////////////////////////////////////////// progress.report({ - // message: "Authenticating...", message: "Authenticating to file...", }); await minDelay(saveAuthToken(authToken, outputChannel)); @@ -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...", diff --git a/src/utils/authenticate.ts b/src/utils/authenticate.ts index a53abcc..41131c6 100644 --- a/src/utils/authenticate.ts +++ b/src/utils/authenticate.ts @@ -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"; @@ -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, @@ -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. * diff --git a/src/utils/license.ts b/src/utils/license.ts new file mode 100644 index 0000000..e41350f --- /dev/null +++ b/src/utils/license.ts @@ -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 { + 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)); + } +} diff --git a/src/utils/manage.ts b/src/utils/manage.ts index 42eae83..153159f 100644 --- a/src/utils/manage.ts +++ b/src/utils/manage.ts @@ -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"; @@ -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( diff --git a/src/utils/promises.ts b/src/utils/promises.ts index cc1bf9a..157133a 100644 --- a/src/utils/promises.ts +++ b/src/utils/promises.ts @@ -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; diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 62238b0..2a46ab5 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -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 { - 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 + ); }