From 7ac744a35618b1e03c45dd9a8909f069aac68250 Mon Sep 17 00:00:00 2001 From: Philip Harrison Date: Mon, 26 Jul 2021 15:35:55 +0100 Subject: [PATCH] Extract image pull to a separate service class Extract logic to pull images from the updater class. --- __tests__/updater-integration.test.ts | 36 +++++++++-------- __tests__/updater.test.ts | 5 +-- src/image-service.ts | 38 ++++++++++++++++++ src/main.ts | 12 +++--- src/updater.ts | 58 +++++---------------------- 5 files changed, 78 insertions(+), 71 deletions(-) create mode 100644 src/image-service.ts diff --git a/__tests__/updater-integration.test.ts b/__tests__/updater-integration.test.ts index d8a86ad..1da0fbe 100644 --- a/__tests__/updater-integration.test.ts +++ b/__tests__/updater-integration.test.ts @@ -2,9 +2,10 @@ import Docker from 'dockerode' import fs from 'fs' import path from 'path' import {Updater} from '../src/updater' +import {UPDATER_IMAGE_NAME} from '../src/main' +import {ImageService} from '../src/image-service' describe('Updater', () => { - const docker = new Docker() // To run the js-code itself against API: // const params = { // jobID: 1, @@ -14,7 +15,7 @@ describe('Updater', () => { // } // const client = axios.create({baseURL: params.dependabotAPI}) // const api = new DependabotAPI(client, params) - // const updater = new Updater(docker, api) + // const updater = new Updater(UPDATER_IMAGE_NAME, api) // This stubs out API calls from JS, but will run the updater against an API // running on the specified API endpoint. @@ -28,26 +29,29 @@ describe('Updater', () => { dependabotAPIURL: 'http://host.docker.internal:3001' } } - const updater = new Updater(docker, mockAPIClient) + const updater = new Updater(UPDATER_IMAGE_NAME, mockAPIClient) - beforeAll(() => { - updater.pullImage() + beforeAll(async () => { + await ImageService.pullImage(UPDATER_IMAGE_NAME) }) - afterEach(() => { - docker.listContainers(function (err, containers) { - if (!containers) return + afterEach(async () => { + const docker = new Docker() + const containers = (await docker.listContainers()) || [] - for (const container of containers) { - if ( - container.Image.includes( - 'docker.pkg.github.com/dependabot/dependabot-updater' - ) - ) { - docker.getContainer(container.Id).remove() + for (const container of containers) { + if ( + container.Image.includes( + 'docker.pkg.github.com/dependabot/dependabot-updater' + ) + ) { + try { + await docker.getContainer(container.Id).remove({v: true, force: true}) + } catch (e) { + // ignore } } - }) + } }) jest.setTimeout(20000) diff --git a/__tests__/updater.test.ts b/__tests__/updater.test.ts index 519c709..c3af888 100644 --- a/__tests__/updater.test.ts +++ b/__tests__/updater.test.ts @@ -1,8 +1,7 @@ -import Docker from 'dockerode' +import {UPDATER_IMAGE_NAME} from '../src/main' import {Updater} from '../src/updater' describe('Updater', () => { - const docker = new Docker() const mockAPIClient: any = { getJobDetails: jest.fn(), getCredentials: jest.fn(), @@ -13,7 +12,7 @@ describe('Updater', () => { dependabotAPIURL: 'http://localhost' } } - const updater = new Updater(docker, mockAPIClient) + const updater = new Updater(UPDATER_IMAGE_NAME, mockAPIClient) it('should fetch job details', async () => { mockAPIClient.getJobDetails.mockImplementation(() => { diff --git a/src/image-service.ts b/src/image-service.ts new file mode 100644 index 0000000..d70d2cd --- /dev/null +++ b/src/image-service.ts @@ -0,0 +1,38 @@ +import * as core from '@actions/core' +import Docker from 'dockerode' +import {Readable} from 'stream' + +const endOfStream = async (docker: Docker, stream: Readable): Promise => { + return new Promise((resolve, reject) => { + docker.modem.followProgress(stream, (err: Error) => + err ? reject(err) : resolve(undefined) + ) + }) +} + +export const ImageService = { + /** Fetch the configured updater image, if it isn't already available. */ + async pullImage(imageName: string, force = false): Promise { + const docker = new Docker() + try { + const image = await docker.getImage(imageName).inspect() + if (!force) { + core.info(`Resolved ${imageName} to existing ${image.Id}`) + return + } // else fallthrough to pull + } catch (e) { + if (!e.message.includes('no such image')) { + throw e + } // else fallthrough to pull + } + + core.info(`Pulling image ${imageName}...`) + const auth = { + username: process.env.GITHUB_PKG_USER, + password: process.env.GITHUB_PKG_TOKEN + } + const stream = await docker.pull(imageName, {authconfig: auth}) + await endOfStream(docker, stream) + core.info(`Pulled image ${imageName}`) + } +} diff --git a/src/main.ts b/src/main.ts index f3431e1..aea4512 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,14 @@ import * as core from '@actions/core' import * as github from '@actions/github' import {getJobParameters} from './inputs' -import Docker from 'dockerode' +import {ImageService} from './image-service' import {Updater} from './updater' import {APIClient} from './api-client' import axios from 'axios' +export const UPDATER_IMAGE_NAME = + 'docker.pkg.github.com/dependabot/dependabot-updater:latest' + async function run(): Promise { try { // Decode JobParameters: @@ -16,11 +19,10 @@ async function run(): Promise { core.setSecret(params.jobToken) core.setSecret(params.credentialsToken) - const docker = new Docker() const client = axios.create({baseURL: params.dependabotAPIURL}) - const api = new APIClient(client, params) - const updater = new Updater(docker, api) - await updater.pullImage() + const apiClient = new APIClient(client, params) + const updater = new Updater(UPDATER_IMAGE_NAME, apiClient) + await ImageService.pullImage(UPDATER_IMAGE_NAME) await updater.runUpdater() } catch (error) { diff --git a/src/updater.ts b/src/updater.ts index c83ee71..0441dee 100644 --- a/src/updater.ts +++ b/src/updater.ts @@ -1,60 +1,26 @@ import * as core from '@actions/core' -import * as Docker from 'dockerode' +import Docker, {Container} from 'dockerode' import path from 'path' import fs from 'fs' import {Credential, JobDetails, APIClient} from './api-client' -import {Readable} from 'stream' import {pack} from 'tar-stream' const JOB_INPUT_FILENAME = 'job.json' const JOB_INPUT_PATH = `/home/dependabot/dependabot-updater` - const JOB_OUTPUT_FILENAME = 'output.json' const JOB_OUTPUT_PATH = '/home/dependabot/dependabot-updater/output' const REPO_CONTENTS_PATH = '/home/dependabot/dependabot-updater/repo' -const DEFAULT_UPDATER_IMAGE = - 'docker.pkg.github.com/dependabot/dependabot-updater:latest' const decode = (str: string): string => Buffer.from(str, 'base64').toString('binary') export class Updater { + docker: Docker constructor( - private readonly docker: Docker, - private readonly apiClient: APIClient, - private readonly updaterImage = DEFAULT_UPDATER_IMAGE - ) {} - - /** Fetch the configured updater image, if it isn't already available. */ - async pullImage(force = false): Promise { - try { - const image = await this.docker.getImage(this.updaterImage).inspect() - if (!force) { - core.info(`Resolved ${this.updaterImage} to existing ${image.Id}`) - return - } // else fallthrough to pull - } catch (e) { - if (!e.message.includes('no such image')) { - throw e - } // else fallthrough to pull - } - - core.info(`Pulling image ${this.updaterImage}...`) - const auth = { - username: process.env.GITHUB_PKG_USER, - password: process.env.GITHUB_PKG_TOKEN - } - const stream = await this.docker.pull(this.updaterImage, {authconfig: auth}) - await this.endOfStream(stream) - core.info(`Pulled image ${this.updaterImage}`) - } - - private async endOfStream(stream: Readable): Promise { - return new Promise((resolve, reject) => { - this.docker.modem.followProgress(stream, (err: Error) => - err ? reject(err) : resolve(undefined) - ) - }) + private readonly updaterImage: string, + private readonly apiClient: APIClient + ) { + this.docker = new Docker() } /** @@ -65,7 +31,7 @@ export class Updater { const details = await this.apiClient.getJobDetails() const credentials = await this.apiClient.getCredentials() // TODO: once the proxy is set up, remove credentials from the job details - details['credentials'] = credentials + details.credentials = credentials const files = await this.runFileFetcher(details, credentials) if (!files) { @@ -121,7 +87,7 @@ export class Updater { details: JobDetails, files: FetchedFiles ): Promise { - core.info(`running update ${details.id} ${files}`) + core.info(`Running update job ${this.apiClient.params.jobID}`) const container = await this.createContainer('update_files') const containerInput: FileUpdaterInput = { base_commit_sha: files.base_commit_sha, @@ -133,9 +99,7 @@ export class Updater { await this.runContainer(container) } - private async createContainer( - updaterCommand: string - ): Promise { + private async createContainer(updaterCommand: string): Promise { const container = await this.docker.createContainer({ Image: this.updaterImage, AttachStdout: true, @@ -162,7 +126,7 @@ export class Updater { } private async storeContainerInput( - container: Docker.Container, + container: Container, input: FileFetcherInput | FileUpdaterInput ): Promise { const tar = pack() @@ -171,7 +135,7 @@ export class Updater { await container.putArchive(tar, {path: JOB_INPUT_PATH}) } - private async runContainer(container: Docker.Container): Promise { + private async runContainer(container: Container): Promise { try { await container.start() const stream = await container.attach({