Skip to content

Commit

Permalink
[plugin-azure-functions] Introduce Azure Functions support (#1497)
Browse files Browse the repository at this point in the history
  • Loading branch information
ikalinin1 authored Mar 23, 2021
1 parent 12bcaf6 commit e990fa8
Show file tree
Hide file tree
Showing 18 changed files with 606 additions and 1 deletion.
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)
.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

0 comments on commit e990fa8

Please sign in to comment.