Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: infer package manager from container exec and set package as an interface #237

Merged
merged 25 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
1e8bb6a
feat: support extra args
musdotdigital Nov 29, 2023
8e00c25
feat: drop dockerfile dep, rely on manifest
musdotdigital Nov 29, 2023
c545389
feat: support ubuntu image
musdotdigital Nov 29, 2023
dc79823
update workflow
musdotdigital Nov 29, 2023
98b4dfe
type fix in dependencies.ts
musdotdigital Nov 29, 2023
68fc31b
fix: tsc dependencies.ts
musdotdigital Nov 29, 2023
89c8544
fix: update dist
musdotdigital Nov 29, 2023
828d5a7
throw errors if not finding correct fields
musdotdigital Nov 29, 2023
0353186
fix: update dist
musdotdigital Nov 29, 2023
5cdfce0
fix: prevent field overwrite
musdotdigital Nov 29, 2023
be7ace4
fix: handle multiple colon
musdotdigital Nov 29, 2023
47a2fc3
rely on dockerfil but support multiple args and ubuntu
musdotdigital Nov 29, 2023
f4723d1
cuda dockerfile test
musdotdigital Nov 29, 2023
a441092
drop delete logic
musdotdigital Nov 30, 2023
ec07312
feat: pkg as interface
musdotdigital Nov 30, 2023
30e74f2
fix: drop dep change
musdotdigital Nov 30, 2023
cf748d2
feat: find pck manager
musdotdigital Nov 30, 2023
ee8f8ec
fix: dist
musdotdigital Nov 30, 2023
6ebd52c
update tests
musdotdigital Nov 30, 2023
13cdecb
feat: drop init in main and seq through pkg managers
musdotdigital Nov 30, 2023
cff1066
feat: test for unsupported pkg manager
musdotdigital Nov 30, 2023
02a21e6
fix: drop unsupported pkg test
musdotdigital Nov 30, 2023
e4eabb4
unsupported pkg manager test
musdotdigital Dec 1, 2023
c7a75f7
package manager tests
musdotdigital Dec 1, 2023
a94e9cd
return dep.json
musdotdigital Dec 4, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
musdotdigital marked this conversation as resolved.
Show resolved Hide resolved
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