Skip to content

Make 'HttpResponseContext' not needed in PS function script #113

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 3 commits into from
Dec 11, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
42 changes: 0 additions & 42 deletions src/Http/HttpResponseContext.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#
# Copyright (c) Microsoft. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
#

@{

# Script module or binary module file associated with this manifest.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
#

# This holds the current state of the output bindings
using namespace System.Management.Automation
using namespace System.Management.Automation.Runspaces
using namespace Microsoft.Azure.Functions.PowerShellWorker

# This holds the current state of the output bindings.
$script:_OutputBindings = @{}
# This loads the resource strings.
Import-LocalizedData LocalizedData -FileName PowerShellWorker.Resource.psd1

<#
.SYNOPSIS
Expand Down Expand Up @@ -49,6 +55,50 @@ function Get-OutputBinding {
}
}

# Helper private function that validates the output value and does necessary conversion.
function Convert-OutputBindingValue {
param(
[Parameter(Mandatory=$true)]
[string]
$Name,

[Parameter(Mandatory=$true)]
[object]
$Value
)

# Check if we can get the binding metadata of the current running function.
$funcMetadataType = "FunctionMetadata" -as [type]
if ($null -eq $funcMetadataType) {
return $Value
}

# Get the runspace where we are currently running in and then get all output bindings.
$bindingMap = $funcMetadataType::GetOutputBindingInfo([Runspace]::DefaultRunspace.InstanceId)
if ($null -eq $bindingMap) {
return $Value
}

# Get the binding information of given output binding name.
$bindingInfo = $bindingMap[$Name]
if ($bindingInfo.Type -ne "http") {
return $Value
}

# Nothing to do if the value is already a HttpResponseContext object.
if ($Value -as [HttpResponseContext]) {
return $Value
}

try {
return [LanguagePrimitives]::ConvertTo($Value, [HttpResponseContext])
} catch [PSInvalidCastException] {
$conversionMsg = $_.Exception.Message
$errorMsg = $LocalizedData.InvalidHttpOutputValue -f $Name, $conversionMsg
throw $errorMsg
}
}

