Skip to content
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

[vividus-plugin-azure-functions] Introduce Azure Functions support #1497

Merged
merged 1 commit into from
Mar 23, 2021
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
6 changes: 5 additions & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,13 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
shell: bash
run: |
if [[ -n $BROWSERSTACK_USER && -n $BROWSERSTACK_KEY && -n $APPLITOOLS_READ_KEY && -n $APPLITOOLS_READ_KEY && -n $MONGODB_USERNAME && -n $MONGODB_PASSWORD && -n $KAFKA_PASSWORD && -n $AWS_ACCESS_KEY_ID && -n $AWS_SECRET_ACCESS_KEY && -n $AWS_REGION ]]; then
if [[ -n $BROWSERSTACK_USER && -n $BROWSERSTACK_KEY && -n $APPLITOOLS_READ_KEY && -n $APPLITOOLS_READ_KEY && -n $MONGODB_USERNAME && -n $MONGODB_PASSWORD && -n $KAFKA_PASSWORD && -n $AWS_ACCESS_KEY_ID && -n $AWS_SECRET_ACCESS_KEY && -n $AWS_REGION && -n $AZURE_CLIENT_ID && -n $AZURE_CLIENT_SECRET && -n $AZURE_TENANT_ID && -n $AZURE_SUBSCRIPTION_ID ]]; then
./gradlew :vividus-tests:runStories -x testVividusInitialization \
-Pvividus.configuration.environments=system/web,system/api \
-Pvividus.configuration.suites=system \
Expand Down
2 changes: 2 additions & 0 deletions docs/modules/plugins/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
** xref:plugin-aws-kinesis.adoc[Kinesis]
** xref:plugin-aws-lambda.adoc[Lambda]
** xref:plugin-aws-s3.adoc[S3]
* xref:azure.adoc[Azure]
** xref:plugin-azure-functions.adoc[Functions]
* xref:plugin-csv.adoc[CSV]
* xref:plugin-datetime.adoc[Date/Time]
* xref:plugin-db.adoc[Relational DB*]
Expand Down
6 changes: 6 additions & 0 deletions docs/modules/plugins/pages/azure.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
= What is Azure

Microsoft https://azure.microsoft.com/[Azure] is a cloud computing service created by Microsoft for building, testing, deploying,
and managing applications and services through Microsoft-managed data centers.

Vividus provides set of plugins to interact with the services.
60 changes: 60 additions & 0 deletions docs/modules/plugins/pages/plugin-azure-functions.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
= Azure functions Plugin

The plugin provides functionality to interact with https://azure.microsoft.com/en-us/services/functions/[Azure Functions]

== Installation

.build.gradle
[source,gradle,subs="attributes+"]
----
implementation(group: 'org.vividus', name: 'vividus-plugin-azure-functions', version: '{current-version}')
----

== Configuration

=== Authentication

Authentication process relies on the configuration of environment variables.

See the official https://github.com/Azure/azure-sdk-for-java/tree/master/sdk/identity/azure-identity#environment-variables["Azure identity"] guide to get more details on what types of authentication could be used.

=== Azure Environment selection

The Azure environment could be selected via a property: `azure.functions.environment`
The default value is AZURE

Possible values are: AZURE, AZURE_CHINA, AZURE_GERMANY, AZURE_US_GOVERNMENT

=== Azure Subscription selection

The azure subscription should be configured via `AZURE_SUBSCRIPTION_ID` environment variable

== Steps

=== Function triggering

Triggers a function

[source,gherkin]
----
When I trigger funtion `$functionName` from function app `$functionAppName` in resource group `$resourceGroup` with payload:$payload and save response into $scopes variable `$variableNames`
----

* `$functionName` - The name of the function to trigger
* `$functionAppName` - The name of the function app
* `$resourceGroup` - The resource group function relates to
* `$payload` - The JSON payload to send to a function (could be empty)
* `$scopes` - xref:parameters:variable-scope.adoc[The comma-separated set of the variables scopes].
* `$variableName` - The variable name to store results in JSON format. If the variable name is `my-var`, the following variables will be created:
** `${my-var.body}` - The response body
** `${my-var.status-code}` - The HTTP status code
** `${my-var.headers}` - The response headers
** `${my-var.url}` - The request URL

