Skip to content

Commit da23258

Browse files
authored
[Spring Apps] Add support for the custom container. (#17040)
1 parent 196e4b6 commit da23258

File tree

10 files changed

+570
-46
lines changed

10 files changed

+570
-46
lines changed

Tasks/AzureSpringCloudV0/Strings/resources.resjson/en-US/resources.resjson

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"loc.input.help.AzureSpringCloud": "Select the Azure Spring Cloud service to which to deploy.",
1313
"loc.input.label.AppName": "App",
1414
"loc.input.help.AppName": "Select the Azure Spring Cloud app to deploy.",
15+
"loc.input.label.DeploymentType": "Deployment Type",
16+
"loc.input.help.DeploymentType": "To deploy with source code or Java package, choose \"Artifacts\"; To deploy with container image, choose \"Custom Container\"",
1517
"loc.input.label.UseStagingDeployment": "Use Staging Deployment",
1618
"loc.input.help.UseStagingDeployment": "Automatically select the deployment that's set as Staging at the time the task runs.",
1719
"loc.input.label.CreateNewDeployment": "Create a new staging deployment if one does not exist.",
@@ -20,6 +22,21 @@
2022
"loc.input.help.DeploymentName": "The deployment to which this task will apply. Lowercase letters and numbers only; must start with a letter.",
2123
"loc.input.label.Package": "Package or folder",
2224
"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.",
25+
"loc.input.label.Builder": "Builder",
26+
"loc.input.help.Builder": "Select a builder of VMware Tanzu® Build Service™, this can be used in enterprise tier. <br/> For detailed description, please check [Use Tanzu Build Service](https://docs.microsoft.com/en-us/azure/spring-cloud/how-to-enterprise-build-service?tabs=azure-portal).",
27+
"loc.input.label.RegistryServer": "Registry Server",
28+
"loc.input.help.RegistryServer": "The registry of the container image. Default: docker.io.",
29+
"loc.input.label.RegistryUsername": "Registry Username",
30+
"loc.input.help.RegistryUsername": "The username of the container registry.",
31+
"loc.input.label.RegistryPassword": "Registry Password",
32+
"loc.input.help.RegistryPassword": "The password of the container registry.",
33+
"loc.input.label.ImageName": "Image Name and Tag",
34+
"loc.input.help.ImageName": "The container image tag.",
35+
"loc.input.label.ImageCommand": "Image Command",
36+
"loc.input.help.ImageCommand": "The command of the container image.",
37+
"loc.input.label.ImageArgs": "Image Arguments",
38+
"loc.input.help.ImageArgs": "The arguments of the container image.",
39+
"loc.input.label.ImageLanguageFramework": "Language Framework",
2340
"loc.input.label.EnvironmentVariables": "Environment Variables",
2441
"loc.input.help.EnvironmentVariables": "Edit the app's environment variables.",
2542
"loc.input.label.JvmOptions": "JVM Options",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as path from 'path';
2+
import assert = require('assert');
3+
import * as ttm from 'azure-pipelines-task-lib/mock-test';
4+
import tmrm = require('azure-pipelines-task-lib/mock-run');
5+
import { printTaskInputs } from './mock_utils';
6+
7+
export class DeploymentCustomImageToStagingSucceeds {
8+
9+
private static mockTaskInputParameters() {
10+
//Just use this to set the environment variables before any of the pipeline SDK code runs.
11+
//The actual TaskMockRunner instance is irrelevant as inputs are set as environment variables,
12+
//visible to the whole process. If we do this in the L0 file, it doesn't work.
13+
//Otherwise, it doesn't work.
14+
let tr : tmrm.TaskMockRunner = new tmrm.TaskMockRunner('dummypath');
15+
console.log('Setting mock inputs for DeploymentCustomImageToStagingSucceedsL0');
16+
tr.setInput('ConnectedServiceName', "AzureRM");
17+
tr.setInput('Action', 'Deploy');
18+
tr.setInput('AzureSpringCloud', 'DeploymentCustomImageToStagingSucceedsL0');
19+
tr.setInput('AppName', 'testcontainerapp');
20+
tr.setInput('DeploymentType', 'CustomContainer');
21+
tr.setInput('UseStagingDeployment', "true");
22+
tr.setInput('RegistryUsername', 'username');
23+
tr.setInput('RegistryPassword', 'password');
24+
tr.setInput('ImageName', 'azurespringcloudtesting/byoc-it-springboot:v1');
25+
tr.setInput('ImageCommand', 'java');
26+
tr.setInput('ImageArgs', "-jar /app.jar");
27+
tr.setInput('ImageLanguageFramework', 'springboot');
28+
tr.setInput('EnvironmentVariables', '-key1 val1 -key2 "val 2"');
29+
printTaskInputs();
30+
}
31+
32+
public static mochaTest = (done: Mocha.Done) => {
33+
34+
DeploymentCustomImageToStagingSucceeds.mockTaskInputParameters();
35+
let testPath = path.join(__dirname, 'DeploymentCustomImageToStagingSucceedsL0.js');
36+
let mockTestRunner: ttm.MockTestRunner = new ttm.MockTestRunner(testPath);
37+
try {
38+
mockTestRunner.run();
39+
assert.deepEqual(mockTestRunner.errorIssues, [], 'No error output expected in a successful deployment');
40+
assert(mockTestRunner.succeeded);
41+
done();
42+
}
43+
catch (error) {
44+
console.error(error);
45+
done(error);
46+
}
47+
};
48+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import * as path from 'path';
2+
import tmrm = require('azure-pipelines-task-lib/mock-run');
3+
import { setEndpointData, setAgentsData, mockTaskArgument, nock, MOCK_SUBSCRIPTION_ID, mockAzureSpringCloudExists, mockCommonAzureAPIs, API_VERSION } from './mock_utils';
4+
import { ASC_RESOURCE_TYPE, MOCK_RESOURCE_GROUP_NAME } from './mock_utils'
5+
import assert = require('assert');
6+
7+
8+
const MOCK_DEPLOYMENT_STATUS_ENDPOINT = `/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${MOCK_RESOURCE_GROUP_NAME}/providers/Microsoft.AppPlatform/locations/eastus2/operationStatus/default/operationId/mockoperationid?api-version=${API_VERSION}`
9+
10+
export class DeploymentCustomImageToStagingSucceedsL0 {
11+
12+
static readonly TEST_NAME = 'DeploymentCustomImageToStagingSucceedsL0';
13+
static readonly MOCK_APP_NAME = 'testcontainerapp';
14+
15+
16+
public static startTest() {
17+
console.log(`running ${this.TEST_NAME}`);
18+
let taskPath = path.join(__dirname, '..', 'azurespringclouddeployment.js');
19+
let taskMockRunner: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
20+
setEndpointData();
21+
setAgentsData();
22+
mockCommonAzureAPIs();
23+
mockAzureSpringCloudExists(this.TEST_NAME);
24+
this.mockTwoDeployments();
25+
let nockScope = this.mockDeploymentApis();
26+
27+
taskMockRunner.setAnswers(mockTaskArgument());
28+
taskMockRunner.run();
29+
}
30+
31+
/**
32+
* Simulate a deployment list API that returns a production deployment and a staging deployment.
33+
*/
34+
private static mockTwoDeployments() {
35+
nock('https://management.azure.com', {
36+
reqheaders: {
37+
"authorization": "Bearer DUMMY_ACCESS_TOKEN",
38+
"content-type": "application/json; charset=utf-8",
39+
"user-agent": "TFS_useragent"
40+
}
41+
}).get(`/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/${ASC_RESOURCE_TYPE}/${this.TEST_NAME}/apps/${this.MOCK_APP_NAME}/deployments?api-version=${API_VERSION}`)
42+
.reply(200, {
43+
"value": [
44+
{
45+
"id": `/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/${ASC_RESOURCE_TYPE}/${this.TEST_NAME}/apps/${this.MOCK_APP_NAME}/deployments/default`,
46+
"name": "default",
47+
"properties": {
48+
"active": true,
49+
"appName": this.MOCK_APP_NAME,
50+
"deploymentSettings": {
51+
"cpu": 1,
52+
"environmentVariables": null,
53+
"memoryInGB": 1,
54+
"runtimeVersion": "Java_8"
55+
},
56+
"instances": [
57+
{
58+
"discoveryStatus": "UP",
59+
"name": `${this.MOCK_APP_NAME}-default-7-7b77f5b6f5-fff9t`,
60+
"startTime": "2021-03-13T01:39:20Z",
61+
"status": "Running"
62+
}
63+
],
64+
"provisioningState": "Succeeded",
65+
"source": {
66+
"relativePath": "<default>",
67+
"type": "Jar"
68+
},
69+
"status": "Running"
70+
},
71+
"resourceGroup": MOCK_RESOURCE_GROUP_NAME,
72+
"sku": {
73+
"capacity": 1,
74+
"name": "S0",
75+
"tier": "Standard"
76+
},
77+
"type": `${ASC_RESOURCE_TYPE}/apps/deployments`
78+
},
79+
{
80+
"id": `/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/${ASC_RESOURCE_TYPE}/${this.TEST_NAME}/apps/${this.MOCK_APP_NAME}/deployments/theOtherOne`,
81+
"name": "theOtherOne",
82+
"properties": {
83+
"active": false,
84+
"appName": this.MOCK_APP_NAME,
85+
"deploymentSettings": {
86+
"cpu": 1,
87+
"environmentVariables": null,
88+
"memoryInGB": 1,
89+
"runtimeVersion": "Java_8"
90+
},
91+
"instances": [
92+
{
93+
"discoveryStatus": "UP",
94+
"name": `${this.MOCK_APP_NAME}-theOtherOne-7-7b77f5b6f5-90210`,
95+
"startTime": "2021-03-13T01:39:20Z",
96+
"status": "Running"
97+
}
98+
],
99+
"provisioningState": "Succeeded",
100+
"source": {
101+
"relativePath": "<default>",
102+
"type": "Jar"
103+
},
104+
"status": "Running"
105+
},
106+
"resourceGroup": MOCK_RESOURCE_GROUP_NAME,
107+
"sku": {
108+
"capacity": 1,
109+
"name": "S0",
110+
"tier": "Standard"
111+
},
112+
"type": `${ASC_RESOURCE_TYPE}/apps/deployments`
113+
}]
114+
115+
}).persist();
116+
}
117+
118+
/** Simulate APIs invoked as part of deployment */
119+
private static mockDeploymentApis() {
120+
//mock get resource upload URL
121+
nock('https://management.azure.com', {
122+
reqheaders: {
123+
"authorization": "Bearer DUMMY_ACCESS_TOKEN",
124+
"content-type": "application/json; charset=utf-8",
125+
"user-agent": "TFS_useragent"
126+
}
127+
})
128+
// mock listTestKeys
129+
.post(`/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${MOCK_RESOURCE_GROUP_NAME}/providers/${ASC_RESOURCE_TYPE}/${this.TEST_NAME}/listTestKeys?api-version=${API_VERSION}`)
130+
.once()
131+
.reply(200,
132+
{
133+
"primaryKey": "mockPrimaryKey",
134+
"secondaryKey": "mockSecondaryKey",
135+
"primaryTestEndpoint": `https://primary:mockPrimaryKey@${this.MOCK_APP_NAME}.test.azuremicroservices.io`,
136+
"secondaryTestEndpoint": `https://secondary:mockSecondaryKey@${this.MOCK_APP_NAME}.test.azuremicroservices.io`,
137+
"enabled": true
138+
}
139+
)
140+
141+
// Mock the deployment update API:
142+
143+
.patch(`/subscriptions/${MOCK_SUBSCRIPTION_ID}/resourceGroups/${encodeURIComponent(MOCK_RESOURCE_GROUP_NAME)}/providers/${ASC_RESOURCE_TYPE}/${this.TEST_NAME}/apps/${this.MOCK_APP_NAME}/deployments/theOtherOne?api-version=${API_VERSION}`)
144+
.once()
145+
.reply((uri, serializedRequestBody) => {
146+
let requestBody = JSON.parse(serializedRequestBody);
147+
assert.strictEqual(requestBody.properties.source.type, 'Container');
148+
assert.strictEqual(requestBody.properties.source.customerContainer.containerImage, 'azurespringcloudtesting/byoc-it-springboot:v1');
149+
let responseBody = {
150+
"provisioningState": "Updating"
151+
}
152+
let returnHeaders = {
153+
'azure-asyncoperation': 'https://management.azure.com' + MOCK_DEPLOYMENT_STATUS_ENDPOINT
154+
}
155+
return [202, responseBody, returnHeaders];
156+
157+
})
158+
159+
// Mock the operation status URL
160+
.get(MOCK_DEPLOYMENT_STATUS_ENDPOINT)
161+
.once()
162+
.reply(200, {
163+
status: "Completed"
164+
})
165+
166+
.persist();
167+
168+
}
169+
}

Tasks/AzureSpringCloudV0/Tests/L0.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { SetProductionNamedDeploymentSucceeds } from './SetProductionNamedDeploy
1212
import { DeleteStagingDeploymentTest } from './DeleteStagingDeploymentTest';
1313
import { DeploymentToStagingSucceedsWithBuildService } from './DeploymentToStagingSucceedsWithBuildService';
1414
import { DeploymentFailsWhenBuilderNotExist } from './DeploymentFailsWhenBuilderNotExist';
15+
import { DeploymentCustomImageToStagingSucceeds } from './DeploymentCustomImageToStagingSucceeds'
1516

1617
describe('Azure Spring Cloud deployment Suite', function () {
1718
afterEach(() => {
@@ -34,6 +35,9 @@ describe('Azure Spring Cloud deployment Suite', function () {
3435
it('Correctly deploys to a current staging deployment with build service', DeploymentToStagingSucceedsWithBuildService.mochaTest);
3536
it('Correctly errors out when the builder resource does not exist', DeploymentFailsWhenBuilderNotExist.mochaTest);
3637

38+
/*************** Deployment with Custom image ***************/
39+
it('Correctly deploys custom image to a current staging deployment', DeploymentCustomImageToStagingSucceeds.mochaTest);
40+
3741
/*************** Set Production Deployment tests ************/
3842
it('Correctly errors out when "Use Staging Deployment" is set but no such deployment exists', SetProductionUseStagingFailsWithNoStaging.mochaTest);
3943
it('Deploys correctly to a staging deployment when "Use Staging Deployment is set', SetProductionUseStagingSucceeds.mochaTest);

Tasks/AzureSpringCloudV0/azurespringclouddeployment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,6 @@ process.on('unhandledRejection', ((error: Error) => {
3131

3232

3333
main().catch((error: Error) => {
34-
tl.error("Deployment Failed with Error: " + error.message);
34+
tl.error("Deployment Failed with Error: " + JSON.stringify(error));
3535
tl.setResult(tl.TaskResult.Failed, error.message);
3636
});

0 commit comments

Comments
 (0)