Skip to content

Commit

Permalink
Annotate base image info in docker v1
Browse files Browse the repository at this point in the history
  • Loading branch information
jcfiorenzano committed Apr 29, 2021
1 parent 923a354 commit 81312f7
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 4 deletions.
26 changes: 26 additions & 0 deletions Tasks/DockerV1/Tests/L0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe('Docker Suite', function() {
delete process.env[shared.TestEnvVars.tagMultipleImages];
delete process.env[shared.TestEnvVars.arguments];
delete process.env[shared.TestEnvVars.qualifySourceImageName];
delete process.env[shared.TestEnvVars.addBaseImageData];
});
after(function () {
});
Expand All @@ -31,6 +32,7 @@ describe('Docker Suite', function() {
let tp = path.join(__dirname, 'TestSetup.js');
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
Expand All @@ -46,6 +48,7 @@ describe('Docker Suite', function() {
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.memoryLimit] = "2GB";
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
Expand All @@ -62,6 +65,7 @@ describe('Docker Suite', function() {
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.imageName] = 'test/Te st:2';
process.env[shared.TestEnvVars.enforceDockerNamingConvention] = 'true';
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
Expand All @@ -78,6 +82,7 @@ describe('Docker Suite', function() {
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.imageName] = 'test/Te st:2';
process.env[shared.TestEnvVars.enforceDockerNamingConvention] = 'false';
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
Expand All @@ -94,6 +99,7 @@ describe('Docker Suite', function() {
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.imageName] = 'test/Test:2';
process.env[shared.TestEnvVars.enforceDockerNamingConvention] = 'true';
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
Expand All @@ -110,6 +116,7 @@ describe('Docker Suite', function() {
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.includeLatestTag] = "true";
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
Expand All @@ -125,6 +132,7 @@ describe('Docker Suite', function() {
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.arguments] = "-t test:testtag";
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
Expand All @@ -140,6 +148,7 @@ describe('Docker Suite', function() {
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.arguments] = "-t test:tag1\n-t test:tag2\n-t test:tag3";
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

assert(tr.invokedToolCount == 1, 'should have invoked tool one times. actual: ' + tr.invokedToolCount);
Expand All @@ -150,6 +159,21 @@ describe('Docker Suite', function() {
done();
});

it('Docker build should add labels with base image info', (done:Mocha.Done) => {
let tp = path.join(__dirname, 'TestSetup.js');
process.env[shared.TestEnvVars.imageName] = "testuser/imagewithannotations:11";
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
tr.run();

assert(tr.invokedToolCount == 3, 'should have invoked tool three time. actual: ' + tr.invokedToolCount);
assert(tr.stderr.length == 0 || tr.errorIssues.length, 'should not have written to stderr');
assert(tr.succeeded, 'task should have succeeded');
assert(tr.stdout.indexOf(`[command]docker build -f ${shared.formatPath("dir1/DockerFile")} --label ${shared.BaseImageLabels.name} --label ${shared.BaseImageLabels.digest} -t testuser/imagewithannotations:11`) != -1, "docker build should run");
console.log(tr.stderr);
done();
});

it('Runs successfully for docker run image', (done:Mocha.Done) => {
let tp = path.join(__dirname, 'TestSetup.js');
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
Expand Down Expand Up @@ -353,6 +377,7 @@ describe('Docker Suite', function() {
let tp = path.join(__dirname, 'TestSetup.js');
let tr : ttm.MockTestRunner = new ttm.MockTestRunner(tp);
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.addBaseImageData] = "false";
process.env[shared.TestEnvVars.containerType] = shared.ContainerTypes.AzureContainerRegistry;
tr.run();

Expand All @@ -370,6 +395,7 @@ describe('Docker Suite', function() {
process.env[shared.TestEnvVars.command] = shared.CommandTypes.buildImage;
process.env[shared.TestEnvVars.containerType] = shared.ContainerTypes.AzureContainerRegistry;
process.env[shared.TestEnvVars.qualifyImageName] = "true";
process.env[shared.TestEnvVars.addBaseImageData] = "false";
tr.run();

//console.log(tr.stdout);
Expand Down
23 changes: 23 additions & 0 deletions Tasks/DockerV1/Tests/TestSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as shared from './TestShared';
const DefaultWorkingDirectory: string = shared.formatPath("a/w");
const ImageNamesPath = shared.formatPath("dir/image_names.txt");
const DockerFilePath = shared.formatPath('dir1/DockerFile');
const Dockerfile: string = `FROM ubuntu\nCMD ["echo","Hello World!"]`

let taskPath = path.join(__dirname, '..', 'container.js');
let tr: tmrm.TaskMockRunner = new tmrm.TaskMockRunner(taskPath);
Expand Down Expand Up @@ -153,6 +154,26 @@ a.exec[`docker images`] = {
"code": 0,
"stdout": "Listed images successfully."
};
a.exec[`docker build -f ${DockerFilePath} --label ${shared.BaseImageLabels.name} --label ${shared.BaseImageLabels.digest} -t testuser/imagewithannotations:11`] = {
"code": 0,
"stdout": "successfully built image and tagged testuser/imagewithannotations:11."
};
a.exec[`docker pull ${shared.BaseImageName}`] = {
"code":0,
"stdout": "Pull complete"
};
a.exec[`docker inspect ${shared.BaseImageName}`] = {
"code":0,
"stdout": `[{
"Id": "sha256:302aba9ce190db9e247d710f4794cc303b169035de2048e76b82c9edbddbef4e",
"RepoTags": [
"alpine:latest"
],
"RepoDigests": [
"ubuntu@sha256:826f70e0ac33e99a72cf20fb0571245a8fee52d68cb26d8bc58e53bfa65dcdfa"
]
}]`
};

tr.setAnswers(<any>a);

Expand All @@ -163,6 +184,8 @@ fsClone.readFileSync = function(filePath, options) {
switch (filePath) {
case ImageNamesPath:
return shared.ImageNamesFileImageName;
case DockerFilePath:
return Dockerfile;
default:
return fs.readFileSync(filePath, options);
}
Expand Down
8 changes: 7 additions & 1 deletion Tasks/DockerV1/Tests/TestShared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export let TestEnvVars = {
pushMultipleImages: "__pushMultipleImages__",
tagMultipleImages: "__tagMultipleImages__",
arguments: "__arguments__",
qualifySourceImageName: "__qualifySourceImageName__"
qualifySourceImageName: "__qualifySourceImageName__",
addBaseImageData: "addBaseImageData"
};

export let OperatingSystems = {
Expand All @@ -34,6 +35,11 @@ export let ContainerTypes = {

export let ImageNamesFileImageName = "test_image";

export let BaseImageName = "ubuntu";
export let BaseImageLabels = {
name:"image.base.ref.name=ubuntu",
digest:"image.base.digest=sha256:826f70e0ac33e99a72cf20fb0571245a8fee52d68cb26d8bc58e53bfa65dcdfa"
};
/**
* Formats the given path to be appropriate for the operating system.
* @param canonicalPath A non-rooted path using a forward slash (/) as a directory separator.
Expand Down
156 changes: 155 additions & 1 deletion Tasks/DockerV1/containerbuild.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use strict";

import * as fs from "fs";
import * as path from "path";
import * as tl from "azure-pipelines-task-lib/task";
import * as dockerCommandUtils from "azure-pipelines-tasks-docker-common-v2/dockercommandutils";
Expand All @@ -10,6 +11,11 @@ import * as sourceUtils from "azure-pipelines-tasks-docker-common-v2/sourceutils
import * as imageUtils from "azure-pipelines-tasks-docker-common-v2/containerimageutils";
import * as utils from "./utils";

interface ImageAnnotations {
BaseImageName: string,
BaseImageDigest: string
}

export function run(connection: ContainerConnection): any {
var command = connection.createCommand();
command.arg("build");
Expand All @@ -21,6 +27,11 @@ export function run(connection: ContainerConnection): any {
throw new Error(tl.loc('ContainerDockerFileNotFound', dockerfilepath));
}

let imageAnnotations: ImageAnnotations = null;
if (isBaseImageLabelAnnotationEnabled()) {
imageAnnotations = GetImageAnnotation(connection, dockerFile);
}

command.arg(["-f", dockerFile]);

var addDefaultLabels = tl.getBoolInput("addDefaultLabels");
Expand All @@ -31,7 +42,20 @@ export function run(connection: ContainerConnection): any {
var commandArguments = dockerCommandUtils.getCommandArguments(tl.getInput("arguments", false));

command.line(commandArguments);

let labelArguments = [];

if (imageAnnotations && imageAnnotations.BaseImageName != "") {
labelArguments.push(`image.base.ref.name=${imageAnnotations.BaseImageName}`)
}

if (imageAnnotations && imageAnnotations.BaseImageDigest != "") {
labelArguments.push(`image.base.digest=${imageAnnotations.BaseImageDigest}`)
}

labelArguments.forEach(label => {
command.arg(["--label", label]);
});

var imageName = utils.getImageName();
var qualifyImageName = tl.getBoolInput("qualifyImageName");
if (qualifyImageName) {
Expand Down Expand Up @@ -68,3 +92,133 @@ export function run(connection: ContainerConnection): any {
command.arg(context);
return connection.execCommand(command);
}

function GetImageAnnotation(connection: ContainerConnection, dockerFilePath: string): ImageAnnotations {
let imageAnnotations: ImageAnnotations = {
BaseImageName: "",
BaseImageDigest: ""
};

imageAnnotations.BaseImageName = getBaseImageName(dockerFilePath);

if (imageAnnotations.BaseImageName && imageAnnotations.BaseImageName != "") {
imageAnnotations.BaseImageDigest = getImageDigest(connection, imageAnnotations.BaseImageName);
}

return imageAnnotations;
}


function getBaseImageName(dockerFilePath: string): string {
// This method takes into consideration multi-stage dockerfiles, it tries to find the final
// base image for the container.

try {
const dockerFileContent = fs.readFileSync(dockerFilePath, 'utf-8');

if (!dockerFileContent || dockerFileContent == "") {
return "";
}

var lines = dockerFileContent.split(/[\r?\n]/);
var tagToImageNameMapping: Map<string, string> = new Map<string, string>();
var baseImage = "";
for (var i = 0; i < lines.length; i++) {
var index = lines[i].toUpperCase().indexOf("FROM");
if (index == -1) {
continue;
}

var nameComponents = lines[i].substring(index + 4).toLowerCase().split(" as ");
var prospectImageName = nameComponents[0].trim()
if (nameComponents.length > 1) {
var tag = nameComponents[1].trim()
if (tagToImageNameMapping.has(prospectImageName)) {
tagToImageNameMapping.set(tag, tagToImageNameMapping.get(prospectImageName));
} else {
tagToImageNameMapping.set(tag, prospectImageName);
}
baseImage = tagToImageNameMapping.get(tag);
} else {
baseImage = tagToImageNameMapping.has(prospectImageName)
? tagToImageNameMapping.get(prospectImageName)
: prospectImageName
}
}
return baseImage.includes("$") // In this case the base image has an argument and we don't know what it is its real value
? ""
: baseImage
} catch (error) {
tl.debug(`An error was found getting the base image for the docker file ${dockerFilePath}. ${error.message}`);
return "";
}
}

function getImageDigest(connection: ContainerConnection, imageName: string,): string {
try {
pullImage(connection, imageName);
let inspectObj = inspectImage(connection, imageName);

if (!inspectObj) {
return "";
}

let repoDigests: string[] = inspectObj.RepoDigests

if (repoDigests.length == 0) {
tl.debug(`No digests where found for image: ${imageName}`);
return "";
}

if (repoDigests.length > 1) {
tl.debug(`Multiple digests where found for image: ${imageName}`);
return "";
}

return repoDigests[0].split("@")[1]
} catch (error) {
tl.debug(`An exception was thrown getting the image digest for ${imageName}, the error was ${error.message}`)
return "";
}
}

function pullImage(connection: ContainerConnection, imageName: string) {
let pullCommand = connection.createCommand();
pullCommand.arg("pull");
pullCommand.arg([imageName]);
let pullResult = pullCommand.execSync();

if (pullResult.stderr && pullResult.stderr != "") {
tl.debug(`An error was found pulling the image ${imageName}, the command output was ${pullResult.stderr}`);
}
}

function inspectImage(connection: ContainerConnection, imageName): any {
let inspectCommand = connection.createCommand();
inspectCommand.arg("inspect");
inspectCommand.arg([imageName]);
let inspectResult = inspectCommand.execSync();

if (inspectResult.stderr && inspectResult.stderr != "") {
tl.debug(`An error was found inspecting the image ${imageName}, the command output was ${inspectResult.stderr}`);
return null;
}

let inspectObj = JSON.parse(inspectResult.stdout);

if (!inspectObj || inspectObj.length == 0) {
tl.debug(`Inspecting the image ${imageName} produced no results.`);
return null;
}

return inspectObj[0]
}

function isBaseImageLabelAnnotationEnabled(): boolean {
const controlVariable = tl.getVariable("addBaseImageData")
if (!controlVariable) {
return true;
}

return controlVariable.toLocaleLowerCase() !== 'false';
}
2 changes: 1 addition & 1 deletion Tasks/DockerV1/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"author": "Microsoft Corporation",
"version": {
"Major": 1,
"Minor": 181,
"Minor": 187,
"Patch": 0
},
"demands": [],
Expand Down
2 changes: 1 addition & 1 deletion Tasks/DockerV1/task.loc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"author": "Microsoft Corporation",
"version": {
"Major": 1,
"Minor": 181,
"Minor": 187,
"Patch": 0
},
"demands": [],
Expand Down

0 comments on commit 81312f7

Please sign in to comment.