# Helper private function that sets an OutputBinding.
function Push-KeyValueOutputBinding {
param (
Expand All @@ -63,10 +113,13 @@ function Push-KeyValueOutputBinding {
[switch]
$Force
)
if(!$script:_OutputBindings.ContainsKey($Name) -or $Force.IsPresent) {

if (!$script:_OutputBindings.ContainsKey($Name) -or $Force.IsPresent) {
$Value = Convert-OutputBindingValue -Name $Name -Value $Value
$script:_OutputBindings[$Name] = $Value
} else {
throw "Output binding '$Name' is already set. To override the value, use -Force."
$errorMsg = $LocalizedData.OutputBindingAlreadySet -f $Name
throw $errorMsg
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#
# Copyright (c) Microsoft. All rights reserved.
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
#

ConvertFrom-StringData @'
###PSLOC
OutputBindingAlreadySet=Output binding '{0}' is already set. To override the value, use -Force.
InvalidHttpOutputValue=The given value for the 'http' output binding '{0}' cannot be converted to the type 'HttpResponseContext'. The conversion failed with the following error: {1}
###PSLOC
'@
40 changes: 30 additions & 10 deletions src/PowerShell/PowerShellManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,36 @@ internal string ConvertToJson(object fromObj)
.InvokeAndClearCommands<string>()[0];
}

private Dictionary<string, ParameterMetadata> RetriveParameterMetadata(
AzFunctionInfo functionInfo,
out string moduleName)
/// <summary>
/// Helper method to prepend the FunctionApp module folder to the module path.
/// </summary>
internal void PrependToPSModulePath(string directory)
{
// Adds the passed in directory to the front of the PSModulePath using the path separator of the OS.
string psModulePath = Environment.GetEnvironmentVariable("PSModulePath");
Environment.SetEnvironmentVariable("PSModulePath", $"{directory}{Path.PathSeparator}{psModulePath}");
}

/// <summary>
/// Helper method to set the output binding metadata for the function that is about to run.
/// </summary>
internal void RegisterFunctionMetadata(AzFunctionInfo functionInfo)
{
var outputBindings = new ReadOnlyDictionary<string, BindingInfo>(functionInfo.OutputBindings);
FunctionMetadata.OutputBindingCache.AddOrUpdate(_pwsh.Runspace.InstanceId,
outputBindings,
(key, value) => outputBindings);
}

/// <summary>
/// Helper method to clear the output binding metadata for the function that has done running.
/// </summary>
internal void UnregisterFunctionMetadata()
{
FunctionMetadata.OutputBindingCache.TryRemove(_pwsh.Runspace.InstanceId, out _);
}

private Dictionary<string, ParameterMetadata> RetriveParameterMetadata(AzFunctionInfo functionInfo, out string moduleName)
{
moduleName = null;
string scriptPath = functionInfo.ScriptPath;
Expand All @@ -266,13 +293,6 @@ private Dictionary<string, ParameterMetadata> RetriveParameterMetadata(
}
}

internal void PrependToPSModulePath(string directory)
{
// Adds the passed in directory to the front of the PSModulePath using the path separator of the OS.
string psModulePath = Environment.GetEnvironmentVariable("PSModulePath");
Environment.SetEnvironmentVariable("PSModulePath", $"{directory}{Path.PathSeparator}{psModulePath}");
}

private void ResetRunspace(string moduleName)
{
// Reset the runspace to the Initial Session State
Expand Down
32 changes: 32 additions & 0 deletions src/Public/FunctionMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//

using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using Microsoft.Azure.WebJobs.Script.Grpc.Messages;
using Google.Protobuf.Collections;

namespace Microsoft.Azure.Functions.PowerShellWorker
{
/// <summary>
/// Function metadata for the PowerShellWorker module to query.
/// </summary>
public static class FunctionMetadata
{
internal static ConcurrentDictionary<Guid, ReadOnlyDictionary<string, BindingInfo>> OutputBindingCache
= new ConcurrentDictionary<Guid, ReadOnlyDictionary<string, BindingInfo>>();

/// <summary>
/// Get the binding metadata for the given Runspace instance id.
/// </summary>
public static ReadOnlyDictionary<string, BindingInfo> GetOutputBindingInfo(Guid runspaceInstanceId)
{
ReadOnlyDictionary<string, BindingInfo> outputBindings = null;
OutputBindingCache.TryGetValue(runspaceInstanceId, out outputBindings);
return outputBindings;
}
}
}
33 changes: 33 additions & 0 deletions src/Http/HttpRequestContext.cs → src/Public/HttpContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
//

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;

namespace Microsoft.Azure.Functions.PowerShellWorker
{
Expand Down Expand Up @@ -58,4 +60,35 @@ public HttpRequestContext()
/// </summary>
public object RawBody { get; internal set; }
}

/// <summary>
/// Custom type represent the context of the Http response.
/// </summary>
public class HttpResponseContext
{
/// <summary>
/// Gets or sets the Body of the Http response.
/// </summary>
public object Body { get; set; }

/// <summary>
/// Gets or sets the ContentType of the Http response.
/// </summary>
public string ContentType { get; set; } = "text/plain";

/// <summary>
/// Gets or sets the EnableContentNegotiation of the Http response.
/// </summary>
public bool EnableContentNegotiation { get; set; }

/// <summary>
/// Gets or sets the Headers of the Http response.
/// </summary>
public IDictionary Headers { get; set; }

/// <summary>
/// Gets or sets the StatusCode of the Http response.
/// </summary>
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
}
}
5 changes: 5 additions & 0 deletions src/RequestProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
{
// Load information about the function
var functionInfo = _functionLoader.GetFunctionInfo(invocationRequest.FunctionId);
_powerShellManager.RegisterFunctionMetadata(functionInfo);

Hashtable results = functionInfo.Type == AzFunctionType.OrchestrationFunction
? InvokeOrchestrationFunction(functionInfo, invocationRequest)
Expand All @@ -155,6 +156,10 @@ internal StreamingMessage ProcessInvocationRequest(StreamingMessage request)
status.Status = StatusResult.Types.Status.Failure;
status.Exception = e.ToRpcException();
}
finally
{
_powerShellManager.UnregisterFunctionMetadata();
}

return response;
}
Expand Down
9 changes: 7 additions & 2 deletions src/Utility/TypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,15 @@ private static RpcHttp ToRpcHttp(this HttpResponseContext httpResponseContext, P
rpcHttp.Body = httpResponseContext.Body.ToTypedData(psHelper);
}

rpcHttp.EnableContentNegotiation = httpResponseContext.EnableContentNegotiation;

// Add all the headers. ContentType is separated for convenience
foreach (var item in httpResponseContext.Headers)
if (httpResponseContext.Headers != null)
{
rpcHttp.Headers.Add(item.Key, item.Value);
foreach (DictionaryEntry item in httpResponseContext.Headers)
{
rpcHttp.Headers.Add(item.Key.ToString(), item.Value.ToString());
}
}

// Allow the user to set content-type in the Headers
Expand Down
20 changes: 10 additions & 10 deletions test/E2E/TestFunctionApp/TestBasicHttpTrigger/run.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ $name = $req.Query.Name
if (-not $name) { $name = $req.Body.Name }

if($name) {
$status = 202
$body = "Hello " + $name
# Cast the value to HttpResponseContext explicitly.
Push-OutputBinding -Name res -Value ([HttpResponseContext]@{
StatusCode = 202
Body = "Hello " + $name
})
}
else {
$status = "400"
$body = "Please pass a name on the query string or in the request body."
# Convert value to HttpResponseContext implicitly for 'http' output.
Push-OutputBinding -Name res -Value @{
StatusCode = "400"
Body = "Please pass a name on the query string or in the request body."
}
}

# You associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name res -Value ([HttpResponseContext]@{
StatusCode = $status
Body = $body
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ $name = $TriggerMetadata.req.Query.Name
if (-not $name) { $name = $TriggerMetadata.req.Body.Name }

if($name) {
$status = [System.Net.HttpStatusCode]::Accepted
$body = "Hello " + $name
# Cast the value to HttpResponseContext explicitly.
Push-OutputBinding -Name res -Value ([HttpResponseContext]@{
StatusCode = [System.Net.HttpStatusCode]::Accepted
Body = "Hello " + $name
})
}
else {
$status = 400
$body = "Please pass a name on the query string or in the request body."
# Convert value to HttpResponseContext implicitly for 'http' output.
Push-OutputBinding -Name res -Value @{
StatusCode = 400
Body = "Please pass a name on the query string or in the request body."
}
}

# You associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name res -Value ([HttpResponseContext]@{
StatusCode = $status
Body = $body
})
14 changes: 14 additions & 0 deletions test/Unit/PowerShell/PowerShellManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,5 +145,19 @@ public void PrependingToPSModulePathShouldWork()
Environment.SetEnvironmentVariable("PSModulePath", modulePathBefore);
}
}

[Fact]
public void RegisterAndUnregisterFunctionMetadataShouldWork()
{
var logger = new ConsoleLogger();
var manager = new PowerShellManager(logger);
var functionInfo = GetAzFunctionInfo("dummy-path", string.Empty);

Assert.Empty(FunctionMetadata.OutputBindingCache);
manager.RegisterFunctionMetadata(functionInfo);
Assert.Single(FunctionMetadata.OutputBindingCache);
manager.UnregisterFunctionMetadata();
Assert.Empty(FunctionMetadata.OutputBindingCache);
}
}
}
2 changes: 1 addition & 1 deletion test/Unit/Utility/TypeExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ public void TestObjectToTypedDataRpcHttpContentTypeInHeader()
var input = new HttpResponseContext
{
Body = data,
Headers = { { "content-type", "text/html" } }
Headers = new Hashtable { { "content-type", "text/html" } }
};
var expected = new TypedData
{
Expand Down