Skip to content

Commit b049377

Browse files
tiurinskyrpex
andauthored
feat: add license check to setup wizard and status bar check (#24)
Adds checking for localstack license during the setup process and as a part of `isSetupRequired` checks on startup and in status bar. Also adds proactive license activation to mitigate caching issues and unnecessary redirects when activation can be done in the background. License check and activation are performed right after authentication. Updated setup flow with license activation: ![auth_and_license_activation_in_setup_wizard](https://github.com/user-attachments/assets/25fd1190-43ff-42c8-a939-eaea10f99f8a) Note: Not adding telemetry yet as I want to clarify schema update process first - this PR adds another step in the middle of setup flow therefore step positions shift. --------- Co-authored-by: Cristian Pallarés <[email protected]>
1 parent c3b05a9 commit b049377

File tree

6 files changed

+100
-31
lines changed

6 files changed

+100
-31
lines changed

src/plugins/setup.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import os from "node:os";
2-
31
import { commands, ProgressLocation, window } from "vscode";
42

53
import { createPlugin } from "../plugins.ts";
@@ -10,6 +8,11 @@ import {
108
} from "../utils/authenticate.ts";
119
import { configureAwsProfiles } from "../utils/configure-aws.ts";
1210
import { runInstallProcess } from "../utils/install.ts";
11+
import {
12+
activateLicense,
13+
checkIsLicenseValid,
14+
activateLicenseUntilValid,
15+
} from "../utils/license.ts";
1316
import { minDelay } from "../utils/promises.ts";
1417

1518
export default createPlugin(
@@ -122,8 +125,6 @@ export default createPlugin(
122125
progress.report({
123126
message:
124127
"Waiting for authentication response from the browser...",
125-
// message: "Waiting for browser response...",
126-
// message: "Waiting for authentication response...",
127128
});
128129
const { authToken } = await minDelay(
129130
requestAuthentication(context, cancellationToken),
@@ -146,7 +147,6 @@ export default createPlugin(
146147

147148
/////////////////////////////////////////////////////////////////////
148149
progress.report({
149-
// message: "Authenticating...",
150150
message: "Authenticating to file...",
151151
});
152152
await minDelay(saveAuthToken(authToken, outputChannel));
@@ -168,6 +168,38 @@ export default createPlugin(
168168
}
169169
}
170170

171+
/////////////////////////////////////////////////////////////////////
172+
progress.report({ message: "Checking LocalStack license..." });
173+
174+
// If an auth token has just been obtained or LocalStack has never been started,
175+
// then there will be no license info to be reported by `localstack license info`.
176+
// Also, an expired license could be cached.
177+
// Activating the license pre-emptively to know its state during the setup process.
178+
const licenseIsValid = await minDelay(
179+
activateLicense(outputChannel).then(() =>
180+
checkIsLicenseValid(outputChannel),
181+
),
182+
);
183+
if (!licenseIsValid) {
184+
progress.report({
185+
message:
186+
"License is not valid or not assigned. Open License settings page to activate it.",
187+
});
188+
189+
commands.executeCommand("localstack.openLicensePage");
190+
191+
await activateLicenseUntilValid(
192+
outputChannel,
193+
cancellationToken,
194+
);
195+
}
196+
197+
if (cancellationToken.isCancellationRequested) {
198+
return;
199+
}
200+
201+
//TODO add telemetry
202+
171203
/////////////////////////////////////////////////////////////////////
172204
progress.report({
173205
message: "Configuring AWS profiles...",

src/utils/authenticate.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { createHash } from "node:crypto";
21
import * as fs from "node:fs/promises";
32
import * as os from "node:os";
43
import * as path from "node:path";
@@ -11,7 +10,6 @@ import type {
1110
import { env, Uri, window } from "vscode";
1211

1312
import { assertIsError } from "./assert.ts";
14-
import { execLocalStack } from "./cli.ts";
1513

1614
/**
1715
* Registers a {@link UriHandler} that waits for an authentication token from the browser,
@@ -114,21 +112,6 @@ export async function saveAuthToken(
114112
}
115113
}
116114

117-
const LICENSE_VALIDITY_MARKER = "license validity: valid";
118-
119-
export async function checkIsLicenseValid(outputChannel: LogOutputChannel) {
120-
try {
121-
const licenseInfoResponse = await execLocalStack(["license", "info"], {
122-
outputChannel,
123-
});
124-
return licenseInfoResponse.stdout.includes(LICENSE_VALIDITY_MARKER);
125-
} catch (error) {
126-
outputChannel.error(error instanceof Error ? error : String(error));
127-
128-
return undefined;
129-
}
130-
}
131-
132115
/**
133116
* Checks if the user is authenticated by validating the stored auth token.
134117
*

src/utils/license.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { CancellationToken, LogOutputChannel } from "vscode";
2+
3+
import { execLocalStack } from "./cli.ts";
4+
5+
const LICENSE_VALIDITY_MARKER = "license validity: valid";
6+
7+
export async function checkIsLicenseValid(outputChannel: LogOutputChannel) {
8+
try {
9+
const licenseInfoResponse = await execLocalStack(["license", "info"], {
10+
outputChannel,
11+
});
12+
return licenseInfoResponse.stdout.includes(LICENSE_VALIDITY_MARKER);
13+
} catch (error) {
14+
outputChannel.error(error instanceof Error ? error : String(error));
15+
16+
return false;
17+
}
18+
}
19+
20+
export async function activateLicense(outputChannel: LogOutputChannel) {
21+
try {
22+
await execLocalStack(["license", "activate"], {
23+
outputChannel,
24+
});
25+
} catch (error) {
26+
outputChannel.error(error instanceof Error ? error : String(error));
27+
}
28+
}
29+
30+
export async function activateLicenseUntilValid(
31+
outputChannel: LogOutputChannel,
32+
cancellationToken: CancellationToken,
33+
): Promise<void> {
34+
while (true) {
35+
if (cancellationToken.isCancellationRequested) {
36+
break;
37+
}
38+
const licenseIsValid = await checkIsLicenseValid(outputChannel);
39+
if (licenseIsValid) {
40+
break;
41+
}
42+
await activateLicense(outputChannel);
43+
// Wait before trying again
44+
await new Promise((resolve) => setTimeout(resolve, 1000));
45+
}
46+
}

src/utils/manage.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import { v7 as uuidv7 } from "uuid";
22
import type { ExtensionContext, LogOutputChannel, MessageItem } from "vscode";
33
import { commands, env, Uri, window } from "vscode";
44

5-
import { checkIsLicenseValid } from "./authenticate.ts";
65
import { spawnLocalStack } from "./cli.ts";
76
import { exec } from "./exec.ts";
7+
import { checkIsLicenseValid } from "./license.ts";
88
import type { Telemetry } from "./telemetry.ts";
99

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

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

189194
async function showInformationMessage(

src/utils/promises.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import pMinDelay from "p-min-delay";
22

33
/**
4-
* Setting up a minimum wait time of 1s allows users
4+
* Setting up a minimum wait time allows users
55
* to visually grasp the text before it goes away, if
6-
* the task was fast (less than 0.5s).
6+
* the task was faster than the minimum wait time.
77
*/
88
const MIN_TIME_BETWEEN_STEPS_MS = 500;
99

src/utils/setup.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ import type { LogOutputChannel } from "vscode";
33
import { checkIsAuthenticated } from "./authenticate.ts";
44
import { checkIsProfileConfigured } from "./configure-aws.ts";
55
import { checkLocalstackInstalled } from "./install.ts";
6+
import { checkIsLicenseValid } from "./license.ts";
67

78
export async function checkIsSetupRequired(
89
outputChannel: LogOutputChannel,
910
): Promise<boolean> {
10-
const [isInstalled, isAuthenticated, isProfileConfigured] = await Promise.all(
11-
[
11+
const [isInstalled, isAuthenticated, isLicenseValid, isProfileConfigured] =
12+
await Promise.all([
1213
checkLocalstackInstalled(outputChannel),
1314
checkIsAuthenticated(),
15+
checkIsLicenseValid(outputChannel),
1416
checkIsProfileConfigured(),
15-
],
16-
);
17+
]);
1718

18-
return !isInstalled || !isAuthenticated || !isProfileConfigured;
19+
return (
20+
!isInstalled || !isAuthenticated || !isLicenseValid || !isProfileConfigured
21+
);
1922
}

0 commit comments

Comments
 (0)