diff --git a/deploy/lib/buildAndPushContainers.js b/deploy/lib/buildAndPushContainers.js index 7d3c5d07..01a05083 100644 --- a/deploy/lib/buildAndPushContainers.js +++ b/deploy/lib/buildAndPushContainers.js @@ -45,12 +45,26 @@ function findErrorInBuildOutput(buildOutput) { } 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 new Error(`Authentication to registry failed`); + } + const containerNames = Object.keys(this.containers); const promises = containerNames.map((containerName) => { const container = this.containers[containerName]; @@ -60,7 +74,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 }) const buildStreamEvents = await extractStreamContents(buildStream, this.provider.options.verbose); const buildError = findErrorInBuildOutput(buildStreamEvents); 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); + }); +});