Skip to content

Commit 159956c

Browse files
Make TemplateFile parameter optional when used with .bicepparam parameter file (#22728)
* Make TemplateFile parameter optional when used with .bicepparam parameter file * Address PR feedback * Address PR feedback --------- Co-authored-by: Beisi Zhou <[email protected]>
1 parent 0e49447 commit 159956c

File tree

40 files changed

+32150
-1507
lines changed

40 files changed

+32150
-1507
lines changed
Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,23 @@
1919
using Microsoft.Azure.Management.Resources;
2020
using Microsoft.Azure.Management.Resources.Models;
2121
using Microsoft.WindowsAzure.Commands.Utilities.Common;
22+
using Newtonsoft.Json;
2223
using Newtonsoft.Json.Bson;
2324
using Newtonsoft.Json.Linq;
2425

2526
using System;
2627
using System.Collections;
2728
using System.Collections.Generic;
29+
using System.IO;
2830
using System.Linq;
2931
using System.Management.Automation;
3032
using System.Net;
33+
using System.Text;
34+
using ProjectResources = Microsoft.Azure.Commands.ResourceManager.Cmdlets.Properties.Resources;
3135

3236
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation
3337
{
34-
public abstract class ResourceWithParameterCmdletBase : ResourceManagerCmdletBase
38+
public abstract class DeploymentCmdletBase : ResourceManagerCmdletBase
3539
{
3640
protected const string TemplateObjectParameterObjectParameterSetName = "ByTemplateObjectAndParameterObject";
3741
protected const string TemplateObjectParameterFileParameterSetName = "ByTemplateObjectAndParameterFile";
@@ -53,6 +57,8 @@ public abstract class ResourceWithParameterCmdletBase : ResourceManagerCmdletBas
5357
protected const string TemplateSpecResourceIdParameterFileParameterSetName = "ByTemplateSpecResourceIdAndParams";
5458
protected const string TemplateSpecResourceIdParameterUriParameterSetName = "ByTemplateSpecResourceIdAndParamsUri";
5559
protected const string TemplateSpecResourceIdParameterObjectParameterSetName = "ByTemplateSpecResourceIdAndParamsObject";
60+
61+
protected const string ByParameterFileWithNoTemplateParameterSetName = "ByParameterFileWithNoTemplate";
5662

5763
protected RuntimeDefinedParameterDictionary dynamicParameters;
5864

@@ -68,7 +74,7 @@ public abstract class ResourceWithParameterCmdletBase : ResourceManagerCmdletBas
6874

6975
private ITemplateSpecsClient templateSpecsClient;
7076

71-
protected ResourceWithParameterCmdletBase()
77+
protected DeploymentCmdletBase()
7278
{
7379
dynamicParameters = new RuntimeDefinedParameterDictionary();
7480
}
@@ -84,13 +90,15 @@ protected ResourceWithParameterCmdletBase()
8490
public Hashtable TemplateParameterObject { get; set; }
8591

8692
[Parameter(ParameterSetName = TemplateObjectParameterFileParameterSetName,
87-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "A file that has the template parameters.")]
93+
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Parameter file to use for the template.")]
8894
[Parameter(ParameterSetName = TemplateFileParameterFileParameterSetName,
89-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "A file that has the template parameters.")]
95+
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Parameter file to use for the template.")]
9096
[Parameter(ParameterSetName = TemplateUriParameterFileParameterSetName,
91-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "A file that has the template parameters.")]
97+
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Parameter file to use for the template.")]
9298
[Parameter(ParameterSetName = TemplateSpecResourceIdParameterFileParameterSetName,
93-
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "A file that has the template parameters.")]
99+
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Parameter file to use for the template.")]
100+
[Parameter(ParameterSetName = ByParameterFileWithNoTemplateParameterSetName,
101+
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Parameter file to use for the template.")]
94102
[ValidateNotNullOrEmpty]
95103
public string TemplateParameterFile { get; set; }
96104

@@ -188,20 +196,33 @@ protected override void OnBeginProcessing()
188196
{
189197
if (BicepUtility.IsBicepFile(TemplateUri))
190198
{
191-
throw new NotSupportedException($"'-TemplateUri {TemplateUri}' is not supported. Please download the bicep file and pass it using -TemplateFile.");
199+
throw new PSInvalidOperationException($"The -{nameof(TemplateUri)} parameter is not supported with .bicep files. Please download the file and pass it using -{nameof(TemplateFile)}.");
192200
}
193201

194-
if (BicepUtility.IsBicepparamFile(TemplateParameterFile) && !BicepUtility.IsBicepFile(TemplateFile))
202+
var isBicepParamFile = BicepUtility.IsBicepparamFile(TemplateParameterFile);
203+
if (!isBicepParamFile && string.IsNullOrEmpty(TemplateFile) && string.IsNullOrEmpty(TemplateUri) && string.IsNullOrEmpty(TemplateSpecId) && TemplateObject == null)
195204
{
196-
throw new NotSupportedException($"Bicepparam file {TemplateParameterFile} is only supported with a Bicep template file");
205+
throw new PSInvalidOperationException($"One of the -{nameof(TemplateFile)}, -{nameof(TemplateUri)}, -{nameof(TemplateSpecId)} or -{nameof(TemplateObject)} parameters must be supplied unless a .bicepparam file is supplied with parameter -{nameof(TemplateParameterFile)}.");
206+
}
207+
208+
if (BicepUtility.IsBicepparamFile(TemplateParameterFile) && !string.IsNullOrEmpty(TemplateFile) && !BicepUtility.IsBicepFile(TemplateFile))
209+
{
210+
throw new PSInvalidOperationException($"Parameter -{nameof(TemplateFile)} only permits .bicep files if a .bicepparam file is supplied with parameter -{nameof(TemplateParameterFile)}.");
211+
}
212+
213+
if (BicepUtility.IsBicepparamFile(TemplateParameterFile) && (!string.IsNullOrEmpty(TemplateUri) || !string.IsNullOrEmpty(TemplateSpecId) || TemplateObject != null))
214+
{
215+
throw new PSInvalidOperationException($"Parameters -{nameof(TemplateUri)}, -{nameof(TemplateSpecId)} or -{nameof(TemplateObject)} cannot be used if a .bicepparam file is supplied with parameter -{nameof(TemplateParameterFile)}.");
216+
}
217+
218+
if (BicepUtility.IsBicepparamFile(TemplateParameterFile))
219+
{
220+
BuildAndUseBicepParameters();
197221
}
198222

199223
if (BicepUtility.IsBicepFile(TemplateFile))
200224
{
201225
BuildAndUseBicepTemplate();
202-
203-
if (BicepUtility.IsBicepparamFile(TemplateParameterFile))
204-
BuildAndUseBicepParameters();
205226
}
206227

207228
if (!this.IsParameterBound(c => c.SkipTemplateParameterPrompt))
@@ -473,7 +494,64 @@ protected void BuildAndUseBicepTemplate()
473494

474495
protected void BuildAndUseBicepParameters()
475496
{
476-
TemplateParameterFile = BicepUtility.BuildParamFile(this.ResolvePath(TemplateParameterFile), this.WriteVerbose, this.WriteWarning);
497+
var output = BicepUtility.BuildParams(this.ResolvePath(TemplateParameterFile), this.WriteVerbose, this.WriteWarning);
498+
499+
TemplateParameterFile = null;
500+
TemplateParameterObject = GetParametersFromJson(output.parametersJson);
501+
502+
if (TemplateObject == null &&
503+
string.IsNullOrEmpty(TemplateFile) &&
504+
string.IsNullOrEmpty(TemplateUri) &&
505+
string.IsNullOrEmpty(TemplateSpecId))
506+
{
507+
// When .bicepparam support was first introduced, we were missing the validation to block overriding the 'using' path in these cmdlets.
508+
// We need to be careful to retain this behavior to avoid breaking existing users, until it is re-introduced intentionally with https://github.com/Azure/bicep/issues/10333.
509+
510+
if (!string.IsNullOrEmpty(output.templateJson))
511+
{
512+
TemplateObject = JsonConvert.DeserializeObject<Hashtable>(output.templateJson);
513+
}
514+
else if (!string.IsNullOrEmpty(output.templateSpecId))
515+
{
516+
TemplateSpecId = output.templateSpecId;
517+
}
518+
else
519+
{
520+
// This shouldn't happen in practice - the Bicep CLI will return either templateJson or templateSpecId (or fail to run entirely).
521+
throw new PSInvalidOperationException(string.Format(ProjectResources.InvalidFilePath, TemplateParameterFile));
522+
}
523+
}
524+
}
525+
526+
private Hashtable GetParametersFromJsonStream(Stream parametersJson)
527+
{
528+
var parameters = new Hashtable();
529+
var parametersFromJson = TemplateUtility.ParseTemplateParameterJson(parametersJson);
530+
531+
parametersFromJson.ForEach(dp =>
532+
{
533+
var parameter = new Hashtable();
534+
if (dp.Value.Value != null)
535+
{
536+
parameter.Add("value", dp.Value.Value);
537+
}
538+
if (dp.Value.Reference != null)
539+
{
540+
parameter.Add("reference", dp.Value.Reference);
541+
}
542+
543+
parameters[dp.Key] = parameter;
544+
});
545+
546+
return parameters;
547+
}
548+
549+
private Hashtable GetParametersFromJson(string parametersJson)
550+
{
551+
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(parametersJson)))
552+
{
553+
return GetParametersFromJsonStream(stream);
554+
}
477555
}
478556
}
479557
}

src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStacksCmdletBase.cs

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities;
1818
using Microsoft.WindowsAzure.Commands.Utilities.Common;
1919
using System.Collections;
20+
using System.IO;
21+
using System.Text;
2022

2123
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation
2224
{
@@ -64,39 +66,55 @@ protected string ResolveBicepFile(string TemplateFile)
6466

6567
}
6668

67-
protected string ResolveBicepParameterFile(string TemplateParameterFile)
69+
protected BicepBuildParamsStdout ResolveBicepParameterFile(string TemplateParameterFile)
6870
{
6971
if (BicepUtility.IsBicepparamFile(TemplateParameterFile))
7072
{
71-
return BicepUtility.BuildParamFile(this.ResolvePath(TemplateParameterFile), this.WriteVerbose, this.WriteWarning);
73+
return BicepUtility.BuildParams(this.ResolvePath(TemplateParameterFile), this.WriteVerbose, this.WriteWarning);
7274
}
73-
else
74-
return TemplateParameterFile;
75+
76+
return null;
7577
}
7678

77-
protected Hashtable GetParameterObject(string parameterFile)
79+
private Hashtable GetParametersFromJsonStream(Stream parametersJson)
7880
{
7981
var parameters = new Hashtable();
82+
var parametersFromJson = TemplateUtility.ParseTemplateParameterJson(parametersJson);
83+
84+
parametersFromJson.ForEach(dp =>
85+
{
86+
var parameter = new Hashtable();
87+
if (dp.Value.Value != null)
88+
{
89+
parameter.Add("value", dp.Value.Value);
90+
}
91+
if (dp.Value.Reference != null)
92+
{
93+
parameter.Add("reference", dp.Value.Reference);
94+
}
95+
96+
parameters[dp.Key] = parameter;
97+
});
98+
99+
return parameters;
100+
}
101+
102+
protected Hashtable GetParametersFromJson(string parametersJson)
103+
{
104+
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(parametersJson)))
105+
{
106+
return GetParametersFromJsonStream(stream);
107+
}
108+
}
109+
110+
protected Hashtable GetParameterObject(string parameterFile)
111+
{
80112
string templateParameterFilePath = this.ResolvePath(parameterFile);
81113
if (parameterFile != null && FileUtilities.DataStore.FileExists(templateParameterFilePath))
82114
{
83-
var parametersFromFile = TemplateUtility.ParseTemplateParameterFileContents(templateParameterFilePath);
84-
parametersFromFile.ForEach(dp =>
85-
{
86-
var parameter = new Hashtable();
87-
if (dp.Value.Value != null)
88-
{
89-
parameter.Add("value", dp.Value.Value);
90-
}
91-
if (dp.Value.Reference != null)
92-
{
93-
parameter.Add("reference", dp.Value.Reference);
94-
}
95-
96-
parameters[dp.Key] = parameter;
97-
});
115+
return GetParametersFromJsonStream(FileUtilities.DataStore.ReadFileAsStream(templateParameterFilePath));
98116
}
99-
return parameters;
117+
return new Hashtable();
100118
}
101119

