Skip to content

Commit 3118ccb

Browse files
authored
Adding Azure Spring Cloud task (#14563) (#14678)
* Initial commit as task builds * Initial commit as task builds * UI Datasources work * Get deployment URL works * Now getting the correct SAS upload URL * Jar deployment without wildcards works * Fixing capitalization * Adding wildcard support * JVM options now work * Inline datasources work! * Environment variable parsing now works properly * Adding set active deployment * Set active deployment now works, fixed display names * Refactoring into single task, adding deletion and test endpoint retrieval * Runtime version setting now works * Deployment tasks now waits for completion * Spring Cloud and app names can now be entered. Resource ID or Spring Cloud Name are accepted * Enabling steeltoe and source directory * Build log now displayed on source deployments. Source uploads now actually work * Working around the task library's mock dependency * First L0 test passes, fixed bug where absent staging deployment did not error out properly with "use staging" * First test works, interference with subsequent tests * Refactoring tests * Fixed issue with premature reading of environment variables * Two tests now pass * Added unit test for deployment info parsing, found and fixed bug in deployment info parsing * Renaming tests for more clarity * End-to-end deployment with mock added and works * Refactoring to split up deployment update request generation from application * Renaming TargetInactive parameter to UseStagingDeployment * Adding test for 'set production' when no staging deployment exists * Adding tests for set production deployment * Setting single error for when named staging deployment does not exist in Set Production whether or not there's room for a new one * Adding tests for deploying to named deployment successfully * Adding tests to delete staging deployment * Externalization/localization of strings * Refactoring deployment actions to have separate methods * Reducing deployment name to single field. Fixing bug where re-creating a deployment that has already existed returns status code 202, not 201. * Applying code review notes * Space fix * Setting task version to reflect spring * Fixing API version that prevents listing of Azure Spring Cloud instances * Adding Main Entry Path for .NET Core deployments * Fix for potential path traversal vulnerability and unit test thereof * Adding missing package error messages. * Using the standard key-value parsing library instead of custom code. Formatting fixes. Eliminating log outputs from tests. * Removing out.tar.gz * Removing surviving test log dumps * Formatting, task version update, CODEOWNERS and make options * Making myself sole CODEOWNER on Azure Spring Cloud task. 😱 * Removing 'postDeploymentScript' from make.json
1 parent d8c39b2 commit 3118ccb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+5450
-0
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ Tasks/AzureRmWebAppDeploymentV3/ @nadesu @chshrikh @pipeline-environment
7777

7878
Tasks/AzureRmWebAppDeploymentV4/ @nadesu @chshrikh @pipeline-environment-team
7979

80+
Tasks/AzureSpringCloudV0 @yevster
81+
8082
Tasks/AzureStaticWebAppV0/ @miwebst @brhopcra
8183

8284
Tasks/AzureWebAppV1/ @nadesu @chshrikh @pipeline-environment-team
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"loc.friendlyName": "Azure Spring Cloud",
3+
"loc.helpMarkDown": "[Learn more about this task](https://aka.ms/azurespringcloud)",
4+
"loc.description": "Deploy applications to Azure Spring Cloud and manage deployments.",
5+
"loc.instanceNameFormat": "Azure Spring Cloud $(Action): $(AppName)",
6+
"loc.group.displayName.ApplicationAndConfigurationSettings": "Application and Configuration Settings",
7+
"loc.input.label.ConnectedServiceName": "Azure subscription",
8+
"loc.input.help.ConnectedServiceName": "Select the Azure Resource Manager subscription for the deployment.",
9+
"loc.input.label.Action": "Action",
10+
"loc.input.help.Action": "Action to be performed on Azure Spring Cloud.",
11+
"loc.input.label.AzureSpringCloud": "Azure Spring Cloud Name",
12+
"loc.input.help.AzureSpringCloud": "Select the Azure Spring Cloud service to which to deploy.",
13+
"loc.input.label.AppName": "App",
14+
"loc.input.help.AppName": "Select the Azure Spring Cloud app to deploy.",
15+
"loc.input.label.UseStagingDeployment": "Use Staging Deployment",
16+
"loc.input.help.UseStagingDeployment": "Automatically select the deployment that's set as Staging at the time the task runs.",
17+
"loc.input.label.CreateNewDeployment": "Create a new staging deployment if one does not exist.",
18+
"loc.input.help.CreateNewDeployment": "Whether to target the deployment that's set as Staging at the time of execution. If unchecked, the 'Deployment Name' setting must be set.",
19+
"loc.input.label.DeploymentName": "Deployment",
20+
"loc.input.help.DeploymentName": "The deployment to which this task will apply. Lowercase letters and numbers only; must start with a letter.",
21+
"loc.input.label.Package": "Package or folder",
22+
"loc.input.help.Package": "File path to the package or a folder containing the Spring Cloud app contents.<br />Variables ( [Build](https://docs.microsoft.com/vsts/pipelines/build/variables) | [Release](https://docs.microsoft.com/vsts/pipelines/release/variables#default-variables)), wildcards are supported. <br/> For example, $(System.DefaultWorkingDirectory)/\\*\\*/\\*.jar.",
23+
"loc.input.label.EnvironmentVariables": "Environment Variables",
24+
"loc.input.help.EnvironmentVariables": "Edit the app's environment variables.",
25+
"loc.input.label.JvmOptions": "JVM Options",
26+
"loc.input.help.JvmOptions": "Edit the app's JVM options. A String containing JVM Options. Example: `-Xms1024m -Xmx2048m`",
27+
"loc.input.label.RuntimeVersion": "Runtime Version",
28+
"loc.input.help.RuntimeVersion": "The runtime on which the app will run.",
29+
"loc.input.label.DotNetCoreMainEntryPath": "Main Entry Path",
30+
"loc.input.help.DotNetCoreMainEntryPath": "The path to the .NET executable relative to zip root.",
31+
"loc.input.label.Version": "Version",
32+
"loc.messages.CompressingSourceDirectory": "Compressing source directory %s to %s",
33+
"loc.messages.DeploymentLog": "Deployment Log",
34+
"loc.messages.StatusCode": "Status Code",
35+
"loc.messages.StartingUploadOf": "Starting upload of %s",
36+
"loc.messages.CompletedUploadOf": "Upload of %s completed.",
37+
"loc.messages.ResourceDoesntExist": "Resource '%s' doesn't exist. Resource should exist before deployment.",
38+
"loc.messages.UnableToGetDeploymentUrl": "Unable to get deployment URL for Azure Spring Cloud '%s', Error: '%s'",
39+
"loc.messages.DeploymentDoesntExist": "Deployment with name '%s' does not exist. Unable to proceed.",
40+
"loc.messages.StagingDeploymentWithNameDoesntExist": "Staging deployment with name %s does not exist. Unable to proceed.",
41+
"loc.messages.NoStagingDeploymentFound": "No staging deployment found.",
42+
"loc.messages.TwoDeploymentsAlreadyExistCannotCreate": "Deployment with name '%s' does not exist and cannot be created, as two deployments already exist.",
43+
"loc.messages.UnsupportedSourceType": "Unsupported source type for %s",
44+
"loc.messages.UnknownOrUnsupportedAction": "Unknown or unsupported action: %s",
45+
"loc.messages.OperationTimedOut": "Operation timed out.",
46+
"loc.messages.OperationFailed": "Operation failed: %s %s",
47+
"loc.messages.UnableToDeleteDeployment": "Unable to delete deployment.",
48+
"loc.messages.PrivateTestEndpointNotEnabled": "Private test endpoint is not enabled.",
49+
"loc.messages.UnableToRetrieveTestEndpointKeys": "Unable to retrieve test endpoint keys",
50+
"loc.messages.NoDeploymentsExist": "No deployments exist.",
51+
"loc.messages.UnableToGetDeploymentInformation": "Unable to get deployment information.",
52+
"loc.messages.DuplicateAzureSpringCloudName": "Illegal state: multiple Azure Spring Cloud instances with same name.",
53+
"loc.messages.InvalidAzureSpringCloudResourceId": "Invalid azure spring cloud resource identifier: %s",
54+
"loc.messages.Nopackagefoundwithspecifiedpattern": "No package found with specified pattern: %s. Check if the package mentioned in the task is published as an artifact in the build or a previous stage and downloaded in the current job.",
55+
"loc.messages.MorethanonepackagematchedwithspecifiedpatternPleaserestrainthesearchpattern": "More than one package matched with specified pattern: %s. Please restrain the search pattern."
56+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import assert = require('assert');
2+
import { AzureSpringCloudDeploymentProvider } from "../deploymentProvider/AzureSpringCloudDeploymentProvider"
3+
import { TaskParameters } from "../operations/taskparameters"
4+
5+
6+
export class AzureSpringCloudUnitTests {
7+
8+
public static pathTraversalAttackTest = (done: Mocha.Done) => {
9+
const resourceIdWithPathAttack = '/subscriptions/mocksubid/resourceGroups/mockresouorcegroup/providers/Microsoft.AppPlatform/Spring/authorized-name/../unauthorized-name';
10+
const taskParameters: TaskParameters = {
11+
AzureSpringCloud: resourceIdWithPathAttack,
12+
AppName: 'appName',
13+
UseStagingDeployment: false,
14+
Action: 'Deploy'
15+
};
16+
17+
const provider = new AzureSpringCloudDeploymentProvider(taskParameters);
18+
provider.PreDeploymentStep().then(() => {
19+
done(assert.fail('Attempted path traversal attack should have failed'));
20+
}).catch(error => {
21+
assert.strictEqual(error.message, `loc_mock_InvalidAzureSpringCloudResourceId ${resourceIdWithPathAttack}`);
22+
done();
23+
});
24+
}
25+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
2+
import { AzureEndpoint } from "azure-pipelines-tasks-azure-arm-rest-v2/azureModels";
3+
import { getMockEndpoint, nock } from '../node_modules/azure-pipelines-tasks-azure-arm-rest-v2/Tests/mock_utils';
4+
import { MOCK_RESOURCE_GROUP_NAME } from "./mock_utils";
5+
import assert = require('assert');
6+
7+
export class AzureSpringCloudUnitTests {
8+
9+
static readonly AZURE_ENDPOINT: AzureEndpoint = getMockEndpoint();
10+
11+
/**
12+
* Tests that deployment names are parsed correctly from API output.
13+
*/
14+
public static testDeploymentNameRetrieval = (done: Mocha.Done) => {
15+
let azureSpringCloudName = 'testDeploymentNameRetrieval';
16+
let appName = 'testapp';
17+
let azureSpringCloud = AzureSpringCloudUnitTests.newAzureSpringCloud(azureSpringCloudName);
18+
AzureSpringCloudUnitTests.mockDeploymentListApiWithTwoDeployments(azureSpringCloudName, appName);
19+
let expectedDeploymentNames = ['default', 'theOtherOne'];
20+
azureSpringCloud.getAllDeploymentNames(appName)
21+
.then(foundDeploymentNames => {
22+
assert.deepStrictEqual(foundDeploymentNames, expectedDeploymentNames);
23+
done();
24+
})
25+
.catch(error => done(error));
26+
}
27+
28+
/** Prepares an instance of the AzureSpringCloudWrapper with a mock endpoint */
29+
private static newAzureSpringCloud(name: string) {
30+
let asc = require('../deploymentProvider/azure-arm-spring-cloud');
31+
let azureSpringCloud = new asc.AzureSpringCloud(this.AZURE_ENDPOINT, `/subscriptions/${this.AZURE_ENDPOINT.subscriptionID}/resourceGroups/${MOCK_RESOURCE_GROUP_NAME}/providers/Microsoft.AppPlatform/Spring/${name}`)
32+
return azureSpringCloud;
33+
}
34+
35+
private static mockDeploymentListApiWithTwoDeployments(azureSpringCloudName: string, appName: string) {
36+
console.log('mockDeploymentListApiWithTwoDeployments');
37+
38+
nock('https://management.azure.com').get(`/subscriptions/${this.AZURE_ENDPOINT.subscriptionID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/Microsoft.AppPlatform/Spring/${azureSpringCloudName}/apps/${appName}/deployments?api-version=2020-07-01`)
39+
.reply(200, {
40+
"value": [
41+
{
42+
"id": `/subscriptions/${this.AZURE_ENDPOINT.subscriptionID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/Microsoft.AppPlatform/Spring/${azureSpringCloudName}/apps/${appName}/deployments/default`,
43+
"name": "default",
44+
"properties": {
45+
"active": true,
46+
"appName": appName,
47+
"deploymentSettings": {
48+
"cpu": 1,
49+
"environmentVariables": null,
50+
"memoryInGB": 1,
51+
"runtimeVersion": "Java_8"
52+
},
53+
"instances": [
54+
{
55+
"discoveryStatus": "UP",
56+
"name": `${appName}-default-7-7b77f5b6f5-fff9t`,
57+
"startTime": "2021-03-13T01:39:20Z",
58+
"status": "Running"
59+
}
60+
],
61+
"provisioningState": "Succeeded",
62+
"source": {
63+
"relativePath": "<default>",
64+
"type": "Jar"
65+
},
66+
"status": "Running"
67+
},
68+
"resourceGroup": MOCK_RESOURCE_GROUP_NAME,
69+
"sku": {
70+
"capacity": 1,
71+
"name": "S0",
72+
"tier": "Standard"
73+
},
74+
"type": `providers/Microsoft.AppPlatform/Spring/apps/deployments`
75+
},
76+
{
77+
"id": `/subscriptions/${this.AZURE_ENDPOINT.subscriptionID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/Microsoft.AppPlatform/Spring/${azureSpringCloudName}/apps/${appName}/deployments/theOtherOne`,
78+
"name": "theOtherOne",
79+
"properties": {
80+
"active": true,
81+
"appName": appName,
82+
"deploymentSettings": {
83+
"cpu": 1,
84+
"environmentVariables": null,
85+
"memoryInGB": 1,
86+
"runtimeVersion": "Java_8"
87+
},
88+
"instances": [
89+
{
90+
"discoveryStatus": "UP",
91+
"name": `${appName}-theOtherOne-7-7b77f5b6f5-90210`,
92+
"startTime": "2021-03-13T01:39:20Z",
93+
"status": "Running"
94+
}
95+
],
96+
"provisioningState": "Succeeded",
97+
"source": {
98+
"relativePath": "<default>",
99+
"type": "Jar"
100+
},
101+
"status": "Running"
102+
},
103+
"resourceGroup": MOCK_RESOURCE_GROUP_NAME,
104+
"sku": {
105+
"capacity": 1,
106+
"name": "S0",
107+
"tier": "Standard"
108+
},
109+
"type": 'Microsoft.AppPlatform/Spring/apps/deployments'
110+
}
111+
]
112+
113+
}).persist();
114+
}
115+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import * as path from 'path';
2+
import assert = require('assert');
3+
import mocha = require('mocha');
4+
import * as ttm from 'azure-pipelines-task-lib/mock-test';
5+
import tmrm = require('azure-pipelines-task-lib/mock-run');
6+
import { printTaskInputs } from './mock_utils';
7+
8+
export class CreateNamedDeploymentFailsDeploymentDoesNotAlreadyExist {
9+
10+
private static mockTaskInputParameters() {
11+
//Just use this to set the environment variables before any of the pipeline SDK code runs.
12+
//The actual TaskMockRunner instance is irrelevant as inputs are set as environment variables,
13+
//visible to the whole process. If we do this in the L0 file, it doesn't work.
14+
//Otherwise, it doesn't work.
15+
let tr = new tmrm.TaskMockRunner('dummypath');
16+
tr.setInput('ConnectedServiceName', "AzureRM");
17+
tr.setInput('Action', 'Deploy');
18+
tr.setInput('AppName', 'testapp');
19+
tr.setInput('AzureSpringCloud', 'CreateNamedDeploymentFailsWhenTwoDeploymentsExistL0');
20+
tr.setInput('UseStagingDeployment', "false");
21+
tr.setInput('Package', 'dummy.jar');
22+
tr.setInput('RuntimeVersion', 'Java_11');
23+
tr.setInput('CreateNewDeployment', "false");
24+
tr.setInput('DeploymentName', 'nonexistentDeployment');
25+
printTaskInputs();
26+
}
27+
28+
public static mochaTest = (done: mocha.Done) => {
29+
30+
let taskPath = path.join(__dirname, 'CreateNamedDeploymentFailsDeploymentDoesNotAlreadyExistL0.js');
31+
let mockTestRunner: ttm.MockTestRunner = new ttm.MockTestRunner(taskPath);
32+
CreateNamedDeploymentFailsDeploymentDoesNotAlreadyExist.mockTaskInputParameters();
33+
try {
34+
mockTestRunner.run();
35+
assert(mockTestRunner.failed);
36+
let expectedError = 'loc_mock_DeploymentDoesntExist nonexistentDeployment';
37+
assert(mockTestRunner.errorIssues.length > 0 || mockTestRunner.stderr.length > 0, 'should have written to stderr');
38+
assert(mockTestRunner.stdErrContained(expectedError) || mockTestRunner.createdErrorIssue(expectedError), 'E should have said: ' + expectedError);
39+
done();
40+
}
41+
catch (error) {
42+
done(error);
43+
}
44+
};
45+
46+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import * as path from 'path';
2+
import tmrm = require('azure-pipelines-task-lib/mock-run');
3+
import { setEndpointData, setAgentsData, mockTaskArgument, mockCommonAzureAPIs, nock, mockAzureSpringCloudExists } from './mock_utils';
4+
import { ASC_RESOURCE_TYPE, MOCK_RESOURCE_GROUP_NAME, MOCK_SUBSCRIPTION_ID } from './mock_utils'
5+
6+
const TEST_APP_NAME = 'testapp';
7+
8+
export class CreateNamedDeploymentFailsDeploymentDoesNotAlreadyExistL0 {
9+
10+
private static readonly TEST_NAME = 'CreateNamedDeploymentFailsWhenTwoDeploymentsExistL0';
11+
12+
public static startTest() {
13+
console.log(`running ${this.TEST_NAME}`);
14+
let taskPath = path.join(__dirname, '..', 'azurespringclouddeployment.js');
15+
let taskMockRunner: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
16+
setEndpointData();
17+
setAgentsData();
18+
mockCommonAzureAPIs();
19+
mockAzureSpringCloudExists(this.TEST_NAME);
20+
this.mockDeploymentListApiWithTwoDeployments();
21+
taskMockRunner.setAnswers(mockTaskArgument());
22+
taskMockRunner.run();
23+
}
24+
25+
/**
26+
* Simulate a deployment list API that returns a single Production deployment.
27+
*/
28+
private static mockDeploymentListApiWithTwoDeployments() {
29+
console.log('mockDeploymentListApiWithTwoDeployments');
30+
console.log('defining endpoint ' + `/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/${ASC_RESOURCE_TYPE}/${this.TEST_NAME}/apps/${TEST_APP_NAME}/deployments?api-version=2020-07-01`);
31+
nock('https://management.azure.com', {
32+
reqheaders: {
33+
"authorization": "Bearer DUMMY_ACCESS_TOKEN",
34+
"content-type": "application/json; charset=utf-8",
35+
"user-agent": "TFS_useragent"
36+
}
37+
}).get(`/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/${ASC_RESOURCE_TYPE}/${this.TEST_NAME}/apps/${TEST_APP_NAME}/deployments?api-version=2020-07-01`)
38+
.reply(200, {
39+
"value": [
40+
{
41+
"id": `/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/${ASC_RESOURCE_TYPE}/${this.TEST_NAME}/apps/${TEST_APP_NAME}/deployments/default`,
42+
"name": "default",
43+
"properties": {
44+
"active": true,
45+
"appName": TEST_APP_NAME,
46+
"deploymentSettings": {
47+
"cpu": 1,
48+
"environmentVariables": null,
49+
"memoryInGB": 1,
50+
"runtimeVersion": "Java_8"
51+
},
52+
"instances": [
53+
{
54+
"discoveryStatus": "UP",
55+
"name": `${TEST_APP_NAME}-default-7-7b77f5b6f5-fff9t`,
56+
"startTime": "2021-03-13T01:39:20Z",
57+
"status": "Running"
58+
}
59+
],
60+
"provisioningState": "Succeeded",
61+
"source": {
62+
"relativePath": "<default>",
63+
"type": "Jar"
64+
},
65+
"status": "Running"
66+
},
67+
"resourceGroup": MOCK_RESOURCE_GROUP_NAME,
68+
"sku": {
69+
"capacity": 1,
70+
"name": "S0",
71+
"tier": "Standard"
72+
},
73+
"type": `${ASC_RESOURCE_TYPE}/apps/deployments`
74+
}]
75+
}).persist();
76+
}
77+
}
78+
79+
CreateNamedDeploymentFailsDeploymentDoesNotAlreadyExistL0.startTest();

0 commit comments

Comments
 (0)