Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

deployImageApi implementation #229

Merged
merged 19 commits into from
Feb 1, 2023
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
18 changes: 18 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@
"NODE_DEBUG": ""
}
},
{
"name": "Launch Extension + Docker",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionDevelopmentPath=${workspaceFolder}/../vscode-docker",
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}",
"env": {
"DEBUGTELEMETRY": "v",
"NODE_DEBUG": ""
}
},
{
"name": "Launch Extension + Host",
"type": "extensionHost",
Expand Down
8 changes: 6 additions & 2 deletions src/commands/deployImage/ContainerRegistryListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ export class ContainerRegistryListStep extends AzureWizardPromptStep<IDeployImag
}

public shouldPrompt(context: IDeployImageContext): boolean {
return !context.tag;
return !context.tag && !context.image;
}

public async getSubWizard(context: IDeployImageContext): Promise<IWizardOptions<IDeployImageContext>> {
public async getSubWizard(context: IDeployImageContext): Promise<IWizardOptions<IDeployImageContext> | undefined> {
if (context.image) {
return undefined;
}

const promptSteps: AzureWizardPromptStep<IDeployImageContext>[] = [];
switch (context.registryDomain) {
case acrDomain:
Expand Down
11 changes: 8 additions & 3 deletions src/commands/deployImage/IDeployImageContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ContainerApp, EnvironmentVar } from "@azure/arm-appcontainers";
import { ContainerRegistryManagementModels } from '@azure/arm-containerregistry';
import type { ContainerApp, EnvironmentVar } from "@azure/arm-appcontainers";
import type { ContainerRegistryManagementModels } from '@azure/arm-containerregistry';
import { ISubscriptionActionContext } from '@microsoft/vscode-azext-utils';
import { SupportedRegistries } from '../../constants';

Expand All @@ -18,7 +18,12 @@ export interface IDeployImageContext extends ISubscriptionActionContext {
repositoryName?: string;
tag?: string;

// fully qualified image name that will be generated by "registy login server:tag" if left undefined
// Fully qualified image name that will be generated by "registry login server:tag" if left undefined
image?: string;
environmentVariables?: EnvironmentVar[];

// Registry credentials
registryName?: string;
username?: string;
secret?: string;
}
36 changes: 15 additions & 21 deletions src/commands/deployImage/deployImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ContainerAppsAPIClient } from "@azure/arm-appcontainers";
import type { ContainerAppsAPIClient } from "@azure/arm-appcontainers";
import { VerifyProvidersStep } from "@microsoft/vscode-azext-azureutils";
import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, ITreeItemPickerContext } from "@microsoft/vscode-azext-utils";
import { MessageItem, ProgressLocation, window } from "vscode";
import { RevisionConstants, rootFilter, webProvider } from "../../constants";
import { acrDomain, RevisionConstants, rootFilter, webProvider } from "../../constants";
import { ext } from "../../extensionVariables";
import { ContainerAppTreeItem } from "../../tree/ContainerAppTreeItem";
import { createContainerAppsAPIClient } from "../../utils/azureClients";
Expand All @@ -16,9 +16,9 @@ import { nonNullValue } from "../../utils/nonNull";
import { EnvironmentVariablesListStep } from "../createContainerApp/EnvironmentVariablesListStep";
import { getLoginServer } from "../createContainerApp/getLoginServer";
import { showContainerAppCreated } from "../createContainerApp/showContainerAppCreated";
import { listCredentialsFromRegistry } from "./acr/listCredentialsFromRegistry";
import { ContainerRegistryListStep } from "./ContainerRegistryListStep";
import { getContainerNameForImage } from "./getContainerNameForImage";
import { getAcrCredentialsAndSecrets, getThirdPartyCredentialsAndSecrets } from "./getRegistryCredentialsAndSecrets";
import { IDeployImageContext } from "./IDeployImageContext";