102120
protected Hashtable GetTemplateParameterObject(Hashtable templateParameterObject)

src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentStacksCreateCmdletBase.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
// limitations under the License.
1313
// ----------------------------------------------------------------------------------
1414

15+
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.Utilities;
16+
using Microsoft.WindowsAzure.Commands.Utilities.Common;
1517
using System.Collections;
18+
using System.IO;
1619
using System.Management.Automation;
20+
using ProjectResources = Microsoft.Azure.Commands.ResourceManager.Cmdlets.Properties.Resources;
1721

1822
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.CmdletBase
1923
{
@@ -43,6 +47,8 @@ protected DeploymentStacksCreateCmdletBase()
4347
internal const string ParameterObjectTemplateUriParameterSetName = "ByTemplateUriWithParameterObject";
4448
internal const string ParameterObjectTemplateSpecParameterSetName = "ByTemplateSpecWithParameterObject";
4549

50+
internal const string ByParameterFileWithNoTemplateParameterSetName = "ByParameterFileWithNoTemplate";
51+
4652
[Parameter(ParameterSetName = ParameterFileTemplateFileParameterSetName,
4753
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "TemplateFile to be used to create the stack.")]
4854
[Parameter(ParameterSetName = ParameterUriTemplateFileParameterSetName,
@@ -79,6 +85,8 @@ protected DeploymentStacksCreateCmdletBase()
7985
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Parameter file to use for the template.")]
8086
[Parameter(ParameterSetName = ParameterFileTemplateSpecParameterSetName,
8187
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Parameter file to use for the template.")]
88+
[Parameter(ParameterSetName = ByParameterFileWithNoTemplateParameterSetName,
89+
Mandatory = true, ValueFromPipelineByPropertyName = true, HelpMessage = "Parameter file to use for the template.")]
8290
public string TemplateParameterFile { get; set; }
8391

8492
[Parameter(ParameterSetName = ParameterUriTemplateFileParameterSetName,
@@ -107,5 +115,73 @@ protected DeploymentStacksCreateCmdletBase()
107115
public string QueryString { get; set; }
108116

109117
#endregion
118+
119+
/// <summary>
120+
/// Compiled Template JSON. This is not directly settable via cmdlet invocation. It is instead only set if a .bicep file is compiled into JSON.
121+
/// </summary>
122+
protected string TemplateJson { get; set; }
123+
124+
private (Hashtable parameters, string templateJson, string templateSpecId) GetParametersAndMetadataFromFile()
125+
{
126+
var isBicepParamFile = BicepUtility.IsBicepparamFile(TemplateParameterFile);
127+
if (!isBicepParamFile && string.IsNullOrEmpty(TemplateFile) && string.IsNullOrEmpty(TemplateUri) && string.IsNullOrEmpty(TemplateSpecId))
128+
{
129+
throw new PSInvalidOperationException($"One of the -{nameof(TemplateFile)}, -{nameof(TemplateUri)} or -{nameof(TemplateSpecId)} parameters must be supplied unless a .bicepparam file is supplied with parameter -{nameof(TemplateParameterFile)}.");
130+
}
131+
132+
if (isBicepParamFile && !string.IsNullOrEmpty(TemplateFile) && !BicepUtility.IsBicepFile(TemplateFile))
133+
{
134+
throw new PSInvalidOperationException($"Parameter -{nameof(TemplateFile)} only permits .bicep files if a .bicepparam file is supplied with parameter -{nameof(TemplateParameterFile)}.");
135+
}
136+
137+
if (isBicepParamFile && (!string.IsNullOrEmpty(TemplateUri) || !string.IsNullOrEmpty(TemplateSpecId)))
138+
{
139+
throw new PSInvalidOperationException($"Parameters -{nameof(TemplateUri)} or -{nameof(TemplateSpecId)} cannot be used if a .bicepparam file is supplied with parameter -{nameof(TemplateParameterFile)}.");
140+
}
141+
142+
var parameterFilePath = this.TryResolvePath(TemplateParameterFile);
143+
if (!File.Exists(parameterFilePath))
144+
{
145+
throw new PSInvalidOperationException(string.Format(ProjectResources.InvalidFilePath, TemplateParameterFile));
146+
}
147+
148+
var result = ResolveBicepParameterFile(parameterFilePath);
149+
var parameters = (result != null) ?
150+
this.GetParametersFromJson(result.parametersJson) :
151+
this.GetParameterObject(parameterFilePath);
152+
var templateJson = result?.templateJson;
153+
var templateSpecId = result?.templateSpecId;
154+
155+
return (parameters, templateJson, templateSpecId);
156+
}
157+
158+
protected Hashtable ResolveParameters()
159+
{
160+
var output = GetParametersAndMetadataFromFile();
161+
162+
if (string.IsNullOrEmpty(TemplateFile) &&
163+
string.IsNullOrEmpty(TemplateUri) &&
164+
string.IsNullOrEmpty(TemplateSpecId))
165+
{
166+
// When .bicepparam support was first introduced, we were missing the validation to block overriding the 'using' path in these cmdlets.
167+
// We need to be careful to retain this behavior to avoid breaking existing users, until it is re-introduced intentionally with https://github.com/Azure/bicep/issues/10333.
168+
169+
if (!string.IsNullOrEmpty(output.templateJson))
170+
{
171+
TemplateJson = output.templateJson;
172+
}
173+
else if (!string.IsNullOrEmpty(output.templateSpecId))
174+
{
175+
TemplateSpecId = output.templateSpecId;
176+
}
177+
else
178+
{
179+
// This shouldn't happen in practice - the Bicep CLI will return either templateJson or templateSpecId (or fail to run entirely).
180+
throw new PSInvalidOperationException(string.Format(ProjectResources.InvalidFilePath, TemplateParameterFile));
181+
}
182+
}
183+
184+
return output.parameters;
185+
}
110186
}
111187
}

src/Resources/ResourceManager/Implementation/CmdletBase/DeploymentWhatIfCmdlet.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.Cmdlet
1818
using System.Management.Automation;
1919
using Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.Deployments;
2020

21-
public abstract class DeploymentWhatIfCmdlet: ResourceWithParameterCmdletBase, IDynamicParameters
21+
public abstract class DeploymentWhatIfCmdlet: DeploymentCmdletBase, IDynamicParameters
2222
{
2323
protected abstract PSDeploymentWhatIfCmdletParameters WhatIfParameters { get; }
2424

src/Resources/ResourceManager/Implementation/CmdletBase/TestDeploymentCmdletBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.CmdletBase
1010
{
11-
public abstract class TestDeploymentCmdletBase : ResourceWithParameterCmdletBase, IDynamicParameters
11+
public abstract class TestDeploymentCmdletBase : DeploymentCmdletBase, IDynamicParameters
1212
{
1313

1414
[Parameter(Mandatory = false, HelpMessage = "The query string (for example, a SAS token) to be used with the TemplateUri parameter. Would be used in case of linked templates")]

0 commit comments

Comments
 (0)