Skip to content

Commit 50659a9

Browse files
bundle and handle Azure PowerShell (#47)
* add logic to grab dependencies * move psdepends logic * PSDepends is not a required dependency * move comment * actually include install deps script * fix path * commit handle Azure authentication * move WriteAsync out of using * authenticate to azure per-request and address feedback * moved Auth to Azure in Worker Init for now
1 parent 7f1779f commit 50659a9

9 files changed

Lines changed: 175 additions & 49 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,6 @@ TestsResults*.xml
5858
PowerShell.sln.DotSettings.user
5959
*.msp
6060
StyleCop.Cache
61+
62+
src/Modules
63+
!src/Modules/Microsoft.AzureFunctions.PowerShellWorker

build.ps1

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ param(
2020

2121
$NeededTools = @{
2222
DotnetSdk = ".NET SDK latest"
23-
PowerShellGet = "PowerShellGet latest"
24-
Pester = "Pester latest"
2523
}
2624

2725
function needsDotnetSdk () {
@@ -33,34 +31,12 @@ function needsDotnetSdk () {
3331
return $false
3432
}
3533

36-
function needsPowerShellGet () {
37-
$modules = Get-Module -ListAvailable -Name PowerShellGet | Where-Object Version -ge 1.6.0
38-
if ($modules.Count -gt 0) {
39-
return $false
40-
}
41-
return $true
42-
}
43-
44-
function needsPester () {
45-
$modules = Get-Module -ListAvailable -Name Pester
46-
if ($modules.Count -gt 0) {
47-
return $false
48-
}
49-
return $true
50-
}
51-
5234
function getMissingTools () {
5335
$missingTools = @()
5436

5537
if (needsDotnetSdk) {
5638
$missingTools += $NeededTools.DotnetSdk
5739
}
58-
if (needsPowerShellGet) {
59-
$missingTools += $NeededTools.PowerShellGet
60-
}
61-
if (needsPester) {
62-
$missingTools += $NeededTools.Pester
63-
}
6440

6541
return $missingTools
6642
}
@@ -87,8 +63,17 @@ if($Clean) {
8763
}
8864

8965
# Build step
66+
67+
# Install using PSDepend if it's available, otherwise use the backup script
68+
if ((Get-Module -ListAvailable -Name PSDepend).Count -gt 0) {
69+
Invoke-PSDepend -Path src -Force
70+
} else {
71+
& "$PSScriptRoot/tools/InstallDependencies.ps1"
72+
}
73+
9074
dotnet build -c $Configuration
9175
dotnet publish -c $Configuration
76+
9277
Push-Location package
9378
dotnet pack -c $Configuration
9479
Pop-Location
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"disabled": false,
3+
"bindings": [
4+
{
5+
"authLevel": "function",
6+
"type": "httpTrigger",
7+
"direction": "in",
8+
"name": "req",
9+
"methods": [
10+
"get",
11+
"post"
12+
]
13+
},
14+
{
15+
"type": "http",
16+
"direction": "out",
17+
"name": "res"
18+
}
19+
]
20+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Trigger the function by running Invoke-RestMethod:
2+
# Get everything: Invoke-RestMethod -Uri http://localhost:7071/api/GetAzureVmHttpTrigger
3+
# Specify parameters: Invoke-RestMethod `
4+
# -Uri http://localhost:7071/api/MyHttpTrigger?Name=testVm&ResourceGroupName=TESTRESOURCEGROUP
5+
6+
# Input bindings are passed in via param block.
7+
param($req, $TriggerMetadata)
8+
9+
$cmdletParameters = $req.Query
10+
11+
# If the cmdlet fails, we want it to throw an exception
12+
$cmdletParameters.ErrorAction = "Stop"
13+
14+
try {
15+
# Splat the parameters that were passed in via query parameters
16+
$vms = Get-AzureRmVM @cmdletParameters
17+
$response = [HttpResponseContext]@{
18+
StatusCode = '200' # OK
19+
Body = ($vms | ConvertTo-Json)
20+
}
21+
} catch {
22+
$response = [HttpResponseContext]@{
23+
StatusCode = '400' # Bad Request
24+
Body = @{ Exception = $_.Exception }
25+
}
26+
}
27+
28+
# You associate values to output bindings by calling 'Push-OutputBinding'.
29+
Push-OutputBinding -Name res -Value $response

src/Messaging/RpcLogger.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public void Dispose()
3838

3939
public async void Log(LogLevel logLevel, string message, Exception exception = null, bool isUserLog = false)
4040
{
41+
var invocationId = _invocationId ?? "N/A";
4142
if (isUserLog)
4243
{
4344
// For user logs, we send them over Rpc with details about the invocation.
@@ -47,7 +48,7 @@ public async void Log(LogLevel logLevel, string message, Exception exception = n
4748
RpcLog = new RpcLog()
4849
{
4950
Exception = exception?.ToRpcException(),
50-
InvocationId = _invocationId,
51+
InvocationId = invocationId,
5152
Level = logLevel,
5253
Message = message
5354
}

src/PowerShell/PowerShellManager.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Collections.Generic;
99
using System.Collections.ObjectModel;
1010
using System.IO;
11+
using System.Security;
1112

1213
using Microsoft.Azure.Functions.PowerShellWorker.Utility;
1314
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
@@ -49,6 +50,39 @@ internal PowerShellManager(RpcLogger logger)
4950
_pwsh.Streams.Warning.DataAdding += streamHandler.WarningDataAdding;
5051
}
5152

53+
internal void AuthenticateToAzure()
54+
{
55+
// Try to authenticate to Azure
56+
// TODO: The Azure Functions Host might supply these differently. This might change but works for the demo
57+
string applicationId = Environment.GetEnvironmentVariable("ApplicationId");
58+
string applicationSecret = Environment.GetEnvironmentVariable("ApplicationSecret");
59+
string tenantId = Environment.GetEnvironmentVariable("TenantId");
60+
61+
if (string.IsNullOrEmpty(applicationId) ||
62+
string.IsNullOrEmpty(applicationSecret) ||
63+
string.IsNullOrEmpty(tenantId))
64+
{
65+
_logger.Log(LogLevel.Warning, "Required environment variables to authenticate to Azure were not present");
66+
return;
67+
}
68+
69+
// Build SecureString
70+
var secureString = new SecureString();
71+
foreach (char item in applicationSecret)
72+
{
73+
secureString.AppendChar(item);
74+
}
75+
76+
using (ExecutionTimer.Start(_logger, "Authentication to Azure completed."))
77+
{
78+
_pwsh.AddCommand("Connect-AzureRmAccount")
79+
.AddParameter("Credential", new PSCredential(applicationId, secureString))
80+
.AddParameter("ServicePrincipal")
81+
.AddParameter("TenantId", tenantId)
82+
.InvokeAndClearCommands();
83+
}
84+
}
85+
5286
internal void InitializeRunspace()
5387
{
5488
// Add HttpResponseContext namespace so users can reference
@@ -57,6 +91,11 @@ internal void InitializeRunspace()
5791

5892
// Set the PSModulePath
5993
Environment.SetEnvironmentVariable("PSModulePath", Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"));
94+
95+
// TODO: remove this when we figure out why it fixed #48
96+
_pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module").AddParameter("Name", "AzureRm.Netcore").InvokeAndClearCommands();
97+
98+
AuthenticateToAzure();
6099
}
61100

62101
internal Hashtable InvokeFunction(

src/RequestProcessor.cs

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,24 +38,27 @@ internal async Task ProcessRequestLoop()
3838
while (await _msgStream.MoveNext())
3939
{
4040
request = _msgStream.GetCurrentMessage();
41-
switch (request.ContentCase)
41+
42+
using (_logger.BeginScope(request.RequestId, request.InvocationRequest?.InvocationId))
4243
{
43-
case StreamingMessage.ContentOneofCase.WorkerInitRequest:
44-
response = ProcessWorkerInitRequest(request);
45-
break;
46-
47-
case StreamingMessage.ContentOneofCase.FunctionLoadRequest:
48-
response = ProcessFunctionLoadRequest(request);
49-
break;
50-
51-
case StreamingMessage.ContentOneofCase.InvocationRequest:
52-
response = ProcessInvocationRequest(request);
53-
break;
54-
55-
default:
56-
throw new InvalidOperationException($"Not supportted message type: {request.ContentCase}");
44+
switch (request.ContentCase)
45+
{
46+
case StreamingMessage.ContentOneofCase.WorkerInitRequest:
47+
response = ProcessWorkerInitRequest(request);
48+
break;
49+
50+
case StreamingMessage.ContentOneofCase.FunctionLoadRequest:
51+
response = ProcessFunctionLoadRequest(request);
52+
break;
53+
54+
case StreamingMessage.ContentOneofCase.InvocationRequest:
55+
response = ProcessInvocationRequest(request);
56+
break;
57+
58+
default:
59+
throw new InvalidOperationException($"Not supportted message type: {request.ContentCase}");
60+
}
5761
}
58-
5962
await _msgStream.WriteAsync(response);
6063
}
6164
}
@@ -156,14 +159,11 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
156159

157160
// Set the RequestId and InvocationId for logging purposes
158161
Hashtable result = null;
159-
using (_logger.BeginScope(request.RequestId, invocationRequest.InvocationId))
160-
{
161-
result = _powerShellManager.InvokeFunction(
162-
functionInfo.ScriptPath,
163-
functionInfo.EntryPoint,
164-
triggerMetadata,
165-
invocationRequest.InputData);
166-
}
162+
result = _powerShellManager.InvokeFunction(
163+
functionInfo.ScriptPath,
164+
functionInfo.EntryPoint,
165+
triggerMetadata,
166+
invocationRequest.InputData);
167167

168168
// Set out binding data and return response to be sent back to host
169169
foreach (KeyValuePair<string, BindingInfo> binding in functionInfo.OutputBindings)

src/requirements.psd1

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@{
2+
# Packaged with the PowerShell Language Worker
3+
'PowerShellGet' = @{
4+
Version = '1.6.7'
5+
Target = 'src/Modules'
6+
}
7+
'Microsoft.PowerShell.Archive' = @{
8+
Version = '1.1.0.0'
9+
Target = 'src/Modules'
10+
}
11+
'AzureRM.Netcore' = @{
12+
Version = '0.13.1'
13+
Target = 'src/Modules'
14+
}
15+
16+
# Dev dependencies
17+
'Pester' = @{
18+
Version = 'latest'
19+
Target = 'CurrentUser'
20+
}
21+
}

tools/InstallDependencies.ps1

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#
2+
# Copyright (c) Microsoft. All rights reserved.
3+
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
4+
#
5+
6+
$dependencies = Import-PowerShellDataFile "$PSScriptRoot/../src/requirements.psd1"
7+
8+
foreach ($key in $dependencies.Keys) {
9+
$params = @{ Name = $key }
10+
11+
if ($dependencies[$key].Version -ne 'latest') {
12+
# Save-Module doesn't have -Version so we have to specify Min and Max
13+
$params.MinimumVersion = $dependencies[$key].Version
14+
$params.MaximumVersion = $dependencies[$key].Version
15+
}
16+
17+
if($dependencies[$key].Target -eq 'CurrentUser') {
18+
$params.Scope = $dependencies[$key].Target
19+
Install-Module @params
20+
} else {
21+
$params.Path = $dependencies[$key].Target
22+
if (Test-Path "$($params.Path)/$key") {
23+
Write-Host "'$key' - Module already installed"
24+
} else {
25+
Save-Module @params
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)