export async function deployImage(context: ITreeItemPickerContext & Partial<IDeployImageContext>, node?: ContainerAppTreeItem): Promise<void> {
Expand Down Expand Up @@ -58,24 +58,18 @@ export async function deployImage(context: ITreeItemPickerContext & Partial<IDep

const containerAppEnvelope = await node.getContainerEnvelopeWithSecrets(wizardContext);

// for ACR
if (wizardContext.registry) {
const registry = wizardContext.registry;
const { username, password } = await listCredentialsFromRegistry(wizardContext, registry);
const passwordName = `${wizardContext.registry.name?.toLocaleLowerCase()}-${password?.name}`;
// remove duplicate registry
containerAppEnvelope.configuration.registries = containerAppEnvelope.configuration.registries?.filter(r => r.server !== registry.loginServer);
containerAppEnvelope.configuration.registries?.push(
{
server: registry.loginServer,
username: username,
passwordSecretRef: passwordName
}
)

// remove duplicate secretRef
containerAppEnvelope.configuration.secrets = containerAppEnvelope.configuration.secrets?.filter(s => s.name !== passwordName);
containerAppEnvelope.configuration.secrets?.push({ name: passwordName, value: password.value });
if (wizardContext.registryDomain === acrDomain) {
// ACR
const acrRegistryCredentialsAndSecrets = await getAcrCredentialsAndSecrets(wizardContext, containerAppEnvelope);
containerAppEnvelope.configuration.registries = acrRegistryCredentialsAndSecrets.registries;
containerAppEnvelope.configuration.secrets = acrRegistryCredentialsAndSecrets.secrets;
} else {
// Docker Hub or other third party registry...
if (wizardContext.registryName && wizardContext.username && wizardContext.secret) {
const thirdPartyRegistryCredentialsAndSecrets = getThirdPartyCredentialsAndSecrets(wizardContext, containerAppEnvelope);
containerAppEnvelope.configuration.registries = thirdPartyRegistryCredentialsAndSecrets.registries;
containerAppEnvelope.configuration.secrets = thirdPartyRegistryCredentialsAndSecrets.secrets;
}
}

// we want to replace the old image
Expand Down
39 changes: 30 additions & 9 deletions src/commands/deployImage/deployImageApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,45 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IActionContext } from "@microsoft/vscode-azext-utils";
import { SubscriptionTreeItemBase } from "@microsoft/vscode-azext-azureutils";
import { ISubscriptionContext } from "@microsoft/vscode-azext-dev";
import { callWithMaskHandling, IActionContext, ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
import { acrDomain } from "../../constants";
import { ext } from "../../extensionVariables";
import { detectRegistryDomain, getRegistryFromAcrName } from "../../utils/imageNameUtils";
import { deployImage } from "./deployImage";
import { IDeployImageContext } from "./IDeployImageContext";

// The interface of the command options passed to the Azure Container Apps extension's deployImageToAca command
// This interface is shared with the Docker extension (https://github.com/microsoft/vscode-docker)
interface DeployImageToAcaOptionsContract {
imageName: string;
loginServer?: string;
image: string;
registryName: string;
username?: string;
secret?: string;
}

export function deployImageApi(context: IActionContext & Partial<IDeployImageContext>, deployImageOptions: DeployImageToAcaOptionsContract): Promise<void> {
// Fill in data from the options into the wizard context
context.image = deployImageOptions.imageName;
// TODO: more stuff to fill in
export async function deployImageApi(context: IActionContext & Partial<IDeployImageContext>, deployImageOptions: DeployImageToAcaOptionsContract): Promise<void> {
const subscription: ISubscriptionContext = (await ext.rgApi.appResourceTree.showTreeItemPicker<SubscriptionTreeItemBase>(SubscriptionTreeItemBase.contextValue, context)).subscription;
Object.assign(context, subscription, deployImageOptions);

// Call the deployImage function programmatically
return deployImage(context, undefined);
context.registryDomain = detectRegistryDomain(deployImageOptions.registryName);
if (context.registryDomain === acrDomain) {
context.registry = await getRegistryFromAcrName(<ISubscriptionActionContext>context, deployImageOptions.registryName);
}

// Mask sensitive data
if (deployImageOptions.secret) {
context.valuesToMask.push(deployImageOptions.secret);
}
if (deployImageOptions.username) {
context.valuesToMask.push(deployImageOptions.username);
}
context.valuesToMask.push(deployImageOptions.image);

if (deployImageOptions.secret) {
return callWithMaskHandling<void>(() => deployImage(context, undefined), deployImageOptions.secret);
} else {
return deployImage(context, undefined);
}
}
64 changes: 64 additions & 0 deletions src/commands/deployImage/getRegistryCredentialsAndSecrets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { ContainerApp, RegistryCredentials, Secret } from "@azure/arm-appcontainers";
import { nonNullProp } from "@microsoft/vscode-azext-utils";
import { dockerHubDomain, dockerHubRegistry } from "../../constants";
import { listCredentialsFromRegistry } from "./acr/listCredentialsFromRegistry";
import { IDeployImageContext } from "./IDeployImageContext";

export interface IRegistryCredentialsAndSecrets {
registries: RegistryCredentials[] | undefined;
secrets: Secret[] | undefined;
}

export async function getAcrCredentialsAndSecrets(context: IDeployImageContext, containerAppEnvelope: Required<ContainerApp>): Promise<IRegistryCredentialsAndSecrets> {
const registry = nonNullProp(context, 'registry');
const { username, password } = await listCredentialsFromRegistry(context, registry);
const passwordName = `${registry.name?.toLocaleLowerCase()}-${password?.name}`;

// Remove duplicate registries
const registries: RegistryCredentials[] | undefined = containerAppEnvelope.configuration.registries?.filter(r => r.server !== registry.loginServer);
registries?.push(
{
server: registry.loginServer,
username: username,
passwordSecretRef: passwordName
}
);

// Remove duplicate secrets
const secrets: Secret[] | undefined = containerAppEnvelope.configuration.secrets?.filter(s => s.name !== passwordName);
secrets?.push({ name: passwordName, value: password.value });

return { registries, secrets };
}

export function getThirdPartyCredentialsAndSecrets(context: IDeployImageContext, containerAppEnvelope: Required<ContainerApp>): IRegistryCredentialsAndSecrets {
// If 'docker.io', convert to 'index.docker.io', else use registryName as loginServer
const loginServer: string = (context.registryDomain === dockerHubDomain) ? dockerHubRegistry : nonNullProp(context, 'registryName').toLowerCase();
const passwordSecretRef: string = `${loginServer.replace(/[\.]+/g, '')}-${context.username}`;

// Remove duplicate registries
const registries: RegistryCredentials[] | undefined = containerAppEnvelope.configuration.registries?.filter(r => r.server !== loginServer);
registries?.push(
{
server: loginServer,
username: context.username,
passwordSecretRef
}
);

// Remove duplicate secrets
const secrets: Secret[] | undefined = containerAppEnvelope.configuration.secrets?.filter(s => s.name !== passwordSecretRef);
secrets?.push(
{
name: passwordSecretRef,
value: context.secret
}
);

return { registries, secrets };
}
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export enum ScaleRuleTypes {

export const acrDomain = 'azurecr.io';
export const dockerHubDomain = 'docker.io';
export const dockerHubRegistry = 'index.docker.io';

export type SupportedRegistries = 'azurecr.io' | 'docker.io';

Expand Down
31 changes: 31 additions & 0 deletions src/utils/imageNameUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { ContainerRegistryManagementClient, ContainerRegistryManagementModels } from "@azure/arm-containerregistry";
import { ISubscriptionActionContext } from "@microsoft/vscode-azext-utils";
import { acrDomain, dockerHubDomain, SupportedRegistries } from "../constants";
import { createContainerRegistryManagementClient } from "./azureClients";

/**
* @param registryName When parsed from a full image name, everything before the first slash
*/
export function detectRegistryDomain(registryName: string): SupportedRegistries | undefined {
if (/\.azurecr\.io$/i.test(registryName)) {
return acrDomain;
} else if (/^docker\.io$/i.test(registryName)) {
return dockerHubDomain;
} else {
return undefined;
}
}

/**
* @param acrName When parsed from a full ACR image name, everything before the first slash
*/
export async function getRegistryFromAcrName(context: ISubscriptionActionContext, acrName: string): Promise<ContainerRegistryManagementModels.Registry> {
const client: ContainerRegistryManagementClient = await createContainerRegistryManagementClient(context);
const registries = await client.registries.list();
return registries.find(r => r.loginServer === acrName) as ContainerRegistryManagementModels.Registry;
}