Skip to content

Commit 4d8419f

Browse files
t-dedahjohndownsApple
authored
Added support for bicep files in AzureResourceManagerTemplateDeploymentV3 (#15788)
* Added support for bicep files * Added tests and updated documentation * Added CSM.bicep file * Update Tasks/AzureResourceManagerTemplateDeploymentV3/task.json Co-authored-by: John Downs <john@johndowns.co.nz> * Update Tasks/AzureResourceManagerTemplateDeploymentV3/task.json Co-authored-by: John Downs <john@johndowns.co.nz> * Update Tasks/AzureResourceManagerTemplateDeploymentV3/task.json Co-authored-by: John Downs <john@johndowns.co.nz> * Update Tasks/AzureResourceManagerTemplateDeploymentV3/README.md Co-authored-by: John Downs <john@johndowns.co.nz> * Updated createOrUpdate.ts * minor changes * Testing * Removed parameter test * Version issue resolved * Updated bicepBuild * Added more tests * Added negative test * Testing output * Resolved test issue * json to bicep bug * bicep build fail test Co-authored-by: John Downs <john@johndowns.co.nz> Co-authored-by: Apple <apple@Apples-MacBook-Pro.local>
1 parent eef1507 commit 4d8419f

File tree

11 files changed

+209
-9
lines changed

11 files changed

+209
-9
lines changed

Tasks/AzureResourceManagerTemplateDeploymentV3/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The parameters of the task are described in details, including examples, to show
5252
- 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.
5353
- For other deployment scopes: Location for storing the deployment metadata.
5454

55-
* **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: <blob_storage_url>/template.json?<SAStoken>. 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).
55+
* **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: <blob_storage_url>/template.json?<SAStoken>. 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).
5656

5757
* **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.
5858
- 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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
param location string = 'eastasia'
2+
3+
var storageAccountName_var = 'deepak2121'
4+
var storageAccountType = 'Premium_LRS'
5+
6+
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-01-01' = {
7+
name: toLower(take(storageAccountName_var, 24))
8+
location: location
9+
sku: {
10+
name: storageAccountType
11+
}
12+
kind: 'StorageV2'
13+
}
14+
15+
output storageAccount_Name string = storageAccount.name
16+
output storageAccount_Location string = storageAccount.location
17+
output storageAccount_SKUName string = storageAccount.sku.name
18+
output storageAccount_Kind string = storageAccount.kind
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
param location string = 'eastasia'
2+
3+
var storageAccountName_var = 'deepak2121'
4+
var storageAccountType = 'Premium_LRS'
5+
6+
randomstring
7+
8+
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-01-01' = {
9+
name: toLower(take(storageAccountName_var, 24))
10+
location: location
11+
sku: {
12+
name: storageAccountType
13+
}
14+
kind: 'StorageV2'
15+
}
16+
17+
output storageAccount_Name string = storageAccount.name
18+
output storageAccount_Location string = storageAccount.location
19+
output storageAccount_SKUName string = storageAccount.sku.name
20+
output storageAccount_Kind string = storageAccount.kind
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
param location string = 'eastasia'
2+
param unusedParam string = 'test'
3+
4+
var storageAccountName_var = 'deepak2121'
5+
var storageAccountType = 'Premium_LRS'
6+
7+
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-01-01' = {
8+
name: toLower(take(storageAccountName_var, 24))
9+
location: location
10+
sku: {
11+
name: storageAccountType
12+
}
13+
kind: 'StorageV2'
14+
}
15+
16+
output storageAccount_Name string = storageAccount.name
17+
output storageAccount_Location string = storageAccount.location
18+
output storageAccount_SKUName string = storageAccount.sku.name
19+
output storageAccount_Kind string = storageAccount.kind

Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/L0.ts

