From d3ea155f5115a690b6c3e65ae523ed44cbf1fe5e Mon Sep 17 00:00:00 2001 From: norbjd Date: Fri, 26 Aug 2022 17:30:09 +0200 Subject: [PATCH 1/3] During container build, authenticate to registry to pull private images --- deploy/lib/buildAndPushContainers.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/deploy/lib/buildAndPushContainers.js b/deploy/lib/buildAndPushContainers.js index ad21a25b..19dae930 100644 --- a/deploy/lib/buildAndPushContainers.js +++ b/deploy/lib/buildAndPushContainers.js @@ -16,12 +16,26 @@ const promisifyStream = (stream, verbose) => new Promise((resolve, reject) => { }); module.exports = { - buildAndPushContainers() { + async buildAndPushContainers() { + // used for pushing const auth = { username: 'any', password: this.provider.scwToken, }; + // used for building: see https://docs.docker.com/engine/api/v1.37/#tag/Image/operation/ImageBuild + const registryAuth = {}; + registryAuth['rg.' + this.provider.scwRegion + '.scw.cloud'] = { + username: 'any', + password: this.provider.scwToken, + }; + + try { + await docker.checkAuth(registryAuth); + } catch (err) { + throw err; + } + const containerNames = Object.keys(this.containers); const promises = containerNames.map((containerName) => { const container = this.containers[containerName]; @@ -31,7 +45,7 @@ module.exports = { this.serverless.cli.log(`Building and pushing container ${container.name} to: ${imageName} ...`); return new Promise(async (resolve, reject) => { - const buildStream = await docker.buildImage(tarStream, { t: imageName }) + const buildStream = await docker.buildImage(tarStream, { t: imageName, registryconfig: registryAuth }) await promisifyStream(buildStream, this.provider.options.verbose); const image = docker.getImage(imageName) From 6a74e202b66c518edbbac67936555eff4fc9336a Mon Sep 17 00:00:00 2001 From: norbjd Date: Thu, 1 Sep 2022 15:19:27 +0200 Subject: [PATCH 2/3] Review --- deploy/lib/buildAndPushContainers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/lib/buildAndPushContainers.js b/deploy/lib/buildAndPushContainers.js index 19dae930..5ccaf1d5 100644 --- a/deploy/lib/buildAndPushContainers.js +++ b/deploy/lib/buildAndPushContainers.js @@ -33,7 +33,7 @@ module.exports = { try { await docker.checkAuth(registryAuth); } catch (err) { - throw err; + throw new Error(`Authentication to registry failed`); } const containerNames = Object.keys(this.containers); From b83bac04f99b88df32c1d52a686184a0f49f91f6 Mon Sep 17 00:00:00 2001 From: norbjd Date: Thu, 1 Sep 2022 15:20:33 +0200 Subject: [PATCH 3/3] Add integration test with container private registry --- shared/api/registry.js | 6 + .../containers_private_registry.test.js | 111 ++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 tests/containers/containers_private_registry.test.js diff --git a/shared/api/registry.js b/shared/api/registry.js index 1d12ef2c..372ee8d8 100644 --- a/shared/api/registry.js +++ b/shared/api/registry.js @@ -21,6 +21,12 @@ class RegistryApi { return this.apiManager.delete(`namespaces/${namespaceId}`) .catch(manageError); } + + createRegistryNamespace(params) { + return this.apiManager.post("namespaces", params) + .then(response => response.data) + .catch(manageError); + } } module.exports = RegistryApi; diff --git a/tests/containers/containers_private_registry.test.js b/tests/containers/containers_private_registry.test.js new file mode 100644 index 00000000..ea176bc3 --- /dev/null +++ b/tests/containers/containers_private_registry.test.js @@ -0,0 +1,111 @@ +'use strict'; + +const crypto = require('crypto'); +const Docker = require('dockerode'); + +const docker = new Docker(); + +const path = require('path'); +const fs = require('fs'); +const { expect } = require('chai'); + +const { getTmpDirPath, replaceTextInFile } = require('../utils/fs'); +const { getServiceName, sleep, serverlessDeploy, serverlessRemove} = require('../utils/misc'); +const { ContainerApi, RegistryApi } = require('../../shared/api'); +const { CONTAINERS_API_URL, REGISTRY_API_URL } = require('../../shared/constants'); +const { execSync, execCaptureOutput } = require('../../shared/child-process'); + +const serverlessExec = path.join('serverless'); + +describe('Build and deploy on container with a base image private', () => { + const templateName = path.resolve(__dirname, '..', '..', 'examples', 'container'); + const tmpDir = getTmpDirPath(); + let oldCwd; + let serviceName; + const scwRegion = process.env.SCW_REGION; + const scwProject = process.env.SCW_DEFAULT_PROJECT_ID || process.env.SCW_PROJECT; + const scwToken = process.env.SCW_SECRET_KEY || process.env.SCW_TOKEN; + const apiUrl = `${CONTAINERS_API_URL}/${scwRegion}`; + const registryApiUrl = `${REGISTRY_API_URL}/${scwRegion}/`; + let api; + let registryApi; + let namespace; + let containerName; + + const originalImageRepo = 'python'; + const imageTag = '3-alpine'; + let privateRegistryImageRepo; + let privateRegistryNamespaceId; + + beforeAll(async () => { + oldCwd = process.cwd(); + serviceName = getServiceName(); + api = new ContainerApi(apiUrl, scwToken); + registryApi = new RegistryApi(registryApiUrl, scwToken); + + // pull the base image, create a private registry, push it into that registry, and remove the image locally + // to check that the image is pulled at build time + const registryName = `private-registry-${crypto.randomBytes(16).toString('hex')}`; + const privateRegistryNamespace = await registryApi.createRegistryNamespace({name: registryName, project_id: scwProject}); + privateRegistryNamespaceId = privateRegistryNamespace.id; + + privateRegistryImageRepo = `rg.${scwRegion}.scw.cloud/${registryName}/python`; + + await docker.pull(`${originalImageRepo}:${imageTag}`); + const originalImage = docker.getImage(`${originalImageRepo}:${imageTag}`); + await originalImage.tag({repo: privateRegistryImageRepo, tag: imageTag}); + const privateRegistryImage = docker.getImage(`${privateRegistryImageRepo}:${imageTag}`); + await privateRegistryImage.push({ + stream: false, + username: 'nologin', + password: scwToken + }); + await privateRegistryImage.remove(); + }); + + afterAll(async () => { + await registryApi.deleteRegistryNamespace(privateRegistryNamespaceId); + process.chdir(oldCwd); + }); + + it('should create service in tmp directory', () => { + execSync(`${serverlessExec} create --template-path ${templateName} --path ${tmpDir}`); + process.chdir(tmpDir); + execSync(`npm link ${oldCwd}`); + replaceTextInFile('serverless.yml', 'scaleway-container', serviceName); + replaceTextInFile('serverless.yml', '', scwToken); + replaceTextInFile('serverless.yml', '', scwProject); + replaceTextInFile(path.join('my-container', 'Dockerfile'), 'FROM python:3-alpine', `FROM ${privateRegistryImageRepo}:${imageTag}`); + expect(fs.existsSync(path.join(tmpDir, 'serverless.yml'))).to.be.equal(true); + expect(fs.existsSync(path.join(tmpDir, 'my-container'))).to.be.equal(true); + }); + + it('should deploy service/container to scaleway', async () => { + serverlessDeploy(); + namespace = await api.getNamespaceFromList(serviceName); + namespace.containers = await api.listContainers(namespace.id); + containerName = namespace.containers[0].name; + }); + + it('should invoke container from scaleway', async () => { + // TODO query function status instead of having an arbitrary sleep + await sleep(30000); + + let output = execCaptureOutput(serverlessExec, ['invoke', '--function', containerName]); + expect(output).to.be.equal('{"message":"Hello, World from Scaleway Container !"}'); + }); + + it('should remove service from scaleway', async () => { + serverlessRemove(); + try { + await api.getNamespace(namespace.id); + } catch (err) { + expect(err.response.status).to.be.equal(404); + } + }); + + it('should remove registry namespace properly', async () => { + const response = await registryApi.deleteRegistryNamespace(namespace.registry_namespace_id); + expect(response.status).to.be.equal(200); + }); +});