Skip to content

Commit 1f1f77a

Browse files
authored
Merge pull request #873 from TylerLeonhardt/add-azure-powershell
Inital Azure Powershell support
2 parents f4a270d + c543d5e commit 1f1f77a

File tree

3 files changed

+137
-7
lines changed

3 files changed

+137
-7
lines changed

build.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ dotnet build Azure.Functions.Cli.sln
33

44
$outDir = "$([System.IO.Path]::GetTempPath())cli"
55

6-
Remove-Item -Recurse -Force $outDir
6+
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $outDir
77

88
if ($IsMacOS) { $runtime = 'osx-x64' }
99
elseif ($IsLinux) { $runtime = 'linux-x64' }

src/Azure.Functions.Cli/Actions/AzureActions/BaseAzureAction.cs

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
using System.Threading.Tasks;
66
using Azure.Functions.Cli.Arm;
77
using Azure.Functions.Cli.Common;
8+
using static Azure.Functions.Cli.Common.OutputTheme;
89
using Azure.Functions.Cli.Interfaces;
10+
using Colors.Net;
911
using Fclp;
1012
using Newtonsoft.Json;
1113
using Newtonsoft.Json.Linq;
@@ -14,6 +16,18 @@ namespace Azure.Functions.Cli.Actions.AzureActions
1416
{
1517
abstract class BaseAzureAction : BaseAction, IInitializableAction
1618
{
19+
// Az is the Azure PowerShell module that works in both PowerShell Core and Windows PowerShell
20+
private const string _azProfileModuleName = "Az.Profile";
21+
22+
// AzureRm is the Azure PowerShell module that only works on Windows PowerShell
23+
private const string _azureRmProfileModuleName = "AzureRM.Profile";
24+
25+
// PowerShell Core is version 6.0 and higher that is cross-platform
26+
private const string _powerShellCoreExecutable = "pwsh";
27+
28+
// Windows PowerShell is PowerShell version 5.1 and lower that only works on Windows
29+
private const string _windowsPowerShellExecutable = "powershell";
30+
1731
public string AccessToken { get; set; }
1832
public bool ReadStdin { get; set; }
1933

@@ -66,10 +80,16 @@ public async Task Initialize()
6680

6781
private async Task<string> GetAccessToken()
6882
{
69-
return await AzureCliGetToken();
83+
(bool cliSucceeded, string cliToken) = await TryGetAzCliToken();
84+
if (cliSucceeded) return cliToken;
85+
86+
(bool powershellSucceeded, string psToken) = await TryGetAzPowerShellToken();
87+
if (powershellSucceeded) return psToken;
88+
89+
throw new CliException("Unable to connect to Azure. Make sure you have the `az` CLI or Azure PowerShell installed and logged in and try again");
7090
}
7191

72-
private async Task<string> AzureCliGetToken()
92+
private async Task<(bool succeeded, string token)> TryGetAzCliToken()
7393
{
7494
if (CommandChecker.CommandExists("az"))
7595
{
@@ -80,19 +100,115 @@ private async Task<string> AzureCliGetToken()
80100
var stdout = new StringBuilder();
81101
var stderr = new StringBuilder();
82102
var exitCode = await az.RunAsync(o => stdout.AppendLine(o), e => stderr.AppendLine(e));
83-
if (exitCode != 0)
103+
if (exitCode == 0)
104+
{
105+
return (true, stdout.ToString().Trim(' ', '\n', '\r', '"'));
106+
}
107+
else
108+
{
109+
if (StaticSettings.IsDebug)
110+
{
111+
ColoredConsole.WriteLine(VerboseColor($"Unable to fetch access token from az cli. Error: {stderr.ToString().Trim(' ', '\n', '\r')}"));
112+
}
113+
}
114+
}
115+
return (false, null);
116+
}
117+
118+
private async Task<(bool succeeded, string token)> TryGetAzPowerShellToken()
119+
{
120+
// PowerShell Core can only use Az so we can check that it exists and that the Az module exists
121+
if (CommandChecker.CommandExists(_powerShellCoreExecutable) &&
122+
await CommandChecker.PowerShellModuleExistsAsync(_powerShellCoreExecutable, _azProfileModuleName))
123+
{
124+
var az = new Executable(_powerShellCoreExecutable,
125+
$"-NonInteractive -o Text -NoProfile -c {GetPowerShellAccessTokenScript(_azProfileModuleName)}");
126+
127+
var stdout = new StringBuilder();
128+
var stderr = new StringBuilder();
129+
var exitCode = await az.RunAsync(o => stdout.AppendLine(o), e => stderr.AppendLine(e));
130+
if (exitCode == 0)
131+
{
132+
return (true, stdout.ToString().Trim(' ', '\n', '\r', '"'));
133+
}
134+
else
135+
{
136+
if (StaticSettings.IsDebug)
137+
{
138+
ColoredConsole.WriteLine(VerboseColor($"Unable to fetch access token from Az.Profile in PowerShell Core. Error: {stderr.ToString().Trim(' ', '\n', '\r')}"));
139+
}
140+
}
141+
}
142+
143+
// Windows PowerShell can use Az or AzureRM so first we check if powershell.exe is available
144+
if (CommandChecker.CommandExists(_windowsPowerShellExecutable))
145+
{
146+
string moduleToUse;
147+
148+
// depending on if Az.Profile or AzureRM.Profile is available, we need to change the prefix
149+
if (await CommandChecker.PowerShellModuleExistsAsync(_windowsPowerShellExecutable, _azProfileModuleName))
150+
{
151+
moduleToUse = _azProfileModuleName;
152+
}
153+
else if (await CommandChecker.PowerShellModuleExistsAsync(_windowsPowerShellExecutable, _azureRmProfileModuleName))
154+
{
155+
moduleToUse = _azureRmProfileModuleName;
156+
}
157+
else
158+
{
159+
// User doesn't have either Az.Profile or AzureRM.Profile
160+
if (StaticSettings.IsDebug)
161+
{
162+
ColoredConsole.WriteLine(VerboseColor("Unable to find Az.Profile or AzureRM.Profile."));
163+
}
164+
return (false, null);
165+
}
166+
167+
var az = new Executable("powershell", $"-NonInteractive -o Text -NoProfile -c {GetPowerShellAccessTokenScript(moduleToUse)}");
168+
169+
var stdout = new StringBuilder();
170+
var stderr = new StringBuilder();
171+
var exitCode = await az.RunAsync(o => stdout.AppendLine(o), e => stderr.AppendLine(e));
172+
if (exitCode == 0)
84173
{
85-
throw new CliException(stderr.ToString().Trim(' ', '\n', '\r') + $"{Environment.NewLine}" + "Make sure to run \"az login\" to log in to Azure and retry this command.");
174+
return (true, stdout.ToString().Trim(' ', '\n', '\r', '"'));
86175
}
87176
else
88177
{
89-
return stdout.ToString().Trim(' ', '\n', '\r', '"');
178+
if (StaticSettings.IsDebug)
179+
{
180+
ColoredConsole.WriteLine(VerboseColor($"Unable to fetch access token from '{moduleToUse}'. Error: {stderr.ToString().Trim(' ', '\n', '\r')}"));
181+
}
90182
}
91183
}
184+
return (false, null);
185+
}
186+
187+
// Sets the prefix of the script in case they have Az.Profile or AzureRM.Profile
188+
private static string GetPowerShellAccessTokenScript (string module)
189+
{
190+
string prefix;
191+
if (module == _azProfileModuleName)
192+
{
193+
prefix = "Az";
194+
}
195+
else if (module == _azureRmProfileModuleName)
196+
{
197+
prefix = "AzureRM";
198+
}
92199
else
93200
{
94-
throw new FileNotFoundException("Cannot find az cli. Please make sure to install az cli.");
201+
throw new ArgumentException($"Expected module to be '{_azProfileModuleName}' or '{_azureRmProfileModuleName}'");
95202
}
203+
204+
// This PowerShell script first grabs the Azure context, fetches the profile client and requests an accesstoken.
205+
// This entirely done using the Az.Profile module or AzureRM.Profile
206+
return $@"
207+
$currentAzureContext = Get-{prefix}Context;
208+
$azureRmProfile = [Microsoft.Azure.Commands.Common.Authentication.Abstractions.AzureRmProfileProvider]::Instance.Profile;
209+
$profileClient = New-Object Microsoft.Azure.Commands.ResourceManager.Common.RMProfileClient $azureRmProfile;
210+
$profileClient.AcquireAccessToken($currentAzureContext.Subscription.TenantId).AccessToken;
211+
";
96212
}
97213
}
98214
}

