Initial changes to send information to deployment azure
This commit is contained in:
Родитель
623e2ed349
Коммит
5252ef66da
|
@ -783,6 +783,15 @@
|
|||
"ms-rest-azure": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"azure-arm-website": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/azure-arm-website/-/azure-arm-website-5.7.0.tgz",
|
||||
"integrity": "sha512-GnwqaelTIhv40YI3Ch8+Q272X6XXWTq99Y1aYWZb1cejSP4gjrWWeppwor4HtjlVU9i9YIvYO91TRjQt8FrHVA==",
|
||||
"requires": {
|
||||
"ms-rest": "^2.3.3",
|
||||
"ms-rest-azure": "^2.5.5"
|
||||
}
|
||||
},
|
||||
"azure-pipelines-language-server": {
|
||||
"version": "0.5.9",
|
||||
"resolved": "https://registry.npmjs.org/azure-pipelines-language-server/-/azure-pipelines-language-server-0.5.9.tgz",
|
||||
|
|
|
@ -134,6 +134,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"azure-arm-resource": "7.3.0",
|
||||
"azure-arm-website": "5.7.0",
|
||||
"azure-pipelines-language-server": "0.5.9",
|
||||
"mustache": "3.0.1",
|
||||
"q": "1.5.1",
|
||||
|
|
|
@ -1,28 +1,33 @@
|
|||
import { AzureResourceClient } from './azureResourceClient';
|
||||
import { Messages } from '../../resources/messages';
|
||||
const uuid = require('uuid/v4');
|
||||
import { ResourceListResult, GenericResource } from 'azure-arm-resource/lib/resource/models';
|
||||
import { WebSiteManagementClient } from 'azure-arm-website';
|
||||
import { SiteConfigResource, StringDictionary, Deployment } from 'azure-arm-website/lib/models';
|
||||
import { ServiceClientCredentials } from 'ms-rest';
|
||||
import { WebAppKind } from '../../model/models';
|
||||
|
||||
import { AzureResourceClient } from './azureResourceClient';
|
||||
import { WebAppKind, ParsedAzureResourceId } from '../../model/models';
|
||||
import * as Constants from '../../resources/constants';
|
||||
import {Build} from '../../model/azureDevOps';
|
||||
import {Messages} from '../../resources/messages';
|
||||
|
||||
export class AppServiceClient extends AzureResourceClient {
|
||||
|
||||
private static apiVersion = '2019-05-01';
|
||||
private static resourceType = 'Microsoft.Web/sites';
|
||||
private webSiteManagementClient: WebSiteManagementClient;
|
||||
|
||||
constructor(credentials: ServiceClientCredentials, subscriptionId: string) {
|
||||
super(credentials, subscriptionId);
|
||||
this.webSiteManagementClient = new WebSiteManagementClient(credentials, subscriptionId);
|
||||
}
|
||||
|
||||
public async getAppServiceResource(resourceId): Promise<GenericResource> {
|
||||
if (!resourceId) {
|
||||
throw new Error(Messages.resourceIdMissing);
|
||||
}
|
||||
|
||||
return await this.getResource(resourceId, AppServiceClient.apiVersion);
|
||||
public async getAppServiceResource(resourceId: string): Promise<GenericResource> {
|
||||
let parsedResourceId: ParsedAzureResourceId = new ParsedAzureResourceId(resourceId);
|
||||
return await this.webSiteManagementClient.webApps.get(parsedResourceId.resourceGroup, parsedResourceId.resourceName);
|
||||
}
|
||||
|
||||
public async GetAppServices(filterForResourceKind: WebAppKind): Promise<ResourceListResult> {
|
||||
let resourceList: ResourceListResult = await this.getResourceList(AppServiceClient.resourceType);
|
||||
|
||||
// this.webSiteManagementClient.webApps.list();
|
||||
if (!!filterForResourceKind) {
|
||||
let filteredResourceList: ResourceListResult = [];
|
||||
resourceList.forEach((resource) => {
|
||||
|
@ -36,4 +41,76 @@ export class AppServiceClient extends AzureResourceClient {
|
|||
|
||||
return resourceList;
|
||||
}
|
||||
|
||||
public async getDeploymentCenterUrl(resourceId: string): string {
|
||||
}
|
||||
|
||||
public async getVstsPipelineUrl(resourceId: string): Promise<string> {
|
||||
let metadata = await this.getSiteMetadata(resourceId);
|
||||
if (metadata.properties['VSTSRM_BuildDefinitionWebAccessUrl']) {
|
||||
return metadata.properties['VSTSRM_BuildDefinitionWebAccessUrl'];
|
||||
}
|
||||
|
||||
throw new Error(Messages.cannotFindPipelineUrlInMetaDataException);
|
||||
}
|
||||
|
||||
public async getAppServiceConfig(resourceId: string): Promise<SiteConfigResource> {
|
||||
let parsedResourceId: ParsedAzureResourceId = new ParsedAzureResourceId(resourceId);
|
||||
return this.webSiteManagementClient.webApps.getConfiguration(parsedResourceId.resourceGroup, parsedResourceId.resourceName);
|
||||
}
|
||||
|
||||
public async updateScmType(resourceId: string): Promise<SiteConfigResource> {
|
||||
let siteConfig = await this.getAppServiceConfig(resourceId);
|
||||
siteConfig.scmType = Constants.VstsRmScmType;
|
||||
let parsedResourceId: ParsedAzureResourceId = new ParsedAzureResourceId(resourceId);
|
||||
return this.webSiteManagementClient.webApps.updateConfiguration(parsedResourceId.resourceGroup, parsedResourceId.resourceName, siteConfig);
|
||||
}
|
||||
|
||||
public async updateAppServiceConfig(resourceId: string, siteConfig: SiteConfigResource): Promise<SiteConfigResource> {
|
||||
let parsedResourceId: ParsedAzureResourceId = new ParsedAzureResourceId(resourceId);
|
||||
return this.webSiteManagementClient.webApps.updateConfiguration(parsedResourceId.resourceGroup, parsedResourceId.resourceName, siteConfig);
|
||||
}
|
||||
|
||||
public async getSiteMetadata(resourceId: string): Promise<StringDictionary> {
|
||||
let parsedResourceId: ParsedAzureResourceId = new ParsedAzureResourceId(resourceId);
|
||||
return this.webSiteManagementClient.webApps.listMetadata(parsedResourceId.resourceGroup, parsedResourceId.resourceName);
|
||||
}
|
||||
|
||||
public async updateSiteMetadata(resourceId: string, metadata: StringDictionary): Promise<StringDictionary> {
|
||||
let parsedResourceId: ParsedAzureResourceId = new ParsedAzureResourceId(resourceId);
|
||||
return this.webSiteManagementClient.webApps.updateMetadata(parsedResourceId.resourceGroup, parsedResourceId.resourceName, metadata);
|
||||
}
|
||||
|
||||
public async publishDeploymentToAppService(resourceId: string, buildDefinitionUrl: string, releaseDefinitionUrl: string, triggeredBuildUrl: string): Promise<Deployment> {
|
||||
let parsedResourceId: ParsedAzureResourceId = new ParsedAzureResourceId(resourceId);
|
||||
|
||||
// create deployment object
|
||||
let deploymentId = uuid();
|
||||
let deployment = this.createDeploymentObject(deploymentId, buildDefinitionUrl, releaseDefinitionUrl, triggeredBuildUrl);
|
||||
return this.webSiteManagementClient.webApps.createDeployment(parsedResourceId.resourceGroup, parsedResourceId.resourceName, deploymentId, deployment);
|
||||
}
|
||||
|
||||
private createDeploymentObject(deploymentId: string, buildDefinitionUrl: string, releaseDefinitionUrl: string, triggeredBuildUrl: string): Deployment {
|
||||
let deployment: Deployment = {
|
||||
id: deploymentId
|
||||
};
|
||||
|
||||
let deploymentMessage: DeploymentMessage = {
|
||||
type: "CDDeploymentConfiguration",
|
||||
message: "Successfully set up continuous delivery from VS Code and triggered deployment to Azure Web App.",
|
||||
VSTSRM_BuildDefinitionWebAccessUrl: `${buildDefinitionUrl}`,
|
||||
VSTSRM_ConfiguredCDEndPoint: `${releaseDefinitionUrl}`,
|
||||
VSTSRM_BuildWebAccessUrl: `${triggeredBuildUrl}`,
|
||||
};
|
||||
deployment.message = JSON.stringify(deploymentMessage);
|
||||
return deployment;
|
||||
}
|
||||
}
|
||||
|
||||
interface DeploymentMessage {
|
||||
type: string,
|
||||
message: string,
|
||||
VSTSRM_BuildDefinitionWebAccessUrl: string,
|
||||
VSTSRM_ConfiguredCDEndPoint: string,
|
||||
VSTSRM_BuildWebAccessUrl: string
|
||||
};
|
|
@ -21,6 +21,7 @@ import { Result, telemetryHelper } from './helper/telemetryHelper';
|
|||
import { ControlProvider } from './helper/controlProvider';
|
||||
import { GitHubProvider } from './helper/gitHubHelper';
|
||||
import { getSubscriptionSession } from './helper/azureSessionHelper';
|
||||
import {Build} from './model/azureDevOps';
|
||||
|
||||
const Layer: string = 'configure';
|
||||
|
||||
|
@ -89,7 +90,7 @@ class PipelineConfigurer {
|
|||
await this.checkInPipelineFileToRepository();
|
||||
|
||||
telemetryHelper.setCurrentStep('CreateAndRunPipeline');
|
||||
let queuedPipelineUrl = await vscode.window.withProgress<string>({ location: vscode.ProgressLocation.Notification, title: Messages.configuringPipelineAndDeployment }, async () => {
|
||||
let queuedPipeline = await vscode.window.withProgress<Build>({ location: vscode.ProgressLocation.Notification, title: Messages.configuringPipelineAndDeployment }, async () => {
|
||||
try {
|
||||
let pipelineName = `${this.inputs.targetResource.resource.name}-${this.uniqueResourceNameSuffix}`;
|
||||
return await this.azureDevOpsHelper.createAndRunPipeline(pipelineName, this.inputs);
|
||||
|
@ -100,13 +101,14 @@ class PipelineConfigurer {
|
|||
}
|
||||
|
||||
});
|
||||
this.updateScmType(queuedPipeline);
|
||||
|
||||
telemetryHelper.setCurrentStep('DisplayCreatedPipeline');
|
||||
vscode.window.showInformationMessage(Messages.pipelineSetupSuccessfully, Messages.browsePipeline)
|
||||
.then((action: string) => {
|
||||
if (action && action.toLowerCase() === Messages.browsePipeline.toLowerCase()) {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.BrowsePipelineClicked, 'true');
|
||||
vscode.env.openExternal(vscode.Uri.parse(queuedPipelineUrl));
|
||||
vscode.env.openExternal(vscode.Uri.parse(queuedPipeline._links.web.href));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -342,6 +344,8 @@ class PipelineConfigurer {
|
|||
default:
|
||||
throw new Error(utils.format(Messages.resourceTypeIsNotSupported, azureResource.type));
|
||||
}
|
||||
|
||||
await this.validateIfPipelineCanBeSetupOnResource(azureResource.id);
|
||||
}
|
||||
catch (error) {
|
||||
telemetryHelper.logError(Layer, TracePoints.ExtractAzureResourceFromNodeFailed, error);
|
||||
|
@ -451,9 +455,60 @@ class PipelineConfigurer {
|
|||
.then((webApps) => webApps.map(x => { return { label: x.name, data: x }; })),
|
||||
{ placeHolder: Messages.selectWebApp },
|
||||
TelemetryKeys.WebAppListCount);
|
||||
|
||||
await this.validateIfPipelineCanBeSetupOnResource(selectedResource.data.id);
|
||||
this.inputs.targetResource.resource = selectedResource.data;
|
||||
}
|
||||
|
||||
private async validateIfPipelineCanBeSetupOnResource(resourceId: string): Promise<void> {
|
||||
// Check for SCM type, if its value is set then a pipeline is already setup.
|
||||
let siteConfig = await this.appServiceClient.getAppServiceConfig(resourceId);
|
||||
if (siteConfig.scmType && siteConfig.scmType.toLowerCase() == 'VSTSRM') {
|
||||
// if pipeline is already setup, the ask the user if we should continue.
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.PipelineAlreadyConfigured, 'true');
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.ScmType, siteConfig.scmType);
|
||||
|
||||
let browsePipeline = await this.controlProvider.showInformationBox(
|
||||
constants.PipelineAlreadyConfigure,
|
||||
Messages.pipelineAlreadyConfigured,
|
||||
constants.BrowsePipeline);
|
||||
if (browsePipeline) {
|
||||
let existingPipelineUrl = await this.appServiceClient.getVstsPipelineUrl(resourceId);
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.BrowsedExistingPipeline, 'true');
|
||||
vscode.env.openExternal(vscode.Uri.parse(existingPipelineUrl));
|
||||
throw new UserCancelledError();
|
||||
}
|
||||
}
|
||||
else if (siteConfig.scmType && siteConfig.scmType.toLowerCase() != '') {
|
||||
let result = await this.controlProvider.showInformationBox(constants.DeploymentResourceAlreadyConfigured, Messages.deploymentCenterAlreadyConfigured, constants.BrowseDeploymentSource);
|
||||
if (result == constants.BrowsePipeline) {
|
||||
let deploymentCenterUrl: string = await this.appServiceClient.getDeploymentCenterUrl(resourceId);
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.OpenedDeploymentCenter, 'true');
|
||||
await vscode.env.openExternal(vscode.Uri.parse(deploymentCenterUrl));
|
||||
throw new UserCancelledError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateScmType(queuedPipeline: Build) {
|
||||
await this.appServiceClient.updateScmType(this.inputs.targetResource.resource.id);
|
||||
|
||||
let metadata = await this.appServiceClient.getSiteMetadata(this.inputs.targetResource.resource.id);
|
||||
// insert BD, RD and other information
|
||||
metadata["properties"] = {
|
||||
"CURRENT_STACK": "dotnetcore",
|
||||
"VSTSRM_ProjectId": `${this.inputs.project.id}`,
|
||||
"VSTSRM_AccountId": `${this.inputs.organizationName}`,
|
||||
"VSTSRM_BuildDefinitionId": `${queuedPipeline.id}`,
|
||||
"VSTSRM_BuildDefinitionWebAccessUrl": `${queuedPipeline._links.web.href}`,
|
||||
"VSTSRM_ConfiguredCDEndPoint": `${queuedPipeline._links.web.href}`,
|
||||
"VSTSRM_ReleaseDefinitionId": `${queuedPipeline.id}`,
|
||||
"VSTSRM_ProdAppName": `${this.inputs.targetResource.resource.name}`,
|
||||
};
|
||||
await this.appServiceClient.updateSiteMetadata(this.inputs.targetResource.resource.id, metadata);
|
||||
await this.appServiceClient.publishDeploymentToAppService(this.inputs.targetResource.resource.id);
|
||||
}
|
||||
|
||||
private async createGithubServiceConnection(): Promise<void> {
|
||||
if (!this.serviceConnectionHelper) {
|
||||
this.serviceConnectionHelper = new ServiceConnectionHelper(this.inputs.organizationName, this.inputs.project.name, this.azureDevOpsClient);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { QuickPickItem, InputBoxOptions } from 'vscode';
|
||||
import { IAzureQuickPickOptions } from 'vscode-azureextensionui';
|
||||
import { QuickPickItem, InputBoxOptions, window, env, Uri } from 'vscode';
|
||||
import { IAzureQuickPickOptions, UserCancelledError } from 'vscode-azureextensionui';
|
||||
import { telemetryHelper } from '../helper/telemetryHelper'
|
||||
import { extensionVariables } from '../model/models';
|
||||
import { TelemetryKeys } from '../resources/telemetryKeys';
|
||||
import {Messages} from '../resources/messages';
|
||||
|
||||
export class ControlProvider {
|
||||
public async showQuickPick<T extends QuickPickItem>(listName: string, listItems: T[] | Thenable<T[]>, options: IAzureQuickPickOptions, itemCountTelemetryKey?: string): Promise<T> {
|
||||
|
@ -21,4 +22,20 @@ export class ControlProvider {
|
|||
telemetryHelper.setTelemetry(TelemetryKeys.CurrentUserInput, inputName);
|
||||
return await extensionVariables.ui.showInputBox(options);
|
||||
}
|
||||
|
||||
public async showInformationBox(informationIdentifier: string, informationMessage: string, actions?): Promise<any> {
|
||||
telemetryHelper.setTelemetry(TelemetryKeys.CurrentUserInput, informationIdentifier);
|
||||
if (!!actions && actions.length > 0) {
|
||||
let result = await window.showInformationMessage(informationMessage, actions);
|
||||
if (!result) {
|
||||
throw new UserCancelledError(Messages.userCancelledExcecption);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else {
|
||||
return await window.showInformationMessage(informationMessage, actions);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -77,12 +77,12 @@ export class AzureDevOpsHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public async createAndRunPipeline(pipelineName: string, inputs: WizardInputs): Promise<string> {
|
||||
public async createAndRunPipeline(pipelineName: string, inputs: WizardInputs): Promise<Build> {
|
||||
try {
|
||||
let buildDefinitionPayload = await this.getBuildDefinitionPayload(pipelineName, inputs);
|
||||
let definition = await this.azureDevOpsClient.createBuildDefinition(inputs.organizationName, buildDefinitionPayload);
|
||||
let build = await this.azureDevOpsClient.queueBuild(inputs.organizationName, this.getQueueBuildPayload(inputs, definition.id, definition.project.id));
|
||||
return build._links.web.href;
|
||||
return build;
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(util.format(Messages.failedToCreateAzurePipeline, error.message));
|
||||
|
|
|
@ -44,7 +44,9 @@ export interface YamlProcess {
|
|||
}
|
||||
|
||||
export interface Build {
|
||||
definition: { id: number };
|
||||
_links: { web: { href: string } };
|
||||
id: string;
|
||||
definition: { id: number, _links: { web: { href: string } } };
|
||||
project: { id: string };
|
||||
sourceBranch: string;
|
||||
sourceVersion: string;
|
||||
|
|
|
@ -4,6 +4,7 @@ import { OutputChannel, ExtensionContext, QuickPickItem } from 'vscode';
|
|||
import { ServiceClientCredentials } from 'ms-rest';
|
||||
import { SubscriptionModels } from 'azure-arm-resource';
|
||||
import { UIExtensionVariables, IAzureUserInput, ITelemetryReporter } from 'vscode-azureextensionui';
|
||||
import { Messages } from '../resources/messages';
|
||||
|
||||
class ExtensionVariables implements UIExtensionVariables {
|
||||
public azureAccountExtensionApi: AzureAccountExtensionExports;
|
||||
|
@ -121,6 +122,58 @@ export class QuickPickItemWithData implements QuickPickItem {
|
|||
detail?: string;
|
||||
}
|
||||
|
||||
export class ParsedAzureResourceId {
|
||||
public resourceId: string;
|
||||
public subscription: string;
|
||||
public resourceGroup: string;
|
||||
public resourceType: string;
|
||||
public resourceProvider: string;
|
||||
public resourceName: string;
|
||||
public childResourceType: string;
|
||||
public childResource: string;
|
||||
|
||||
constructor(resourceId: string) {
|
||||
if (!resourceId) {
|
||||
throw Messages.invalidAzureResourceId;
|
||||
}
|
||||
|
||||
this.resourceId = resourceId;
|
||||
this.parseId();
|
||||
}
|
||||
|
||||
private parseId() {
|
||||
// remove all empty parts in the resource to avoid failing in case there are leading/trailing/extra '/'
|
||||
let parts = this.resourceId.split('/').filter((part) => !!part);
|
||||
if (!!parts) {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
switch (i) {
|
||||
case 1:
|
||||
this.subscription = parts[1];
|
||||
break;
|
||||
case 3:
|
||||
this.resourceGroup = parts[3];
|
||||
break;
|
||||
case 5:
|
||||
this.resourceProvider = parts[5];
|
||||
break;
|
||||
case 6:
|
||||
this.resourceType = parts[6];
|
||||
break;
|
||||
case 7:
|
||||
this.resourceName = parts[7];
|
||||
break;
|
||||
case 8:
|
||||
this.childResourceType = parts[8];
|
||||
break;
|
||||
case 9:
|
||||
this.childResource = parts[9];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface Token {
|
||||
session: AzureSession;
|
||||
accessToken: string;
|
||||
|
|
|
@ -142,4 +142,9 @@ export const SelectSubscription = 'selectSubscription';
|
|||
export const SelectWebApp = 'selectWebApp';
|
||||
export const GitHubPat = 'gitHubPat';
|
||||
export const SelectFromMultipleWorkSpace = 'selectFromMultipleWorkSpace';
|
||||
export const SelectRemoteForRepo = 'selectRemoteForRepo';
|
||||
export const SelectRemoteForRepo = 'selectRemoteForRepo';
|
||||
export const VstsRmScmType = 'VSTSRM';
|
||||
export const DeploymentResourceAlreadyConfigured = 'DeploymentResourceAlreadyConfigured';
|
||||
export const PipelineAlreadyConfigure = 'PipelineAlreadyConfigure';
|
||||
export const BrowseDeploymentSource = 'BrowseDeploymentSource';
|
||||
export const BrowsePipeline = 'BrowsePipeline';
|
|
@ -52,4 +52,9 @@ export class Messages {
|
|||
public static azureServicePrincipalFailedMessage: string =`Failed while creating Azure service principal.`;
|
||||
public static roleAssignmentFailedMessage: string =`Failed while role assignement.`;
|
||||
public static waitForAzureSignIn: string =`Waiting for Azure sign-in...`;
|
||||
public static invalidAzureResourceId: string = 'Azure Resource Id: %s, could not be parsed correctly. Kindly verify if the Id is correct.'
|
||||
public static userCancelledExcecption = 'User cancelled the action';
|
||||
public static deploymentCenterAlreadyConfigured = 'Deployment source of type: %s is already setup for your web app, you can view more by browsing to deployment center.';
|
||||
public static pipelineAlreadyConfigured = 'Pipeline is already setup for your web app.';
|
||||
public static cannotFindPipelineUrlInMetaDataException = 'We were unable to find pipeline url for the app service. This can be caused due to corrupt/invalida metadata of site. You can open deployment center for more information.'
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ export class TelemetryKeys {
|
|||
public static BrowsePipelineClicked: string = 'browsePipelineClicked';
|
||||
public static MultipleWorkspaceFolders: string = 'multipleWorkspaceFolders';
|
||||
public static GitFolderExists: string = 'gitFolderExists';
|
||||
public static PipelineAlreadyConfigured: string = 'pipelineAlreadyConfigured';
|
||||
public static ScmType: string = 'scmType';
|
||||
public static OpenedDeploymentCenter = 'openedDeploymentCenter';
|
||||
public static BrowsedExistingPipeline = 'browsedExistingPipeline';
|
||||
|
||||
// Durations
|
||||
public static ExtensionActivationDuration = 'extensionActivationDuration';
|
||||
|
|
Загрузка…
Ссылка в новой задаче