Skip to content

Commit 7c29db3

Browse files
authored
Support for adding secrets (#410)
1 parent a6fcf7f commit 7c29db3

File tree

11 files changed

+274
-0
lines changed

11 files changed

+274
-0
lines changed

extension.bundle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ export * from '@microsoft/vscode-azext-utils';
1818
// Export activate/deactivate for main.js
1919
export { activate, deactivate } from './src/extension';
2020
export * from './src/extensionVariables';
21+
export * from './src/utils/validateUtils';
2122

2223
// NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen

package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@
111111
"title": "%containerApps.editTargetPort%",
112112
"category": "Azure Container Apps"
113113
},
114+
{
115+
"command": "containerApps.addSecret",
116+
"title": "%containerApps.addSecret%",
117+
"category": "Azure Container Apps"
118+
},
114119
{
115120
"command": "containerApps.createRevisionDraft",
116121
"title": "%containerApps.createRevisionDraft%",
@@ -346,6 +351,11 @@
346351
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && (viewItem =~ /ingressEnabledItem/i || viewItem =~ /targetPortItem/i)",
347352
"group": "1@3"
348353
},
354+
{
355+
"command": "containerApps.addSecret",
356+
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /secretsItem/i",
357+
"group": "1@1"
358+
},
349359
{
350360
"command": "containerApps.openGitHubRepo",
351361
"when": "view =~ /(azureResourceGroups|azureFocusView)/ && viewItem =~ /actionsConnected:true(.*)containerAppsActionsItem/i",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"containerApps.enableIngress": "Enable Ingress for Container App...",
1515
"containerApps.toggleVisibility": "Switch Ingress Visibility...",
1616
"containerApps.editTargetPort": "Edit Target Port...",
17+
"containerApps.addSecret": "Add Secret...",
1718
"containerApps.chooseRevisionMode": "Choose Revision Mode...",
1819
"containerApps.createRevisionDraft": "Create Draft...",
1920
"containerApps.editRevisionDraft": "Edit Draft (Advanced)...",

src/commands/registerCommands.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { discardRevisionDraft } from './revisionDraft/discardRevisionDraft';
3131
import { editRevisionDraft } from './revisionDraft/editRevisionDraft';
3232
import { addScaleRule } from './scaling/addScaleRule/addScaleRule';
3333
import { editScalingRange } from './scaling/editScalingRange';
34+
import { addSecret } from './secret/addSecret/addSecret';
3435

3536
export function registerCommands(): void {
3637
// managed environments
@@ -57,6 +58,9 @@ export function registerCommands(): void {
5758
registerCommandWithTreeNodeUnwrapping('containerApps.toggleVisibility', toggleIngressVisibility);
5859
registerCommandWithTreeNodeUnwrapping('containerApps.editTargetPort', editTargetPort);
5960

61+
// secret
62+
registerCommandWithTreeNodeUnwrapping('containerApps.addSecret', addSecret);
63+
6064
// revisions
6165
registerCommandWithTreeNodeUnwrapping('containerApps.chooseRevisionMode', chooseRevisionMode);
6266
registerCommandWithTreeNodeUnwrapping('containerApps.activateRevision', activateRevision);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { Secret } from "@azure/arm-appcontainers";
7+
import type { ExecuteActivityContext } from "@microsoft/vscode-azext-utils";
8+
import type { IContainerAppContext } from "../IContainerAppContext";
9+
10+
export interface ISecretContext extends IContainerAppContext, ExecuteActivityContext {
11+
newSecretName?: string;
12+
newSecretValue?: string;
13+
14+
secret?: Secret;
15+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils";
7+
import type { Progress } from "vscode";
8+
import { ext } from "../../../extensionVariables";
9+
import { ContainerAppModel, getContainerEnvelopeWithSecrets } from "../../../tree/ContainerAppItem";
10+
import { localize } from "../../../utils/localize";
11+
import { updateContainerApp } from "../../../utils/updateContainerApp";
12+
import type { ISecretContext } from "../ISecretContext";
13+
14+
export class SecretCreateStep extends AzureWizardExecuteStep<ISecretContext> {
15+
public priority: number = 200;
16+
17+
public async execute(context: ISecretContext, progress: Progress<{ message?: string | undefined; increment?: number | undefined }>): Promise<void> {
18+
const containerApp: ContainerAppModel = nonNullProp(context, 'containerApp');
19+
const containerAppEnvelope = await getContainerEnvelopeWithSecrets(context, context.subscription, containerApp);
20+
21+
containerAppEnvelope.configuration.secrets ||= [];
22+
containerAppEnvelope.configuration.secrets.push({
23+
name: context.newSecretName,
24+
value: context.newSecretValue
25+
});
26+
27+
const addSecret: string = localize('addSecret', 'Add secret "{0}" to container app "{1}"', context.newSecretName, containerApp.name);
28+
const creatingSecret: string = localize('creatingSecret', 'Creating secret...');
29+
30+
context.activityTitle = addSecret;
31+
progress.report({ message: creatingSecret });
32+
33+
await updateContainerApp(context, context.subscription, containerAppEnvelope);
34+
35+
const addedSecret: string = localize('addedSecret', 'Added secret "{0}" to container app "{1}"', context.newSecretName, containerApp.name);
36+
ext.outputChannel.appendLog(addedSecret);
37+
}
38+
39+
public shouldExecute(context: ISecretContext): boolean {
40+
return !!context.newSecretName && !!context.newSecretValue;
41+
}
42+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { Secret } from "@azure/arm-appcontainers";
7+
import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils";
8+
import { localize } from "../../../utils/localize";
9+
import { validateUtils } from "../../../utils/validateUtils";
10+
import type { ISecretContext } from "../ISecretContext";
11+
12+
export class SecretNameStep extends AzureWizardPromptStep<ISecretContext> {
13+
public async prompt(context: ISecretContext): Promise<void> {
14+
context.newSecretName = (await context.ui.showInputBox({
15+
prompt: localize('secretName', 'Enter a secret name.'),
16+
validateInput: (val: string | undefined) => this.validateInput(context, val),
17+
})).trim();
18+
context.valuesToMask.push(context.newSecretName);
19+
}
20+
21+
public shouldPrompt(context: ISecretContext): boolean {
22+
return !context.newSecretName;
23+
}
24+
25+
private validateInput(context: ISecretContext, val: string | undefined): string | undefined {
26+
const value: string = val ? val.trim() : '';
27+
28+
if (!validateUtils.isValidLength(value)) {
29+
return validateUtils.getInvalidLengthMessage();
30+
}
31+
32+
const allowedSymbols: string = '-.';
33+
if (!validateUtils.isLowerCaseAlphanumericWithSymbols(value, allowedSymbols)) {
34+
return validateUtils.getInvalidLowerCaseAlphanumericWithSymbolsMessage(allowedSymbols);
35+
}
36+
37+
const secrets: Secret[] = context.containerApp?.configuration?.secrets ?? [];
38+
if (secrets.some((secret) => secret.name?.trim().toLocaleLowerCase() === value.toLocaleLowerCase())) {
39+
return localize('secretAlreadyExists', 'Secret with name "{0}" already exists.', value);
40+
}
41+
42+
return undefined;
43+
}
44+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils";
7+
import { localize } from "../../../utils/localize";
8+
import { validateUtils } from "../../../utils/validateUtils";
9+
import type { ISecretContext } from "../ISecretContext";
10+
11+
export class SecretValueStep extends AzureWizardPromptStep<ISecretContext> {
12+
public async prompt(context: ISecretContext): Promise<void> {
13+
context.newSecretValue = await context.ui.showInputBox({
14+
prompt: localize('secretValue', 'Enter a secret value.'),
15+
password: true,
16+
validateInput: this.validateInput
17+
});
18+
context.valuesToMask.push(context.newSecretValue);
19+
}
20+
21+
public shouldPrompt(context: ISecretContext): boolean {
22+
return !context.newSecretValue;
23+
}
24+
25+
private validateInput(val: string | undefined): string | undefined {
26+
val ??= '';
27+
28+
if (!validateUtils.isValidLength(val)) {
29+
return validateUtils.getInvalidLengthMessage();
30+
}
31+
32+
return undefined;
33+
}
34+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, IActionContext, createSubscriptionContext } from "@microsoft/vscode-azext-utils";
7+
import { ext } from "../../../extensionVariables";
8+
import { SecretsItem } from "../../../tree/configurations/secrets/SecretsItem";
9+
import { createActivityContext } from "../../../utils/activityUtils";
10+
import { localize } from "../../../utils/localize";
11+
import { pickContainerApp } from "../../../utils/pickContainerApp";
12+
import type { ISecretContext } from "../ISecretContext";
13+
import { SecretCreateStep } from "./SecretCreateStep";
14+
import { SecretNameStep } from "./SecretNameStep";
15+
import { SecretValueStep } from "./SecretValueStep";
16+
17+
export async function addSecret(context: IActionContext, node?: SecretsItem): Promise<void> {
18+
const { subscription, containerApp } = node ?? await pickContainerApp(context);
19+
20+
const wizardContext: ISecretContext = {
21+
...context,
22+
...createSubscriptionContext(subscription),
23+
...(await createActivityContext()),
24+
subscription,
25+
containerApp,
26+
};
27+
28+
const promptSteps: AzureWizardPromptStep<ISecretContext>[] = [
29+
new SecretNameStep(),
30+
new SecretValueStep()
31+
];
32+
33+
const executeSteps: AzureWizardExecuteStep<ISecretContext>[] = [
34+
new SecretCreateStep()
35+
];
36+
37+
const wizard: AzureWizard<ISecretContext> = new AzureWizard(wizardContext, {
38+
title: localize('addSecret', 'Add secret to container app "{0}"', containerApp.name),
39+
promptSteps,
40+
executeSteps,
41+
showLoadingPrompt: true
42+
});
43+
44+
await wizard.prompt();
45+
46+
const parentId: string = `${containerApp.id}/${SecretsItem.idSuffix}`;
47+
await ext.state.showCreatingChild(parentId, localize('creatingSecret', 'Creating secret...'), async () => {
48+
await wizard.execute();
49+
});
50+
51+
ext.state.notifyChildrenChanged(containerApp.managedEnvironmentId);
52+
}

src/utils/validateUtils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export namespace validateUtils {
99
const thirtyTwoBitMaxSafeInteger: number = 2147483647;
1010
// Estimated using UTF-8 encoding, where a character can be up to ~4 bytes long
1111
const maxSafeCharacterLength: number = thirtyTwoBitMaxSafeInteger / 32;
12+
const allowedSymbols: string = '[-\/\\^$*+?.()|[\]{}]';
1213

1314
/**
1415
* Validates that the given input string is the appropriate length as determined by the optional lower and upper limit parameters
@@ -40,4 +41,30 @@ export namespace validateUtils {
4041
return localize('invalidBetweenInputLength', 'The input value must be between {0} and {1} characters long.', lowerLimitIncl, upperLimitIncl);
4142
}
4243
}
44+
45+
/**
46+
* Validates that the given input string consists of lower case alphanumeric characters,
47+
* starts and ends with an alphanumeric character, and does not contain any special symbols not explicitly specified
48+
*
49+
* @param value The original input string to validate
50+
* @param symbols Any custom symbols that are also allowed in the input string. Defaults to '-'.
51+
*
52+
* @example
53+
* "abcd-1234" // returns true
54+
* "-abcd-1234" // returns false
55+
*/
56+
export function isLowerCaseAlphanumericWithSymbols(value: string, symbols: string = '-'): boolean {
57+
// Search through the passed symbols and match any allowed symbols
58+
// If we find a match, escape the symbol using '\\$&'
59+
const symbolPattern: string = symbols.replace(new RegExp(allowedSymbols, 'g'), '\\$&');
60+
const pattern: RegExp = new RegExp(`^[a-z0-9](?:[a-z0-9${symbolPattern}]*[a-z0-9])?$`);
61+
return pattern.test(value);
62+
}
63+
64+
/**
65+
* @param symbols Any custom symbols that are also allowed in the input string. Defaults to '-'.
66+
*/
67+
export function getInvalidLowerCaseAlphanumericWithSymbolsMessage(symbols: string = '-'): string {
68+
return localize('invalidLowerAlphanumericWithSymbols', `A name must consist of lower case alphanumeric characters or one of the following symbols: "{0}", and must start and end with a lower case alphanumeric character.`, symbols);
69+
}
4370
}

0 commit comments

Comments
 (0)