src/Azure.Functions.Cli/Common/CommandChecker.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Diagnostics;
22
using System.Runtime.InteropServices;
3+
using System.Text;
4+
using System.Threading.Tasks;
35

46
namespace Azure.Functions.Cli.Common
57
{
@@ -13,6 +15,18 @@ public static bool CommandExists(string command)
1315
? CheckExitCode("where", command)
1416
: CheckExitCode("bash", $"-c \"command -v {command}\"");
1517

18+
public async static Task<bool> PowerShellModuleExistsAsync(string powershellExecutable, string module)
19+
{
20+
// Attempt to get the specified module. If it cannot be found, throw.
21+
var exe = new Executable(powershellExecutable,
22+
$"-NonInteractive -o Text -NoProfile -c if(!(Get-Module -ListAvailable {module})) {{ throw '{module} module not found' }}");
23+
24+
var stdout = new StringBuilder();
25+
var stderr = new StringBuilder();
26+
var exitCode = await exe.RunAsync(o => stdout.AppendLine(o), e => stderr.AppendLine(e));
27+
return exitCode == 0;
28+
}
29+
1630
private static bool CheckExitCode(string fileName, string args, int expectedExitCode = 0)
1731
{
1832
var processStartInfo = new ProcessStartInfo

0 commit comments

Comments
 (0)