diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/README.md b/Tasks/AzureResourceManagerTemplateDeploymentV3/README.md index c07255e9d89b..3af25178c4d2 100644 --- a/Tasks/AzureResourceManagerTemplateDeploymentV3/README.md +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/README.md @@ -52,7 +52,7 @@ The parameters of the task are described in details, including examples, to show - For Resource Group deployment scope: Location for deploying the resource group. If the resource group already exists in the subscription, then this value will be ignored. - For other deployment scopes: Location for storing the deployment metadata. - * **Template location**: The location of the Template & the Parameters JSON files. Select "Linked Artifact" if the files are part of the linked code/build artifacts. Select "URL of the file" if the JSON files are located at any publicly accessible http/https URLs. To use a file stored in a private storage account, retrieve and include the shared access signature (SAS) token in the URL of the template. Example: /template.json?. To upload a parameters file to a storage account and generate a SAS token, you could use [Azure file copy task](https://aka.ms/azurefilecopyreadme) or follow the steps using [PowerShell](https://go.microsoft.com/fwlink/?linkid=838080) or [Azure CLI](https://go.microsoft.com/fwlink/?linkid=836911). + * **Template location**: The location of the Template & the Parameters JSON files. Select "Linked Artifact" if the files are part of the linked code/build artifacts. For "Linked Artifacts", you can also specify the path to a Bicep file. Select "URL of the file" if the JSON files are located at any publicly accessible http/https URLs. To use a file stored in a private storage account, retrieve and include the shared access signature (SAS) token in the URL of the template. Example: /template.json?. To upload a parameters file to a storage account and generate a SAS token, you could use [Azure file copy task](https://aka.ms/azurefilecopyreadme) or follow the steps using [PowerShell](https://go.microsoft.com/fwlink/?linkid=838080) or [Azure CLI](https://go.microsoft.com/fwlink/?linkid=836911). * **Template and its Parameters**: The templates and the templates parameters file are the Azure templates available at [GitHub](https://github.com/Azure/azure-quickstart-templates) or in the [Azure gallery](https://azure.microsoft.com/en-in/documentation/articles/powershell-azure-resource-manager/). To get started immediately use [this](https://aka.ms/sampletemplate) template that is available on GitHub. - These files can be either be located at any publicly accessible http/https URLs or be in a checked in the Version Control or they can be part of the build itself. If the files are part of the Build, use the pre-defined [system variables](https://msdn.microsoft.com/Library/vs/alm/Build/scripts/variables) provided by the Build to specify their location. The variables to use are $(Build.Repository.LocalPath), if the templates are checked-in but are not built, or $(Agent.BuildDirectory), if the templates are built as part of the solution. Be sure to specify the full path like $(Build.Repository.LocalPath)\Azure Templates\AzureRGDeploy.json. Wildcards like \*\*\\\*.json or \*\*\\*.param.json are also supported and there needs to be only one file that matches the search pattern at the location. If more than one file matches the search pattern, then the task will error out. diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicep.bicep b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicep.bicep new file mode 100644 index 000000000000..ad2523a9f7b9 --- /dev/null +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicep.bicep @@ -0,0 +1,18 @@ +param location string = 'eastasia' + +var storageAccountName_var = 'deepak2121' +var storageAccountType = 'Premium_LRS' + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-01-01' = { + name: toLower(take(storageAccountName_var, 24)) + location: location + sku: { + name: storageAccountType + } + kind: 'StorageV2' +} + +output storageAccount_Name string = storageAccount.name +output storageAccount_Location string = storageAccount.location +output storageAccount_SKUName string = storageAccount.sku.name +output storageAccount_Kind string = storageAccount.kind \ No newline at end of file diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicepWithError.bicep b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicepWithError.bicep new file mode 100644 index 000000000000..f11f9841591b --- /dev/null +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicepWithError.bicep @@ -0,0 +1,20 @@ +param location string = 'eastasia' + +var storageAccountName_var = 'deepak2121' +var storageAccountType = 'Premium_LRS' + +randomstring + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-01-01' = { + name: toLower(take(storageAccountName_var, 24)) + location: location + sku: { + name: storageAccountType + } + kind: 'StorageV2' +} + +output storageAccount_Name string = storageAccount.name +output storageAccount_Location string = storageAccount.location +output storageAccount_SKUName string = storageAccount.sku.name +output storageAccount_Kind string = storageAccount.kind \ No newline at end of file diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicepWithWarning.bicep b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicepWithWarning.bicep new file mode 100644 index 000000000000..ee96f96d64bf --- /dev/null +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/CSMwithBicepWithWarning.bicep @@ -0,0 +1,19 @@ +param location string = 'eastasia' +param unusedParam string = 'test' + +var storageAccountName_var = 'deepak2121' +var storageAccountType = 'Premium_LRS' + +resource storageAccount 'Microsoft.Storage/storageAccounts@2021-01-01' = { + name: toLower(take(storageAccountName_var, 24)) + location: location + sku: { + name: storageAccountType + } + kind: 'StorageV2' +} + +output storageAccount_Name string = storageAccount.name +output storageAccount_Location string = storageAccount.location +output storageAccount_SKUName string = storageAccount.sku.name +output storageAccount_Kind string = storageAccount.kind \ No newline at end of file diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/L0.ts b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/L0.ts index a47595a0fc2c..98eefaeb0d00 100644 --- a/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/L0.ts +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/L0.ts @@ -172,4 +172,63 @@ describe('Azure Resource Manager Template Deployment', function () { done(error); } }); + + it('Successfully triggered createOrUpdate deployment using bicep file', (done) => { + let tp = path.join(__dirname, 'createOrUpdate.js'); + process.env["csmFile"] = "CSMwithBicep.bicep"; + process.env["csmParametersFile"] = ""; + process.env["deploymentOutputs"] = "someVar"; + let tr = new ttm.MockTestRunner(tp); + tr.run(); + try { + assert(tr.succeeded, "Should have succeeded"); + assert(tr.stdout.indexOf("deployments.createOrUpdate is called") > 0, "deployments.createOrUpdate function should have been called from azure-sdk"); + assert(tr.stdout.indexOf("##vso[task.setvariable variable=someVar;]") >= 0, "deploymentsOutput should have been updated"); + done(); + } + catch (error) { + console.log("STDERR", tr.stderr); + console.log("STDOUT", tr.stdout); + done(error); + } + }); + + it('Successfully triggered createOrUpdate deployment using bicep file with unused params', (done) => { + let tp = path.join(__dirname, 'createOrUpdate.js'); + process.env["csmFile"] = "CSMwithBicepWithWarning.bicep"; + process.env["csmParametersFile"] = ""; + process.env["deploymentOutputs"] = "someVar"; + let tr = new ttm.MockTestRunner(tp); + tr.run(); + try { + assert(tr.succeeded, "Should have succeeded"); + assert(tr.stdout.indexOf("deployments.createOrUpdate is called") > 0, "deployments.createOrUpdate function should have been called from azure-sdk"); + assert(tr.stdout.indexOf("##vso[task.setvariable variable=someVar;]") >= 0, "deploymentsOutput should have been updated"); + done(); + } + catch (error) { + console.log("STDERR", tr.stderr); + console.log("STDOUT", tr.stdout); + done(error); + } + }); + + it('createOrUpdate deployment should fail when bicep file contains error', (done) => { + let tp = path.join(__dirname, 'createOrUpdate.js'); + process.env["csmFile"] = "CSMwithBicepWithError.bicep"; + process.env["csmParametersFile"] = ""; + let tr = new ttm.MockTestRunner(tp); + tr.run(); + try { + assert(!tr.succeeded, "Should have failed"); + assert(tr.stdout.indexOf("This declaration type is not recognized. Specify a parameter, variable, resource, or output declaration.") > 0, "should have printed the error message") + assert(tr.stdout.indexOf("deployments.createOrUpdate is called") < 0, "deployments.createOrUpdate function should not have been called from azure-sdk"); + done(); + } + catch (error) { + console.log("STDERR", tr.stderr); + console.log("STDOUT", tr.stdout); + done(error); + } + }); }); diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/createOrUpdate.ts b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/createOrUpdate.ts index b2f153b8a041..19a639387daa 100644 --- a/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/createOrUpdate.ts +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/createOrUpdate.ts @@ -28,6 +28,9 @@ process.env["ENDPOINT_DATA_AzureRM_ENVIRONMENTAUTHORITYURL"] = "https://login.wi process.env["ENDPOINT_DATA_AzureRM_ACTIVEDIRECTORYSERVICEENDPOINTRESOURCEID"] = "https://management.azure.com"; var CSMJson = path.join(__dirname, "CSM.json"); +var CSMBicep = path.join(__dirname, "CSMwithBicep.bicep"); +var CSMBicepWithWarning = path.join(__dirname, "CSMwithBicepWithWarning.bicep"); +var CSMBicepWithError = path.join(__dirname, "CSMwithBicepWithError.bicep"); var CSMwithComments = path.join(__dirname, "CSMwithComments.json"); var defaults = path.join(__dirname, "defaults.json"); var faultyCSM = path.join(__dirname, "faultyCSM.json"); @@ -35,6 +38,9 @@ var faultyCSM = path.join(__dirname, "faultyCSM.json"); let a: ma.TaskLibAnswers = { "findMatch": { "CSM.json": [CSMJson], + "CSMwithBicep.bicep": [CSMBicep], + "CSMwithBicepWithWarning.bicep": [CSMBicepWithWarning], + "CSMwithBicepWithError.bicep": [CSMBicepWithError], "CSMwithComments.json": [CSMwithComments], "defaults.json": [defaults], "faultyCSM.json": [faultyCSM], diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/DeploymentScopeBase.ts b/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/DeploymentScopeBase.ts index f654aebfc49d..da9a02b6021d 100644 --- a/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/DeploymentScopeBase.ts +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/DeploymentScopeBase.ts @@ -21,13 +21,14 @@ export class DeploymentScopeBase { public async deploy(): Promise { await this.createTemplateDeployment(); + utils.deleteGeneratedFiles() } protected async createTemplateDeployment() { console.log(tl.loc("CreatingTemplateDeployment")); var params: DeploymentParameters; if (this.taskParameters.templateLocation === "Linked artifact") { - params = utils.getDeploymentDataForLinkedArtifact(this.taskParameters); + params = await utils.getDeploymentDataForLinkedArtifact(this.taskParameters); } else if (this.taskParameters.templateLocation === "URL of the file") { params = await utils.getDeploymentObjectForPublicURL(this.taskParameters); } else { diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/ResourceGroup.ts b/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/ResourceGroup.ts index ece98e6fe41d..c45be0d71766 100644 --- a/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/ResourceGroup.ts +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/ResourceGroup.ts @@ -17,6 +17,7 @@ export class ResourceGroup extends DeploymentScopeBase { public async deploy(): Promise { await this.createResourceGroupIfRequired(); await this.createTemplateDeployment(); + utils.deleteGeneratedFiles() } public deleteResourceGroup(): Promise { diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/Utils.ts b/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/Utils.ts index 9ce8c52da667..b8203f12097f 100644 --- a/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/Utils.ts +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/operations/Utils.ts @@ -10,6 +10,7 @@ import { TemplateObject, ParameterValue } from "../models/Types"; import httpInterfaces = require("typed-rest-client/Interfaces"); import { DeploymentParameters } from "./DeploymentParameters"; +var cpExec = util.promisify(require('child_process').exec); var hm = require("typed-rest-client/HttpClient"); var uuid = require("uuid"); @@ -33,6 +34,8 @@ function formatNumber(num: number): string { } class Utils { + public static cleanupFileList = [] + public static isNonEmpty(str: string): boolean { return (!!str && !!str.trim()); } @@ -222,7 +225,7 @@ class Utils { return deploymentName; } - public static getDeploymentDataForLinkedArtifact(taskParameters: armDeployTaskParameters.TaskParameters): DeploymentParameters { + public static async getDeploymentDataForLinkedArtifact(taskParameters: armDeployTaskParameters.TaskParameters): Promise { var template: TemplateObject; var fileMatches = tl.findMatch(tl.getVariable("System.DefaultWorkingDirectory"), this.escapeBlockCharacters(taskParameters.csmFile)); if (fileMatches.length > 1) { @@ -234,6 +237,7 @@ class Utils { var csmFilePath = fileMatches[0]; if (!fs.lstatSync(csmFilePath).isDirectory()) { tl.debug("Loading CSM Template File.. " + csmFilePath); + csmFilePath = await this.getFilePathForLinkedArtifact(csmFilePath) try { template = JSON.parse(this.stripJsonComments(fileEncoding.readFileContentsAsText(csmFilePath))); } @@ -257,6 +261,7 @@ class Utils { var csmParametersFilePath = fileMatches[0]; if (!fs.lstatSync(csmParametersFilePath).isDirectory()) { tl.debug("Loading Parameters File.. " + csmParametersFilePath); + csmParametersFilePath = await this.getFilePathForLinkedArtifact(csmParametersFilePath) try { var parameterFile = JSON.parse(this.stripJsonComments(fileEncoding.readFileContentsAsText(csmParametersFilePath))); tl.debug("Loaded Parameters File"); @@ -285,6 +290,16 @@ class Utils { return deploymentParameters; } + public static deleteGeneratedFiles(): void{ + this.cleanupFileList.forEach(filePath => { + try{ + fs.unlinkSync(filePath); + }catch(err){ + console.log(tl.loc("BicepFileCleanupFailed", err)) + } + }); + } + private static getPolicyHelpLink(taskParameters: armDeployTaskParameters.TaskParameters, errorDetail) { var additionalInfo = errorDetail.additionalInfo; if (!!additionalInfo) { @@ -407,6 +422,57 @@ class Utils { private static escapeBlockCharacters(str: string): string { return str.replace(/[\[]/g, '$&[]'); } + + private static async getFilePathForLinkedArtifact(filePath: string): Promise { + var filePathExtension: string = filePath.split('.').pop(); + if(filePathExtension === 'bicep'){ + let azcliversion = await this.getAzureCliVersion() + if(parseFloat(azcliversion)){ + if(this.isBicepAvailable(azcliversion)){ + await this.execBicepBuild(filePath) + filePath = filePath.replace('.bicep', '.json') + this.cleanupFileList.push(filePath) + }else{ + throw new Error(tl.loc("IncompatibleAzureCLIVersion")); + } + }else{ + throw new Error(tl.loc("AzureCLINotFound")); + } + } + return filePath + } + + private static async getAzureCliVersion(): Promise { + let azcliversion: string = "" ; + const {error, stdout, stderr } = await cpExec('az version'); + if(error && error.code !== 0){ + throw new Error(tl.loc("FailedToFetchAzureCLIVersion", stderr)); + }else{ + try{ + azcliversion = JSON.parse(stdout)["azure-cli"] + }catch(err){ + throw new Error(tl.loc("FailedToFetchAzureCLIVersion", err)); + } + } + return azcliversion + } + + private static async execBicepBuild(filePath): Promise { + const {error, stdout, stderr} = await cpExec(`az bicep build --file ${filePath}`); + if(error && error.code !== 0){ + throw new Error(tl.loc("BicepBuildFailed", stderr)); + } + } + + private static isBicepAvailable(azcliversion): Boolean{ + let majorVersion = azcliversion.split('.')[0] + let minorVersion = azcliversion.split('.')[1] + // Support Bicep was introduced in az-cli 2.20.0 + if((majorVersion == 2 && minorVersion >= 20) || majorVersion > 2){ + return true + } + return false + } } export = Utils; \ No newline at end of file diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/task.json b/Tasks/AzureResourceManagerTemplateDeploymentV3/task.json index 3d874338e35d..4eb9c380bb37 100644 --- a/Tasks/AzureResourceManagerTemplateDeploymentV3/task.json +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/task.json @@ -14,7 +14,7 @@ "author": "Microsoft Corporation", "version": { "Major": 3, - "Minor": 198, + "Minor": 199, "Patch": 0 }, "demands": [], @@ -161,7 +161,7 @@ "required": true, "groupName": "Template", "visibleRule": " templateLocation = Linked artifact", - "helpMarkDown": "Specify the path or a pattern pointing to the Azure Resource Manager template. For more information about the templates see https://aka.ms/azuretemplates. To get started immediately use template https://aka.ms/sampletemplate." + "helpMarkDown": "Specify the path or a pattern pointing to the Azure Resource Manager template. For more information about the templates see https://aka.ms/azuretemplates. To get started immediately use template https://aka.ms/sampletemplate. 'Linked artifact' also has support for Bicep files when the Azure CLI version > 2.20.0" }, { "name": "csmParametersFile", @@ -170,7 +170,7 @@ "defaultValue": "", "required": false, "groupName": "Template", - "helpMarkDown": "Specify the path or a pattern pointing for the parameters file for the Azure Resource Manager template.", + "helpMarkDown": "Specify the path or a pattern pointing for the parameters file for the Azure Resource Manager template. 'Linked artifact' also has support for Bicep files when the Azure CLI version > 2.20.0", "visibleRule": " templateLocation = Linked artifact" }, { @@ -309,6 +309,11 @@ "ManagedServiceIdentityDetails": "Please make sure the Managed Service Identity used for deployment is assigned the right roles for the Resource Group %s. Follow the link for more details: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/howto-assign-access-portal", "CompleteDeploymentModeNotSupported": "Deployment mode 'Complete' is not supported for deployment at '%s' scope", "TemplateValidationFailure": "Validation errors were found in the Azure Resource Manager template. This can potentially cause template deployment to fail. %s. Please follow https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/template-syntax", - "TroubleshootingGuide": "Check out the troubleshooting guide to see if your issue is addressed: https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/azure-resource-group-deployment?view=azure-devops#troubleshooting" + "TroubleshootingGuide": "Check out the troubleshooting guide to see if your issue is addressed: https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/azure-resource-group-deployment?view=azure-devops#troubleshooting", + "IncompatibleAzureCLIVersion": "Azure CLI version should be >= 2.20.0", + "AzureCLINotFound": "Azure CLI not found on the agent.", + "FailedToFetchAzureCLIVersion": "Failed to fetch az cli version from agent. Error: %s", + "BicepBuildFailed": "\"az bicep build\" failed. Error: %s", + "BicepFileCleanupFailed": "Failed to delete Bicep file. Error: %s" } } \ No newline at end of file diff --git a/Tasks/AzureResourceManagerTemplateDeploymentV3/task.loc.json b/Tasks/AzureResourceManagerTemplateDeploymentV3/task.loc.json index 055dbb205b75..bcf54b533514 100644 --- a/Tasks/AzureResourceManagerTemplateDeploymentV3/task.loc.json +++ b/Tasks/AzureResourceManagerTemplateDeploymentV3/task.loc.json @@ -14,7 +14,7 @@ "author": "Microsoft Corporation", "version": { "Major": 3, - "Minor": 198, + "Minor": 199, "Patch": 0 }, "demands": [], @@ -309,6 +309,11 @@ "ManagedServiceIdentityDetails": "ms-resource:loc.messages.ManagedServiceIdentityDetails", "CompleteDeploymentModeNotSupported": "ms-resource:loc.messages.CompleteDeploymentModeNotSupported", "TemplateValidationFailure": "ms-resource:loc.messages.TemplateValidationFailure", - "TroubleshootingGuide": "ms-resource:loc.messages.TroubleshootingGuide" + "TroubleshootingGuide": "ms-resource:loc.messages.TroubleshootingGuide", + "IncompatibleAzureCLIVersion": "ms-resource:loc.messages.IncompatibleAzureCLIVersion", + "AzureCLINotFound": "ms-resource:loc.messages.AzureCLINotFound", + "FailedToFetchAzureCLIVersion": "ms-resource:loc.messages.FailedToFetchAzureCLIVersion", + "BicepBuildFailed": "ms-resource:loc.messages.BicepBuildFailed", + "BicepFileCleanupFailed": "ms-resource:loc.messages.BicepFileCleanupFailed" } } \ No newline at end of file