Skip to content

Commit 5fdca95

Browse files
profile support and remove auto-auth to azure
1 parent 4593c43 commit 5fdca95

File tree

3 files changed

+76
-61
lines changed

3 files changed

+76
-61
lines changed

examples/PSCoreApp/Profile.ps1

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# This script will be run on every COLD START of the Function App
2+
# You can define helper functions, run commands, or specify environment variables
3+
# NOTE: any variables defined that are not environment variables will get reset after the first execution
4+
5+
# Example usecases for a Profile.ps1:
6+
7+
# Authenticating with Azure PowerShell using MSI
8+
# $tokenAuthURI = $env:MSI_ENDPOINT + "?resource=https://management.azure.com&api-version=2017-09-01"
9+
# $tokenResponse = Invoke-RestMethod -Method Get -Headers @{"Secret"="$env:MSI_SECRET"} -Uri $tokenAuthURI
10+
# Connect-AzAccount -AccessToken $tokenResponse.access_token -AccountId $env:WEBSITE_SITE_NAME
11+
12+
# Enabling legacy AzureRm alias in Azure PowerShell
13+
# Enable-AzureRmAlias
14+
15+
# Defining a function that you can refernce in any of your PowerShell functions:
16+
# function Get-MyData {
17+
# @{
18+
# Foo = 5
19+
# }
20+
# }

src/PowerShell/PowerShellManager.cs

Lines changed: 43 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,19 @@
1717