.Trigger function
[source,gherkin]
----
When I trigger function `HttpTrigger1` from function app `vivdus-http-function` in resource group `vividus` with payload:
and save response into scenario variable `functionTrigger`
Then `${functionTrigger.status-code}` is equal to `202`
----
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ include 'vividus-plugin-aws-dynamodb'
include 'vividus-plugin-aws-kinesis'
include 'vividus-plugin-aws-lambda'
include 'vividus-plugin-aws-s3'
include 'vividus-plugin-azure-functions'
include 'vividus-plugin-browserstack'
include 'vividus-plugin-csv'
include 'vividus-plugin-datetime'
Expand Down
19 changes: 19 additions & 0 deletions vividus-plugin-azure-functions/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
project.description = 'Vividus plugin for Azure Functions'

dependencies {
api project(':vividus-bdd-engine')
implementation project(':vividus-soft-assert')
implementation project(':vividus-util')
implementation(group: 'com.azure.resourcemanager', name: 'azure-resourcemanager-appservice', version: '2.2.0')
implementation(group: 'com.azure', name: 'azure-identity', version: '1.3.0-beta.2')

implementation(group: 'org.slf4j', name: 'slf4j-api', version: versions.slf4j)

testImplementation platform(group: 'org.junit', name: 'junit-bom', version: versions.junit)
testImplementation(group: 'org.junit.jupiter', name: 'junit-jupiter')
testImplementation(group: 'org.mockito', name: 'mockito-junit-jupiter', version: versions.mockito)
testImplementation(group: 'com.github.valfirst', name: 'slf4j-test', version: versions.slf4jTest)
}

tasks.artifactoryPublish.enabled = false
tasks.publish.enabled = false
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.vividus.azure.functions.service;

import java.util.Map;

import com.azure.core.credential.TokenCredential;
import com.azure.core.http.policy.HttpLogDetailLevel;
import com.azure.core.management.profile.AzureProfile;
import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.resourcemanager.appservice.AppServiceManager;

public class FunctionService
{
private final AzureProfile azureProfile;

public FunctionService(AzureProfile azureProfile)
{
this.azureProfile = azureProfile;
}

private AppServiceManager createManager(ResponseCapturingHttpPipelinePolicy responseCapturingHttpPipelinePolicy)
{
TokenCredential credential = new DefaultAzureCredentialBuilder()
.authorityHost(azureProfile.getEnvironment().getActiveDirectoryEndpoint()).build();
return AppServiceManager.configure()
.withLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS)
.withPolicy(responseCapturingHttpPipelinePolicy)
.authenticate(credential, azureProfile);
}