+59
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,63 @@ describe('Azure Resource Manager Template Deployment', function () {
172172
done(error);
173173
}
174174
});
175+
176+
it('Successfully triggered createOrUpdate deployment using bicep file', (done) => {
177+
let tp = path.join(__dirname, 'createOrUpdate.js');
178+
process.env["csmFile"] = "CSMwithBicep.bicep";
179+
process.env["csmParametersFile"] = "";
180+
process.env["deploymentOutputs"] = "someVar";
181+
let tr = new ttm.MockTestRunner(tp);
182+
tr.run();
183+
try {
184+
assert(tr.succeeded, "Should have succeeded");
185+
assert(tr.stdout.indexOf("deployments.createOrUpdate is called") > 0, "deployments.createOrUpdate function should have been called from azure-sdk");
186+
assert(tr.stdout.indexOf("##vso[task.setvariable variable=someVar;]") >= 0, "deploymentsOutput should have been updated");
187+
done();
188+
}
189+
catch (error) {
190+
console.log("STDERR", tr.stderr);
191+
console.log("STDOUT", tr.stdout);
192+
done(error);
193+
}
194+
});
195+
196+
it('Successfully triggered createOrUpdate deployment using bicep file with unused params', (done) => {
197+
let tp = path.join(__dirname, 'createOrUpdate.js');
198+
process.env["csmFile"] = "CSMwithBicepWithWarning.bicep";
199+
process.env["csmParametersFile"] = "";
200+
process.env["deploymentOutputs"] = "someVar";
201+
let tr = new ttm.MockTestRunner(tp);
202+
tr.run();
203+
try {
204+
assert(tr.succeeded, "Should have succeeded");
205+
assert(tr.stdout.indexOf("deployments.createOrUpdate is called") > 0, "deployments.createOrUpdate function should have been called from azure-sdk");
206+
assert(tr.stdout.indexOf("##vso[task.setvariable variable=someVar;]") >= 0, "deploymentsOutput should have been updated");
207+
done();
208+
}
209+
catch (error) {
210+
console.log("STDERR", tr.stderr);
211+
console.log("STDOUT", tr.stdout);
212+
done(error);
213+
}
214+
});
215+
216+
it('createOrUpdate deployment should fail when bicep file contains error', (done) => {
217+
let tp = path.join(__dirname, 'createOrUpdate.js');
218+
process.env["csmFile"] = "CSMwithBicepWithError.bicep";
219+
process.env["csmParametersFile"] = "";
220+
let tr = new ttm.MockTestRunner(tp);
221+
tr.run();
222+
try {
223+
assert(!tr.succeeded, "Should have failed");
224+
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")
225+
assert(tr.stdout.indexOf("deployments.createOrUpdate is called") < 0, "deployments.createOrUpdate function should not have been called from azure-sdk");
226+
done();
227+
}
228+
catch (error) {
229+
console.log("STDERR", tr.stderr);
230+
console.log("STDOUT", tr.stdout);
231+
done(error);
232+
}
233+
});
175234
});

Tasks/AzureResourceManagerTemplateDeploymentV3/Tests/createOrUpdate.ts

+6
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,19 @@ process.env["ENDPOINT_DATA_AzureRM_ENVIRONMENTAUTHORITYURL"] = "https://login.wi
2828
process.env["ENDPOINT_DATA_AzureRM_ACTIVEDIRECTORYSERVICEENDPOINTRESOURCEID"] = "https://management.azure.com";
2929

3030
var CSMJson = path.join(__dirname, "CSM.json");
31+
var CSMBicep = path.join(__dirname, "CSMwithBicep.bicep");
32+
var CSMBicepWithWarning = path.join(__dirname, "CSMwithBicepWithWarning.bicep");
33+
var CSMBicepWithError = path.join(__dirname, "CSMwithBicepWithError.bicep");
3134
var CSMwithComments = path.join(__dirname, "CSMwithComments.json");
3235
var defaults = path.join(__dirname, "defaults.json");
3336
var faultyCSM = path.join(__dirname, "faultyCSM.json");
3437

