Skip to content

Commit

Permalink
feat: infer package manager from container exec and set package as an…
Browse files Browse the repository at this point in the history
… interface (#237)

* feat: support extra args

* feat: drop dockerfile dep, rely on manifest

* feat: support ubuntu image

* update workflow

* type fix in dependencies.ts

* fix: tsc dependencies.ts

* fix: update dist

* throw errors if not finding correct fields

* fix: update dist

* fix: prevent field overwrite

* fix: handle multiple colon

* rely on dockerfil but support multiple args and ubuntu

* cuda dockerfile test

* drop delete logic

* feat: pkg as interface

* fix: drop dep change

* feat: find pck manager

* fix: dist

* update tests

* feat: drop init in main and seq through pkg managers

* feat: test for unsupported pkg manager

* fix: drop unsupported pkg test

* unsupported pkg manager test

* package manager tests

* return dep.json
  • Loading branch information
musdotdigital authored Dec 4, 2023
1 parent b3ccf2a commit a9335dd
Show file tree
Hide file tree
Showing 15 changed files with 142 additions and 136 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ jobs:
- uses: actions/checkout@v3
- uses: ./
with:
dockerfile: ./__tests__/data/Dockerfile
dockerfile: ./__tests__/data/Dockerfile.apk
dependencies: ./__tests__/data/dependencies.json
File renamed without changes.
1 change: 1 addition & 0 deletions __tests__/data/Dockerfile.apt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM ubuntu:latest
1 change: 1 addition & 0 deletions __tests__/data/Dockerfile.unsupported
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FROM busybox:latest
3 changes: 0 additions & 3 deletions __tests__/data/InvalidDockerfile

This file was deleted.

3 changes: 0 additions & 3 deletions __tests__/data/debianDockerfile

This file was deleted.

7 changes: 4 additions & 3 deletions __tests__/dependencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {mkdtemp} from 'fs/promises'
import * as path from 'path'

test('save dependencies', async () => {
const pkg = new Package('test', 'latest')
const pkg = {name: 'test', version: 'latest', extra: 'field'}
const tmpdir = await mkdtemp(path.join(os.tmpdir(), 'test-save-dep-'))
const outPath = path.join(tmpdir, 'deps.json')
save(outPath, [pkg])
Expand All @@ -15,7 +15,8 @@ test('save dependencies', async () => {
const expected = `[
{
"name": "test",
"version": "latest"
"version": "latest",
"extra": "field"
}
]`
expect(content).toBe(expected)
Expand All @@ -24,7 +25,7 @@ test('save dependencies', async () => {
test('save and load dependencies', async () => {
const tmpdir = await mkdtemp(path.join(os.tmpdir(), 'test-load-dep'))
const depPath = path.join(tmpdir, 'deps.json')
const pkg = new Package('test', 'latest')
const pkg = {name: 'test', version: 'latest', extra: 'field'}

save(depPath, [pkg])
const packages = load(depPath)
Expand Down
34 changes: 16 additions & 18 deletions __tests__/dockerfile.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import {test, expect} from '@jest/globals'
import * as path from 'path'
import {load} from '../src/dockerfile'
import {AlpineImage, DebImage} from '../src/image'

test('load invalid dockerfile', () => {
let dockerfilePath = path.join(__dirname, 'data', 'InvalidDockerfile')
function loadInvalid() {
load(dockerfilePath)
}
expect(loadInvalid).toThrowError('Unable to extract image from Dockerfile')
})

test('load alpine dockerfile', () => {
const dockerfilePath = path.join(__dirname, 'data', 'Dockerfile')
const dockerfile = load(dockerfilePath)
expect(dockerfile).toBeInstanceOf(AlpineImage)
test('load alpine dockerfile', async () => {
const dockerfilePath = path.join(__dirname, 'data', 'Dockerfile.apk')
const dockerfile = await load(dockerfilePath)
expect(dockerfile.pkgManager).toBe('apk')
expect(dockerfile.name).toBe('alpine:latest')
})

test('load debian dockerfile', () => {
const dockerfilePath = path.join(__dirname, 'data', 'debianDockerfile')
const dockerfile = load(dockerfilePath)
expect(dockerfile).toBeInstanceOf(DebImage)
expect(dockerfile.name).toBe('debian:bullseye-slim')
test('load ubuntu dockerfile', async () => {
const dockerfilePath = path.join(__dirname, 'data', 'Dockerfile.apt')
const dockerfile = await load(dockerfilePath)
expect(dockerfile.pkgManager).toBe('apt')
expect(dockerfile.name).toBe('ubuntu:latest')
})

test('load dockerfile with unsupported package manager', async () => {
const dockerfilePath = path.join(__dirname, 'data', 'Dockerfile.unsupported')
await expect(load(dockerfilePath)).rejects.toThrow(
'Unable to find supported package manager'
)
}, 10000)
8 changes: 6 additions & 2 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import * as process from 'process'
import * as cp from 'child_process'
import * as path from 'path'
import {test, expect} from '@jest/globals'
import {test} from '@jest/globals'

// shows how the runner will run a javascript action with env / stdout protocol
test('test runs', () => {
process.env['INPUT_DOCKERFILE'] = path.join(__dirname, 'data', 'Dockerfile')
process.env['INPUT_DOCKERFILE'] = path.join(
__dirname,
'data',
'Dockerfile.apk'
)
process.env['INPUT_DEPENDENCIES'] = path.join(
__dirname,
'data',
Expand Down
111 changes: 60 additions & 51 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.Package = exports.save = exports.load = void 0;
exports.save = exports.load = void 0;
const fs_1 = __importDefault(__nccwpck_require__(147));
function load(dependencies_path) {
const content = fs_1.default.readFileSync(dependencies_path).toString('utf-8');
const jsonContent = JSON.parse(content);
return packages_from_dict(jsonContent);
return JSON.parse(content);
}
exports.load = load;
function save(dependencies_path, dependencies) {
const jsonContent = JSON.stringify(dependencies, null, 2);
fs_1.default.writeFileSync(dependencies_path, jsonContent);
}
exports.save = save;
class Package {
constructor(name, version) {
this.name = name;
this.version = version;
}
}
exports.Package = Package;
function packages_from_dict(dict) {
const packages = [];
for (const storedPackage of dict) {
packages.push(new Package(storedPackage.name, storedPackage.version));
}
return packages;
}


/***/ }),
Expand Down Expand Up @@ -69,6 +54,15 @@ var __importStar = (this && this.__importStar) || function (mod) {
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Expand All @@ -77,8 +71,17 @@ exports.load = void 0;
const image = __importStar(__nccwpck_require__(281));
const fs_1 = __importDefault(__nccwpck_require__(147));
function load(dockerfile) {
const content = fs_1.default.readFileSync(dockerfile).toString('utf-8');
return extract_docker_image(content);
return __awaiter(this, void 0, void 0, function* () {
const content = fs_1.default.readFileSync(dockerfile).toString('utf-8');
const extractedImage = extract_docker_image(content);
try {
yield extractedImage.init_package_manager();
}
catch (error) {
return Promise.reject(error);
}
return extractedImage;
});
}
exports.load = load;
function extract_docker_image(dockerfile_content) {
Expand All @@ -88,11 +91,8 @@ function extract_docker_image(dockerfile_content) {
if (line.includes('FROM')) {
imageName = line.split(' ')[1].trim();
}
if (line.includes('apk add') || line.includes('apt-get install')) {
return image.factory(imageName);
}
}
throw Error('Unable to extract image from Dockerfile');
return new image.Image(imageName);
}


Expand All @@ -113,63 +113,72 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.factory = exports.DebImage = exports.AlpineImage = exports.Image = void 0;
exports.Image = void 0;
const docker_cli_js_1 = __nccwpck_require__(771);
const dependencies_1 = __nccwpck_require__(31);
const packageManagers = [
{ command: 'apk --version', name: 'apk' },
{ command: 'apt-get --version', name: 'apt' }
];
class Image {
constructor(name) {
this.name = name;
this.pkgManager = null;
const options = new docker_cli_js_1.Options(undefined, undefined, false, undefined, undefined);
this.docker = new docker_cli_js_1.Docker(options);
}
get_latest_version(installed_package) {
init_package_manager() {
return __awaiter(this, void 0, void 0, function* () {
throw Error(`Not implemented can't get latest version of ${installed_package}`);
for (const manager of packageManagers) {
try {
yield this.docker.command(`run ${this.name} sh -c "${manager.command} > /dev/null"`);
this.pkgManager = manager.name;
return;
}
catch (error) {
// Continue to the next iteration if the current one fails
}
}
throw Error('Unable to find supported package manager');
});
}
}
exports.Image = Image;
class AlpineImage extends Image {
get_latest_version(installed_package) {
return __awaiter(this, void 0, void 0, function* () {
switch (this.pkgManager) {
case 'apk':
return this.get_latest_version_apk(installed_package);
case 'apt':
return this.get_latest_version_apt(installed_package);
default:
throw Error('Unable to get package manager');
}
});
}
get_latest_version_apk(installed_package) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.docker.command(`run ${this.name} sh -c "apk update > /dev/null && apk info ${installed_package.name}"`);
const updated_version = remove_prefix(response.raw.split(' ')[0], `${installed_package.name}-`);
return new dependencies_1.Package(installed_package.name, updated_version);
return Object.assign(Object.assign({}, installed_package), { version: updated_version });
});
}
}
exports.AlpineImage = AlpineImage;
class DebImage extends Image {
get_latest_version(installed_package) {
get_latest_version_apt(installed_package) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield this.docker.command(`run ${this.name} sh -c "apt-get update > /dev/null && apt-cache policy ${installed_package.name}"`);
let updated_version = undefined;
for (const info of response.raw.split('\n')) {
if (info.includes('Candidate')) {
updated_version = info.split(':')[1].trim();
// must handle case of multiple : in the line i.e. Candidate: 1:8.9p1-3ubuntu0.4
updated_version = info.split(':').slice(1).join(':').trim();
break;
}
}
if (updated_version !== undefined) {
return new dependencies_1.Package(installed_package.name, updated_version);
return Object.assign(Object.assign({}, installed_package), { version: updated_version });
}
throw Error('Unable to extract new version from package infos');
});
}
}
exports.DebImage = DebImage;
function factory(name) {
if (name.includes('alpine')) {
return new AlpineImage(name);
}
if (name.includes('debian') ||
name.includes('bulleye') ||
name.includes('buster')) {
return new DebImage(name);
}
throw Error('Unsupported image type');
}
exports.factory = factory;
exports.Image = Image;
function remove_prefix(text, prefix) {
if (text.startsWith(prefix)) {
return text.substring(prefix.length);
Expand Down Expand Up @@ -227,7 +236,7 @@ function run() {
const dockerfile_path = core.getInput('dockerfile');
const dependencies_path = core.getInput('dependencies');
const apply = core.getBooleanInput('apply');
const image = dockerfile.load(dockerfile_path);
const image = yield dockerfile.load(dockerfile_path);
const dependencies_info = dependencies.load(dependencies_path);
const packages_update = dependencies_info.map(function (installed_pkg) {
return __awaiter(this, void 0, void 0, function* () {
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

23 changes: 2 additions & 21 deletions src/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,15 @@ import fs from 'fs'

export function load(dependencies_path: string): Package[] {
const content = fs.readFileSync(dependencies_path).toString('utf-8')
const jsonContent = JSON.parse(content)
return packages_from_dict(jsonContent)
return JSON.parse(content)
}

export function save(dependencies_path: string, dependencies: Package[]): void {
const jsonContent = JSON.stringify(dependencies, null, 2)
fs.writeFileSync(dependencies_path, jsonContent)
}

interface StoredJSON {
export interface Package {
name: string
version: string
}

export class Package {
name: string
version: string

constructor(name: string, version: string) {
this.name = name
this.version = version
}
}

function packages_from_dict(dict: StoredJSON[]): Package[] {
const packages: Package[] = []
for (const storedPackage of dict) {
packages.push(new Package(storedPackage.name, storedPackage.version))
}
return packages
}
15 changes: 9 additions & 6 deletions src/dockerfile.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import * as image from './image'
import fs from 'fs'

export function load(dockerfile: string): image.Image {
export async function load(dockerfile: string): Promise<image.Image> {
const content = fs.readFileSync(dockerfile).toString('utf-8')
return extract_docker_image(content)
const extractedImage = extract_docker_image(content)
try {
await extractedImage.init_package_manager()
} catch (error) {
return Promise.reject(error)
}
return extractedImage
}

function extract_docker_image(dockerfile_content: string): image.Image {
Expand All @@ -13,9 +19,6 @@ function extract_docker_image(dockerfile_content: string): image.Image {
if (line.includes('FROM')) {
imageName = line.split(' ')[1].trim()
}
if (line.includes('apk add') || line.includes('apt-get install')) {
return image.factory(imageName)
}
}
throw Error('Unable to extract image from Dockerfile')
return new image.Image(imageName)
}
Loading

0 comments on commit a9335dd

Please sign in to comment.