public Map<String, Object> triggerFunction(String resourceGroup, String appName, String functionName,
Object payload)
{
ResponseCapturingHttpPipelinePolicy responseCapturingHttpPipelinePolicy =
new ResponseCapturingHttpPipelinePolicy(functionName);
createManager(responseCapturingHttpPipelinePolicy)
ikalinin1 marked this conversation as resolved.
Show resolved Hide resolved
.functionApps()
.getByResourceGroup(resourceGroup, appName)
.triggerFunction(functionName, payload);
return responseCapturingHttpPipelinePolicy.getResponses();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.vividus.azure.functions.service;

import java.util.HashMap;
import java.util.Map;

import com.azure.core.http.HttpPipelineCallContext;
import com.azure.core.http.HttpPipelineNextPolicy;
import com.azure.core.http.HttpResponse;
import com.azure.core.http.policy.HttpPipelinePolicy;

import reactor.core.publisher.Mono;

public class ResponseCapturingHttpPipelinePolicy implements HttpPipelinePolicy
{
private final Map<String, Object> recorded = new HashMap<>();
private final String funcitonName;

public ResponseCapturingHttpPipelinePolicy(String funcitonName)
{
this.funcitonName = funcitonName;
}

public Map<String, Object> getResponses()
{
return new HashMap<>(recorded);
}

@Override
public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next)
{
return next.process().doOnSuccess(this::saveResponse);
}

private void saveResponse(HttpResponse response)
{
String url = response.getRequest().getUrl().toString();
if (url.endsWith(funcitonName))
{
recorded.put("url", url);
recorded.put("status-code", response.getStatusCode());
recorded.put("body", response.getBodyAsString());
recorded.put("headers", response.getHeaders().toMap());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.vividus.azure.functions.steps;

import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.jbehave.core.annotations.When;
import org.vividus.azure.functions.service.FunctionService;
import org.vividus.bdd.context.IBddVariableContext;
import org.vividus.bdd.variable.VariableScope;
import org.vividus.util.json.JsonUtils;

import reactor.core.publisher.Mono;

public class FunctionSteps
{
private final JsonUtils jsonUtils;
private final FunctionService functionsService;
private final IBddVariableContext bddVariableContext;

public FunctionSteps(JsonUtils jsonUtils, FunctionService functionsService, IBddVariableContext bddVariableContext)
{
this.jsonUtils = jsonUtils;
this.functionsService = functionsService;
this.bddVariableContext = bddVariableContext;
}

/**
* Triggers Azure function by its name. And saves response from the service into variable with
* a provided scope and name
* @param functionApp The name of Azure Function App. The value can be retrieved by looking
* at the function in the Azure Portal.
* @param functionName The name of the function to execute. The value can be retrieved by looking
* at the function in the Azure Portal.
* @param resourceGroup Resource group name. Resource group - container that holds related resources
* for an Azure solution. The value can be retrieved by looking
* at the function in the Azure Portal.
* @param payload the JSON that to provide to Function App function as input.
* @param scopes The set (comma separated list of scopes e.g.: STORY, NEXT_BATCHES) of variables scopes<br>
* <i>Available scopes:</i>
* <ul>
* <li><b>STEP</b> - the variable will be available only within the step,
* <li><b>SCENARIO</b> - the variable will be available only within the scenario,
* <li><b>STORY</b> - the variable will be available within the whole story,
* <li><b>NEXT_BATCHES</b> - the variable will be available starting from next batch
* </ul>scopes
* @param variableName The variable name to store result. If the variable name is my-var, the following
* variables will be created:
* <ul>
* <li>${my-var.body} - the response body</li>
* <li>${my-var.status-code} - the HTTP status code is in the 200 range for a successful
* request</li>
* <li>${my-var.headers} - the response headers</li>
* <li>${my-var.url} - the request URL</li>
* </ul>
*/
@SuppressWarnings("unchecked")
@When("I trigger function `$functionName` from function app `$functionAppName` in resource group `$resourceGroup`"
+ " with payload:$payload and save response into $scopes variable `$variableNames`")
public void triggerFunction(String functionName, String functionApp, String resourceGroup, String payload,
Set<VariableScope> scopes, String variableName)
{
Map<String, Object> responses = functionsService.triggerFunction(resourceGroup, functionApp, functionName,
convertPayload(payload));
responses.compute("body", (k, v) -> ((Mono<String>) v).block());
bddVariableContext.putVariable(scopes, variableName, responses);
}

@SuppressWarnings("unchecked")
private Map<String, String> convertPayload(String payload)
{
if (StringUtils.isBlank(payload))
{
return Map.of();
}
return jsonUtils.toObject(payload, Map.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Loggers>
<Logger name="com.azure.core" level="ERROR" additivity="false">
<AppenderRef ref="console" />
<AppenderRef ref="file" />
</Logger>
</Loggers>
</Configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
azure.functions.environment=AZURE
24 changes: 24 additions & 0 deletions vividus-plugin-azure-functions/src/main/resources/spring.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"
default-lazy-init="true">

<bean id="functionSteps" class="org.vividus.azure.functions.steps.FunctionSteps">
<constructor-arg>
<bean class="org.vividus.azure.functions.service.FunctionService">
<constructor-arg>
<bean class="com.azure.core.management.profile.AzureProfile">
<constructor-arg type="com.azure.core.management.AzureEnvironment" value="${azure.functions.environment}" />
</bean>
</constructor-arg>
</bean>
</constructor-arg>
</bean>

<util:list id="stepBeanNames-Azure-Function" value-type="java.lang.String">
<idref bean="functionSteps" />
</util:list>
</beans>
Loading