3538
let a: ma.TaskLibAnswers = <ma.TaskLibAnswers>{
3639
"findMatch": {
3740
"CSM.json": [CSMJson],
41+
"CSMwithBicep.bicep": [CSMBicep],
42+
"CSMwithBicepWithWarning.bicep": [CSMBicepWithWarning],
43+
"CSMwithBicepWithError.bicep": [CSMBicepWithError],
3844
"CSMwithComments.json": [CSMwithComments],
3945
"defaults.json": [defaults],
4046
"faultyCSM.json": [faultyCSM],

Tasks/AzureResourceManagerTemplateDeploymentV3/operations/DeploymentScopeBase.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@ export class DeploymentScopeBase {
2121

2222
public async deploy(): Promise<void> {
2323
await this.createTemplateDeployment();
24+
utils.deleteGeneratedFiles()
2425
}
2526

2627
protected async createTemplateDeployment() {
2728
console.log(tl.loc("CreatingTemplateDeployment"));
2829
var params: DeploymentParameters;
2930
if (this.taskParameters.templateLocation === "Linked artifact") {
30-
params = utils.getDeploymentDataForLinkedArtifact(this.taskParameters);
31+
params = await utils.getDeploymentDataForLinkedArtifact(this.taskParameters);
3132
} else if (this.taskParameters.templateLocation === "URL of the file") {
3233
params = await utils.getDeploymentObjectForPublicURL(this.taskParameters);
3334
} else {

Tasks/AzureResourceManagerTemplateDeploymentV3/operations/ResourceGroup.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export class ResourceGroup extends DeploymentScopeBase {
1717
public async deploy(): Promise<void> {
1818
await this.createResourceGroupIfRequired();
1919
await this.createTemplateDeployment();
20+
utils.deleteGeneratedFiles()
2021
}
2122

2223
public deleteResourceGroup(): Promise<void> {

Tasks/AzureResourceManagerTemplateDeploymentV3/operations/Utils.ts

+67-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TemplateObject, ParameterValue } from "../models/Types";
1010
import httpInterfaces = require("typed-rest-client/Interfaces");
1111
import { DeploymentParameters } from "./DeploymentParameters";
1212

13+
var cpExec = util.promisify(require('child_process').exec);
1314
var hm = require("typed-rest-client/HttpClient");
1415
var uuid = require("uuid");
1516

@@ -33,6 +34,8 @@ function formatNumber(num: number): string {
3334
}
3435

3536
class Utils {
37+
public static cleanupFileList = []
38+
3639
public static isNonEmpty(str: string): boolean {
3740
return (!!str && !!str.trim());
3841
}
@@ -222,7 +225,7 @@ class Utils {
222225
return deploymentName;
223226
}
224227

225-
public static getDeploymentDataForLinkedArtifact(taskParameters: armDeployTaskParameters.TaskParameters): DeploymentParameters {
228+
public static async getDeploymentDataForLinkedArtifact(taskParameters: armDeployTaskParameters.TaskParameters): Promise<DeploymentParameters> {
226229
var template: TemplateObject;
227230
var fileMatches = tl.findMatch(tl.getVariable("System.DefaultWorkingDirectory"), this.escapeBlockCharacters(taskParameters.csmFile));
228231
if (fileMatches.length > 1) {
@@ -234,6 +237,7 @@ class Utils {
234237
var csmFilePath = fileMatches[0];
235238
if (!fs.lstatSync(csmFilePath).isDirectory()) {
236239
tl.debug("Loading CSM Template File.. " + csmFilePath);
240+
csmFilePath = await this.getFilePathForLinkedArtifact(csmFilePath)
237241
try {
238242
template = JSON.parse(this.stripJsonComments(fileEncoding.readFileContentsAsText(csmFilePath)));
239243
}
@@ -257,6 +261,7 @@ class Utils {
257261
var csmParametersFilePath = fileMatches[0];
258262
if (!fs.lstatSync(csmParametersFilePath).isDirectory()) {
259263
tl.debug("Loading Parameters File.. " + csmParametersFilePath);
264+
csmParametersFilePath = await this.getFilePathForLinkedArtifact(csmParametersFilePath)
260265
try {
261266
var parameterFile = JSON.parse(this.stripJsonComments(fileEncoding.readFileContentsAsText(csmParametersFilePath)));
262267
tl.debug("Loaded Parameters File");
@@ -285,6 +290,16 @@ class Utils {
285290
return deploymentParameters;
286291
}
287292

293+
public static deleteGeneratedFiles(): void{
294+
this.cleanupFileList.forEach(filePath => {
295+
try{
296+
fs.unlinkSync(filePath);
297+
}catch(err){
298+
console.log(tl.loc("BicepFileCleanupFailed", err))
299+
}
300+
});
301+
}
302+
288303
private static getPolicyHelpLink(taskParameters: armDeployTaskParameters.TaskParameters, errorDetail) {
289304
var additionalInfo = errorDetail.additionalInfo;
290305
if (!!additionalInfo) {
@@ -407,6 +422,57 @@ class Utils {
407422
private static escapeBlockCharacters(str: string): string {
408423
return str.replace(/[\[]/g, '$&[]');
409424
}
425+
426+
private static async getFilePathForLinkedArtifact(filePath: string): Promise<string> {
427+
var filePathExtension: string = filePath.split('.').pop();
428+
if(filePathExtension === 'bicep'){
429+
let azcliversion = await this.getAzureCliVersion()
430+
if(parseFloat(azcliversion)){
431+
if(this.isBicepAvailable(azcliversion)){
432+
await this.execBicepBuild(filePath)
433+
filePath = filePath.replace('.bicep', '.json')
434+
this.cleanupFileList.push(filePath)
435+
}else{
436+
throw new Error(tl.loc("IncompatibleAzureCLIVersion"));
437+
}
438+
}else{
439+
throw new Error(tl.loc("AzureCLINotFound"));
440+
}
441+
}
442+
return filePath
443+
}
444+
445+
private static async getAzureCliVersion(): Promise<string> {
446+
let azcliversion: string = "" ;
447+
const {error, stdout, stderr } = await cpExec('az version');
448+
if(error && error.code !== 0){
449+
throw new Error(tl.loc("FailedToFetchAzureCLIVersion", stderr));
450+
}else{
451+
try{
452+
azcliversion = JSON.parse(stdout)["azure-cli"]
453+
}catch(err){
454+
throw new Error(tl.loc("FailedToFetchAzureCLIVersion", err));
455+
}
456+
}
457+
return azcliversion
458+
}
459+
460+
private static async execBicepBuild(filePath): Promise<void> {
461+
const {error, stdout, stderr} = await cpExec(`az bicep build --file ${filePath}`);
462+
if(error && error.code !== 0){
463+
throw new Error(tl.loc("BicepBuildFailed", stderr));
464+
}
465+
}
466+
467+
private static isBicepAvailable(azcliversion): Boolean{
468+
let majorVersion = azcliversion.split('.')[0]
469+
let minorVersion = azcliversion.split('.')[1]
470+
// Support Bicep was introduced in az-cli 2.20.0
471+
if((majorVersion == 2 && minorVersion >= 20) || majorVersion > 2){
472+
return true
473+
}
474+
return false
475+
}
410476
}
411477

412478
export = Utils;

Tasks/AzureResourceManagerTemplateDeploymentV3/task.json

+9-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"author": "Microsoft Corporation",
1515
"version": {
1616
"Major": 3,
17-
"Minor": 198,
17+
"Minor": 199,
1818
"Patch": 0
1919
},
2020
"demands": [],
@@ -161,7 +161,7 @@
161161
"required": true,
162162
"groupName": "Template",
163163
"visibleRule": " templateLocation = Linked artifact",
164-
"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."
164+
"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"
165165
},
166166
{
167167
"name": "csmParametersFile",
@@ -170,7 +170,7 @@
170170
"defaultValue": "",
171171
"required": false,
172172
"groupName": "Template",
173-
"helpMarkDown": "Specify the path or a pattern pointing for the parameters file for the Azure Resource Manager template.",
173+
"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",
174174
"visibleRule": " templateLocation = Linked artifact"
175175
},
176176
{
@@ -309,6 +309,11 @@
309309
"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",
310310
"CompleteDeploymentModeNotSupported": "Deployment mode 'Complete' is not supported for deployment at '%s' scope",
311311
"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",
312-
"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"
312+
"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",
313+
"IncompatibleAzureCLIVersion": "Azure CLI version should be >= 2.20.0",
314+
"AzureCLINotFound": "Azure CLI not found on the agent.",
315+
"FailedToFetchAzureCLIVersion": "Failed to fetch az cli version from agent. Error: %s",
316+
"BicepBuildFailed": "\"az bicep build\" failed. Error: %s",
317+
"BicepFileCleanupFailed": "Failed to delete Bicep file. Error: %s"
313318
}
314319
}

Tasks/AzureResourceManagerTemplateDeploymentV3/task.loc.json

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"author": "Microsoft Corporation",
1515
"version": {
1616
"Major": 3,
17-
"Minor": 198,
17+
"Minor": 199,
1818
"Patch": 0
1919
},
2020
"demands": [],
@@ -309,6 +309,11 @@
309309
"ManagedServiceIdentityDetails": "ms-resource:loc.messages.ManagedServiceIdentityDetails",
310310
"CompleteDeploymentModeNotSupported": "ms-resource:loc.messages.CompleteDeploymentModeNotSupported",
311311
"TemplateValidationFailure": "ms-resource:loc.messages.TemplateValidationFailure",
312-
"TroubleshootingGuide": "ms-resource:loc.messages.TroubleshootingGuide"
312+
"TroubleshootingGuide": "ms-resource:loc.messages.TroubleshootingGuide",
313+
"IncompatibleAzureCLIVersion": "ms-resource:loc.messages.IncompatibleAzureCLIVersion",
314+
"AzureCLINotFound": "ms-resource:loc.messages.AzureCLINotFound",
315+
"FailedToFetchAzureCLIVersion": "ms-resource:loc.messages.FailedToFetchAzureCLIVersion",
316+
"BicepBuildFailed": "ms-resource:loc.messages.BicepBuildFailed",
317+
"BicepFileCleanupFailed": "ms-resource:loc.messages.BicepFileCleanupFailed"
313318
}
314319
}

0 commit comments

Comments
 (0)