Skip to content

Commit

Permalink
Publishing Test Details from CST task to Metadata store. (#11502)
Browse files Browse the repository at this point in the history
* Publishing Test Details from CST task to Metadata store.

* Fixing the payload.

* using the right note name

* Refactoring and addressing the PR comments

* nit changes.
  • Loading branch information
navin22 authored and leantk committed Dec 23, 2019
1 parent 0877074 commit 358a2bc
Show file tree
Hide file tree
Showing 10 changed files with 486 additions and 148 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"loc.friendlyName": "Container Structure Test",
"loc.helpMarkDown": "[Learn more about this task](https://go.microsoft.com/fwlink/?LinkID=613742)",
"loc.helpMarkDown": "[Learn more about this task](https://aka.ms/containerstructuretest)",
"loc.description": "Uses container-structure-test (https://github.com/GoogleContainerTools/container-structure-test) to validate the structure of an image based on four categories of tests - command tests, file existence tests, file content tests and metadata tests",
"loc.instanceNameFormat": "Container Structure Test $(testFile)",
"loc.group.displayName.containerRepository": "Container Repository",
Expand All @@ -12,6 +12,8 @@
"loc.input.help.tag": "The tag is used in pulling the image from docker registry service connection",
"loc.input.label.configFile": "Config file path",
"loc.input.help.configFile": "Config files path, that contains container structure tests. Either .yaml or .json files",
"loc.input.label.testRunTitle": "Test run title",
"loc.input.help.testRunTitle": "Provide a name for the Test Run.",
"loc.input.label.failTaskOnFailedTests": "Fail task if there are test failures",
"loc.input.help.failTaskOnFailedTests": "Fail the task if there are any test failures. Check this option to fail the task if test failures are detected.",
"loc.messages.NoMatchingFilesFound": "No test files matching '%s' were found.",
Expand Down
32 changes: 32 additions & 0 deletions Tasks/ContainerStructureTestV0/containerregistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import RegistryAuthenticationToken from "docker-common-v2/registryauthenticationprovider/registryauthenticationtoken";
import ContainerConnection from "docker-common-v2/containerconnection";
import { getDockerRegistryEndpointAuthenticationToken } from "docker-common-v2/registryauthenticationprovider/registryauthenticationtoken";
import * as dockerCommandUtils from "docker-common-v2/dockercommandutils";


export class ContainerRegistry {
constructor(registryConnection: string) {
let registryAuthenticationToken: RegistryAuthenticationToken = getDockerRegistryEndpointAuthenticationToken(registryConnection);
this.connection = new ContainerConnection();
this.connection.open(null, registryAuthenticationToken, true, false);
}

public getQualifiedImageName(repository: string, tag: string){
return `${this.connection.getQualifiedImageName(repository, true)}:${tag}`
}

public async pull(repository: string, tag: string): Promise<any> {
return new Promise<any>((resolve, reject) => {
try {
const imageName = this.getQualifiedImageName(repository, tag);
dockerCommandUtils.command(this.connection, "pull", imageName, (output: any) => {
resolve(output);
})
} catch (error) {
reject(error);
}
});
}

private connection: ContainerConnection;
}
186 changes: 39 additions & 147 deletions Tasks/ContainerStructureTestV0/containerstructuretest.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,73 @@
import * as tl from 'azure-pipelines-task-lib/task';
import { chmodSync, writeFileSync, existsSync } from 'fs';
import * as path from "path";
import downloadutility = require("utility-common/downloadutility");
import RegistryAuthenticationToken from "docker-common-v2/registryauthenticationprovider/registryauthenticationtoken";
import ContainerConnection from "docker-common-v2/containerconnection";
import { getDockerRegistryEndpointAuthenticationToken } from "docker-common-v2/registryauthenticationprovider/registryauthenticationtoken";
import * as dockerCommandUtils from "docker-common-v2/dockercommandutils";
let uuid = require('uuid');

interface TestSummary {
"Total": number;
"Pass": number;
"Fail": number;
"Results": TestResult[];

}

interface TestResult {
"Name": string;
"Pass": boolean;
"Errors": string[] | undefined;
}
import { WebResponse } from 'utility-common-v2/restutilities';
import { ContainerRegistry } from "./containerregistry";
import { TestResultPublisher, TestSummary } from "./testresultspublisher";
import { TestRunner } from "./testrunner";

const telemetryArea: string = 'TestExecution';
const telemetryFeature: string = 'PublishTestResultsTask';
const telemetryData: { [key: string]: any; } = <{ [key: string]: any; }>{};

const defaultRunTitlePrefix: string = 'ContainerStructureTest_TestResults_';
const buildString = "build";
const hostType = tl.getVariable("System.HostType").toLowerCase();
const isBuild = hostType === buildString;
const osType = tl.osType().toLowerCase();

async function run() {
let taskResult = true;
try {
const osType = tl.osType().toLowerCase();
tl.debug(`Os Type: ${osType}`);
telemetryData["OsType"] = osType;

if(osType == "windows_nt") {
throw new Error(tl.loc('NotSupportedOS', osType));
}

const artifactId = isBuild ? parseInt(tl.getVariable("Build.BuildId")) : parseInt(tl.getVariable("Release.ReleaseId"));
const testFilePath = tl.getInput('configFile', true);
const repository = tl.getInput('repository', true);
const testRunTitleInput = tl.getInput('testRunTitle');
const endpointId = tl.getInput("dockerRegistryServiceConnection");
let tagInput = tl.getInput('tag');
const tag = tagInput ? tagInput : "latest";

const image = `${repository}:${tag}`;
let endpointId = tl.getInput("dockerRegistryServiceConnection");
const testRunTitle = testRunTitleInput ? testRunTitleInput : `${defaultRunTitlePrefix}${artifactId}`;
const failTaskOnFailedTests: boolean = tl.getInput('failTaskOnFailedTests').toLowerCase() == 'true' ? true : false;

tl.setResourcePath(path.join(__dirname, 'task.json'));
let registryAuthenticationToken: RegistryAuthenticationToken = getDockerRegistryEndpointAuthenticationToken(endpointId);
let connection = new ContainerConnection();

connection.open(null, registryAuthenticationToken, true, false);
// Establishing registry connection and pulling the container.
let containerRegistry = new ContainerRegistry(endpointId);
tl.debug(`Successfully finished docker login`);

await dockerPull(connection, image);
const image = `${containerRegistry.getQualifiedImageName(repository, tag)}`;
tl.debug(`Image: ${image}`)
await containerRegistry.pull(repository, tag);
tl.debug(`Successfully finished docker pull`);

const downloadUrl = getContainerStructureTestRunnerDownloadPath(osType);
if (!downloadUrl) {
return;
}

tl.debug(`Successfully downloaded : ${downloadUrl}`);
const runnerPath = await downloadTestRunner(downloadUrl);
const output: string = runContainerStructureTest(runnerPath, testFilePath, image);
// Running the container structure test on the above pulled container.
const testRunner = new TestRunner(testFilePath, image);
let resultObj: TestSummary = await testRunner.Run();

if (!output || output.length <= 0) {
throw new Error("No output from runner");
// Publishing the test results to TCM.
// Not failing task if there are any errors while publishing.
let testResultPublisher = new TestResultPublisher();
try {
testResultPublisher.publishToTcm(resultObj, testRunTitle);
telemetryData["TCMPublishStatus"] = true;
tl.debug("Finished publishing the test results to TCM");
} catch(error) {
telemetryData["TCMPublishError"] = error;
}

tl.debug(`Successfully finished testing`);
let resultObj: TestSummary = JSON.parse(output);
tl.debug(`Total Tests: ${resultObj.Total}, Pass: ${resultObj.Pass}, Fail: ${resultObj.Fail}`);

publishTheTestResultsToTCM(output);
publishTestResultsToMetadataStore(resultObj);
// Publishing the test results to Metadata Store.
try {
var response:WebResponse = await testResultPublisher.publishToMetaDataStore(resultObj, image);
console.log(`Publishing test data to metadata store. Status: ${response.statusCode} and Message : ${response.statusMessage}`)
tl.debug(`Response from publishing the test details to MetaData store: ${JSON.stringify(response)}`);
telemetryData["MetaDataPublishStatus"] = true;
} catch(error) {
telemetryData["MetaDataPublishError"] = error;
}

if (failTaskOnFailedTests && resultObj && resultObj.Fail > 0) {
taskResult = false;
Expand All @@ -92,106 +84,6 @@ async function run() {
}
}

async function dockerPull(connection: ContainerConnection, imageName: string): Promise<any> {
return new Promise<any>((resolve, reject) => {
try {
dockerCommandUtils.command(connection, "pull", imageName, (output: any) => {
resolve(output);
})
} catch (error) {
reject(error);
}
});
}

function createResultsFile(fileContent: string): string {
let resultFilePath: string = null;
try {
const agentTempDirectory = tl.getVariable('Agent.TempDirectory');
resultFilePath = path.join(agentTempDirectory, uuid.v1() + '.json');

writeFileSync(resultFilePath, fileContent);
} catch (ex) {
tl.warning(`Exception while creating results file: ${ex}`);
return null;
}

return resultFilePath;
}

function getContainerStructureTestRunnerDownloadPath(osType: string): string {
switch (osType) {
case 'darwin':
return "https://storage.googleapis.com/container-structure-test/latest/container-structure-test-darwin-amd64";
case 'linux':
return "https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64";
default:
return null;
}
}

async function downloadTestRunner(downloadUrl: string): Promise<string> {
const gcst = path.join(__dirname, "container-structure-test");
return downloadutility.download(downloadUrl, gcst, false, true).then((res) => {
chmodSync(gcst, "777");
if (!existsSync(gcst)) {
tl.error(tl.loc('FileNotFoundException', path));
throw new Error(tl.loc('FileNotFoundException', path));
}
telemetryData["DownloadStatus"] = true;
return gcst;
}).catch((reason) => {
telemetryData["DownloadStatus"] = false;
telemetryData["DownloadError"] = reason;
tl.error(tl.loc('DownloadException', reason));
throw new Error(tl.loc('DownloadException', reason));
})
}

function runContainerStructureTest(runnerPath: string, testFilePath: string, image: string): string {
let command = tl.tool(runnerPath);
command.arg(["test", "--image", image, "--config", testFilePath, "--json"]);

const output = command.execSync();
let jsonOutput: string;

if (!output.error) {
jsonOutput = output.stdout;
} else {
tl.error(tl.loc('ErrorInExecutingCommand', output.error));
throw new Error(tl.loc('ErrorInExecutingCommand', output.error));
}

tl.debug("Standard Output: " + output.stdout);
tl.debug("Standard Error: " + output.stderr);
tl.debug("Error from command executor: " + output.error);
tl.debug("Return code from command executor: " + output.code);

return jsonOutput;
}

function publishTheTestResultsToTCM(jsonResutlsString: string) {
let resultsFile = createResultsFile(jsonResutlsString);

if (!resultsFile) {
tl.warning("Unable to create the resutls file, hence not publishg the test results");
return;
}

var properties = <{ [key: string]: string }>{};
properties['type'] = "ContainerStructure";
properties['mergeResults'] = "false";
properties['runTitle'] = "Container Structure Tests";
properties['resultFiles'] = resultsFile;
properties['testRunSystem'] = "VSTS-PTR";

tl.command('results.publish', properties, '');
telemetryData["TCMPublishStatus"] = true;
}

function publishTestResultsToMetadataStore(testSummary: TestSummary) {
}

function publishTelemetry() {
try {
console.log(`##vso[telemetry.publish area=${telemetryArea};feature=${telemetryFeature}]${JSON.stringify(telemetryData)}`);
Expand Down
6 changes: 6 additions & 0 deletions Tasks/ContainerStructureTestV0/make.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
"type": "node",
"dest": "./",
"compile": true
},
{
"module": "../Common/utility-common-v2",
"type": "node",
"dest" : "./",
"compile" : true
}
]
}
70 changes: 70 additions & 0 deletions Tasks/ContainerStructureTestV0/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Tasks/ContainerStructureTestV0/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"azure-pipelines-task-lib": "2.8.0",
"docker-common-v2": "file:../../_build/Tasks/Common/docker-common-v2-1.0.0.tgz",
"utility-common": "file:../../_build/Tasks/Common/utility-common-1.0.2.tgz",
"utility-common-v2": "file:../../_build/Tasks/Common/utility-common-v2-2.0.0.tgz",
"uuid": "^3.0.1"
}
}
Loading

0 comments on commit 358a2bc

Please sign in to comment.