1818
namespace Microsoft.Azure.Functions.PowerShellWorker.PowerShell
1919
{
20+
using System.Linq;
2021
using System.Management.Automation;
22+
using System.Text;
2123

2224
internal class PowerShellManager
2325
{
2426
private readonly ILogger _logger;
2527
private readonly PowerShell _pwsh;
28+
private const string PROFILE_FILENAME = "Profile.ps1";
29+
30+
// The path to the FunctionApp root. This is set at the first FunctionLoad message
31+
//and used for determining the path to the 'Profile.ps1' and 'Modules' folder.
32+
internal string FunctionAppRootLocation { get; set; }
2633

2734
internal PowerShellManager(ILogger logger)
2835
{
@@ -48,69 +55,54 @@ internal PowerShellManager(ILogger logger)
4855
_pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding;
4956
}
5057

51-
internal void AuthenticateToAzure()
58+
internal void InvokeProfile()
5259
{
53-
// Check if Az.Profile is available
54-
Collection<PSModuleInfo> azProfile = _pwsh.AddCommand("Get-Module")
55-
.AddParameter("ListAvailable")
56-
.AddParameter("Name", "Az.Profile")
57-
.InvokeAndClearCommands<PSModuleInfo>();
58-
59-
if (azProfile.Count == 0)
60+
IEnumerable<string> profiles = Directory.EnumerateFiles(FunctionAppRootLocation, PROFILE_FILENAME);
61+
if (profiles.Count() == 0)
6062
{
61-
_logger.Log(LogLevel.Trace, "Required module to automatically authenticate with Azure `Az.Profile` was not found in the PSModulePath.");
63+
_logger.Log(LogLevel.Trace, $"No 'Profile.ps1' found at: {FunctionAppRootLocation}");
6264
return;
6365
}
6466

65-
// Try to authenticate to Azure using MSI
66-
string msiSecret = Environment.GetEnvironmentVariable("MSI_SECRET");
67-
string msiEndpoint = Environment.GetEnvironmentVariable("MSI_ENDPOINT");
68-
string accountId = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME");
69-
70-
if (!string.IsNullOrEmpty(msiSecret) &&
71-
!string.IsNullOrEmpty(msiEndpoint) &&
72-
!string.IsNullOrEmpty(accountId))
67+
var dotSourced = new StringBuilder(". ").Append(QuoteEscapeString(profiles.First()));
68+
_pwsh.AddScript(dotSourced.ToString()).InvokeAndClearCommands();
69+
70+
if (_pwsh.HadErrors)
7371
{
74-
// NOTE: There is a limitation in Azure PowerShell that prevents us from using the parameter set:
75-
// Connect-AzAccount -MSI or Connect-AzAccount -Identity
76-
// see this GitHub issue https://github.com/Azure/azure-powershell/issues/7876
77-
// As a workaround, we can all an API endpoint on the MSI_ENDPOINT to get an AccessToken and use that to authenticate
78-
Collection<PSObject> response = _pwsh.AddCommand("Microsoft.PowerShell.Utility\\Invoke-RestMethod")
79-
.AddParameter("Method", "Get")
80-
.AddParameter("Headers", new Hashtable {{ "Secret", msiSecret }})
81-
.AddParameter("Uri", $"{msiEndpoint}?resource=https://management.azure.com&api-version=2017-09-01")
82-
.InvokeAndClearCommands<PSObject>();
72+
var logMessage = $"Invoking the Profile had errors. See logs for details. Profile location: {FunctionAppRootLocation}";
73+
_logger.Log(LogLevel.Error, logMessage, isUserLog: true);
74+
throw new InvalidOperationException(logMessage);
75+
}
76+
}
8377

84-
if(_pwsh.HadErrors)
85-
{
86-
_logger.Log(LogLevel.Warning, "Failed to Authenticate to Azure via MSI. Check the logs for the errors generated.");
87-
}
88-
else
78+
/// <summary>
79+
/// Wrap a string in quotes to make it safe to use in scripts.
80+
/// </summary>
81+
/// <param name="path">The path to wrap in quotes.</param>
82+
/// <returns>The given path wrapped in quotes appropriately.</returns>
83+
private static StringBuilder QuoteEscapeString(string path)
84+
{
85+
var sb = new StringBuilder(path.Length + 2); // Length of string plus two quotes
86+
sb.Append('\'');
87+
if (!path.Contains('\''))
88+
{
89+
sb.Append(path);
90+
}
91+
else
92+
{
93+
foreach (char c in path)
8994
{
90-
// We have successfully authenticated to Azure so we can return out.
91-
using (ExecutionTimer.Start(_logger, "Authentication to Azure"))
95+
if (c == '\'')
9296
{
93-
_pwsh.AddCommand("Az.Profile\\Connect-AzAccount")
94-
.AddParameter("AccessToken", response[0].Properties["access_token"].Value)
95-
.AddParameter("AccountId", accountId)
96-
.InvokeAndClearCommands();
97-
98-
if(_pwsh.HadErrors)
99-
{
100-
_logger.Log(LogLevel.Warning, "Failed to Authenticate to Azure. Check the logs for the errors generated.");
101-
}
102-
else
103-
{
104-
// We've successfully authenticated to Azure so we can return
105-
return;
106-
}
97+
sb.Append("''");
98+
continue;
10799
}
100+
101+
sb.Append(c);
108102
}
109103
}
110-
else
111-
{
112-
_logger.Log(LogLevel.Trace, "Skip authentication to Azure via MSI. Environment variables for authenticating to Azure are not present.");
113-
}
104+
sb.Append('\'');
105+
return sb;
114106
}
115107

116108
internal void InitializeRunspace()

src/RequestProcessor.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ internal class RequestProcessor
2323
private readonly MessagingStream _msgStream;
2424
private readonly PowerShellManager _powerShellManager;
2525

26-
// used to determine if we have already added the Function App's 'Modules'
27-
// folder to the PSModulePath
28-
private bool _prependedPath;
26+
// This is somewhat of a workaround for the fact that the WorkerInitialize message does
27+
// not contain the file path of the Function App. Instead, we use this bool during the
28+
// FunctionLoad message to initialize the Function App since we have the path.
29+
private bool _initializedFunctionApp;
2930

3031
internal RequestProcessor(MessagingStream msgStream)
3132
{
@@ -106,17 +107,19 @@ internal StreamingMessage ProcessFunctionLoadRequest(StreamingMessage request)
106107

107108
// if we haven't yet, add the well-known Function App module path to the PSModulePath
108109
// The location of this module path is in a folder called "Modules" in the root of the Function App.
109-
if (!_prependedPath)
110+
if (!_initializedFunctionApp)
110111
{
111-
string functionAppModulesPath = Path.GetFullPath(
112-
Path.Combine(functionLoadRequest.Metadata.Directory, "..", "Modules"));
113-
_powerShellManager.PrependToPSModulePath(functionAppModulesPath);
112+
string functionAppRoot = Path.GetFullPath(Path.Combine(functionLoadRequest.Metadata.Directory, ".."));
113+
_powerShellManager.FunctionAppRootLocation = functionAppRoot;
114+
115+
// Prepend the Function App's 'Modules' folder to the PSModulePath
116+
_powerShellManager.PrependToPSModulePath(Path.Combine(functionAppRoot, "Modules"));
114117

115118
// Since this is the first time we know where the location of the FunctionApp is,
116-
// we can attempt to authenticate to Azure at this time.
117-
_powerShellManager.AuthenticateToAzure();
119+
// we can attempt to execute the Profile.
120+
_powerShellManager.InvokeProfile();
118121

119-
_prependedPath = true;
122+
_initializedFunctionApp = true;
120123
}
121124
}
122125
catch (Exception e)

0 commit comments

Comments
 (0)