Skip to content

bundle and handle Azure PowerShell #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Sep 12, 2018
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ TestsResults*.xml
PowerShell.sln.DotSettings.user
*.msp
StyleCop.Cache

src/Modules
!src/Modules/Microsoft.AzureFunctions.PowerShellWorker
33 changes: 9 additions & 24 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ param(

$NeededTools = @{
DotnetSdk = ".NET SDK latest"
PowerShellGet = "PowerShellGet latest"
Pester = "Pester latest"
}

function needsDotnetSdk () {
Expand All @@ -33,34 +31,12 @@ function needsDotnetSdk () {
return $false
}

function needsPowerShellGet () {
$modules = Get-Module -ListAvailable -Name PowerShellGet | Where-Object Version -ge 1.6.0
if ($modules.Count -gt 0) {
return $false
}
return $true
}

function needsPester () {
$modules = Get-Module -ListAvailable -Name Pester
if ($modules.Count -gt 0) {
return $false
}
return $true
}

function getMissingTools () {
$missingTools = @()

if (needsDotnetSdk) {
$missingTools += $NeededTools.DotnetSdk
}
if (needsPowerShellGet) {
$missingTools += $NeededTools.PowerShellGet
}
if (needsPester) {
$missingTools += $NeededTools.Pester
}

return $missingTools
}
Expand All @@ -87,8 +63,17 @@ if($Clean) {
}

# Build step

# Install using PSDepend if it's available, otherwise use the backup script
if ((Get-Module -ListAvailable -Name PSDepend).Count -gt 0) {
Invoke-PSDepend -Path src -Force
} else {
& "$PSScriptRoot/tools/InstallDependencies.ps1"
}

dotnet build -c $Configuration
dotnet publish -c $Configuration

Push-Location package
dotnet pack -c $Configuration
Pop-Location
Expand Down
20 changes: 20 additions & 0 deletions examples/PSCoreApp/GetAzureVmHttpTrigger/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"disabled": false,
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
29 changes: 29 additions & 0 deletions examples/PSCoreApp/GetAzureVmHttpTrigger/run.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Trigger the function by running Invoke-RestMethod:
# Get everything: Invoke-RestMethod -Uri http://localhost:7071/api/GetAzureVmHttpTrigger
# Specify parameters: Invoke-RestMethod `
# -Uri http://localhost:7071/api/MyHttpTrigger?Name=testVm&ResourceGroupName=TESTRESOURCEGROUP

# Input bindings are passed in via param block.
param($req, $TriggerMetadata)

$cmdletParameters = $req.Query

# If the cmdlet fails, we want it to throw an exception
$cmdletParameters.ErrorAction = "Stop"

try {
# Splat the parameters that were passed in via query parameters
$vms = Get-AzureRmVM @cmdletParameters
$response = [HttpResponseContext]@{
StatusCode = '200' # OK
Body = ($vms | ConvertTo-Json)
}
} catch {
$response = [HttpResponseContext]@{
StatusCode = '400' # Bad Request
Body = @{ Exception = $_.Exception }
}
}

# You associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name res -Value $response
3 changes: 2 additions & 1 deletion src/Messaging/RpcLogger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public void Dispose()

public async void Log(LogLevel logLevel, string message, Exception exception = null, bool isUserLog = false)
{
var invocationId = _invocationId ?? "N/A";
if (isUserLog)
{
// For user logs, we send them over Rpc with details about the invocation.
Expand All @@ -47,7 +48,7 @@ public async void Log(LogLevel logLevel, string message, Exception exception = n
RpcLog = new RpcLog()
{
Exception = exception?.ToRpcException(),
InvocationId = _invocationId,
InvocationId = invocationId,
Level = logLevel,
Message = message
}
Expand Down
40 changes: 40 additions & 0 deletions src/PowerShell/PowerShellManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Security;

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

internal void AuthenticateToAzure()
{
// Try to authenticate to Azure
// TODO: The Azure Functions Host might supply these differently. This might change but works for the demo
string applicationId = Environment.GetEnvironmentVariable("ApplicationId");
string applicationSecret = Environment.GetEnvironmentVariable("ApplicationSecret");
string tenantId = Environment.GetEnvironmentVariable("TenantId");

if (string.IsNullOrEmpty(applicationId) ||
string.IsNullOrEmpty(applicationSecret) ||
string.IsNullOrEmpty(tenantId))
{
_logger.Log(LogLevel.Warning, "Required environment variables to authenticate to Azure were not present");
return;
}

// Build SecureString
var secureString = new SecureString();
foreach (char item in applicationSecret)
{
secureString.AppendChar(item);
}

using (ExecutionTimer.Start(_logger, "Authentication to Azure completed."))
{
_pwsh.AddCommand("Connect-AzureRmAccount")
.AddParameter("Credential", new PSCredential(applicationId, secureString))
.AddParameter("ServicePrincipal")
.AddParameter("TenantId", tenantId)
.InvokeAndClearCommands();
}
}

internal void InitializeRunspace()
{
// Add HttpResponseContext namespace so users can reference
Expand All @@ -57,6 +91,9 @@ internal void InitializeRunspace()

// Set the PSModulePath
Environment.SetEnvironmentVariable("PSModulePath", Path.Join(AppDomain.CurrentDomain.BaseDirectory, "Modules"));

// TODO: remove this when we figure out why it fixed #48
_pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module").AddParameter("Name", "AzureRm.Netcore").InvokeAndClearCommands();
}

internal Hashtable InvokeFunction(
Expand All @@ -69,6 +106,9 @@ internal Hashtable InvokeFunction(
{
Dictionary<string, ParameterMetadata> parameterMetadata;

// We attempt to authenticate to Azure with every invocation
AuthenticateToAzure();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please explain why we are doing this for every function invocation? Are you expecting the environment variables may change for each function invocation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we talked offline. I will move it to WorkerInit for the demo but also opened #49 for understanding.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed.


// We need to take into account if the user has an entry point.
// If it does, we invoke the command of that name. We also need to fetch
// the ParameterMetadata so that we can tell whether or not the user is asking
Expand Down
48 changes: 24 additions & 24 deletions src/RequestProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,27 @@ internal async Task ProcessRequestLoop()
while (await _msgStream.MoveNext())
{
request = _msgStream.GetCurrentMessage();
switch (request.ContentCase)

using (_logger.BeginScope(request.RequestId, request.InvocationRequest?.InvocationId))
{
case StreamingMessage.ContentOneofCase.WorkerInitRequest:
response = ProcessWorkerInitRequest(request);
break;

case StreamingMessage.ContentOneofCase.FunctionLoadRequest:
response = ProcessFunctionLoadRequest(request);
break;

case StreamingMessage.ContentOneofCase.InvocationRequest:
response = ProcessInvocationRequest(request);
break;

default:
throw new InvalidOperationException($"Not supportted message type: {request.ContentCase}");
switch (request.ContentCase)
{
case StreamingMessage.ContentOneofCase.WorkerInitRequest:
response = ProcessWorkerInitRequest(request);
break;

case StreamingMessage.ContentOneofCase.FunctionLoadRequest:
response = ProcessFunctionLoadRequest(request);
break;

case StreamingMessage.ContentOneofCase.InvocationRequest:
response = ProcessInvocationRequest(request);
break;

default:
throw new InvalidOperationException($"Not supportted message type: {request.ContentCase}");
}
}

await _msgStream.WriteAsync(response);
}
}
Expand Down Expand Up @@ -156,14 +159,11 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)

// Set the RequestId and InvocationId for logging purposes
Hashtable result = null;
using (_logger.BeginScope(request.RequestId, invocationRequest.InvocationId))
{
result = _powerShellManager.InvokeFunction(
functionInfo.ScriptPath,
functionInfo.EntryPoint,
triggerMetadata,
invocationRequest.InputData);
}
result = _powerShellManager.InvokeFunction(
functionInfo.ScriptPath,
functionInfo.EntryPoint,
triggerMetadata,
invocationRequest.InputData);

// Set out binding data and return response to be sent back to host
foreach (KeyValuePair<string, BindingInfo> binding in functionInfo.OutputBindings)
Expand Down
21 changes: 21 additions & 0 deletions src/requirements.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@{
# Packaged with the PowerShell Language Worker
'PowerShellGet' = @{
Version = '1.6.7'
Target = 'src/Modules'
}
'Microsoft.PowerShell.Archive' = @{
Version = '1.1.0.0'
Target = 'src/Modules'
}
'AzureRM.Netcore' = @{
Version = '0.13.1'
Target = 'src/Modules'
}

# Dev dependencies
'Pester' = @{
Version = 'latest'
Target = 'CurrentUser'
}
}
28 changes: 28 additions & 0 deletions tools/InstallDependencies.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#
# Copyright (c) Microsoft. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
#

$dependencies = Import-PowerShellDataFile "$PSScriptRoot/../src/requirements.psd1"

foreach ($key in $dependencies.Keys) {
$params = @{ Name = $key }

if ($dependencies[$key].Version -ne 'latest') {
# Save-Module doesn't have -Version so we have to specify Min and Max
$params.MinimumVersion = $dependencies[$key].Version
$params.MaximumVersion = $dependencies[$key].Version
}

if($dependencies[$key].Target -eq 'CurrentUser') {
$params.Scope = $dependencies[$key].Target
Install-Module @params
} else {
$params.Path = $dependencies[$key].Target
if (Test-Path "$($params.Path)/$key") {
Write-Host "'$key' - Module already installed"
} else {
Save-Module @params
}
}
}