Skip to content

Commit a728ad3

Browse files
authored
During container build, authenticate to registry to pull private images (#103)
1 parent cc1fa94 commit a728ad3

File tree

3 files changed

+133
-2
lines changed

3 files changed

+133
-2
lines changed

deploy/lib/buildAndPushContainers.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,26 @@ function findErrorInBuildOutput(buildOutput) {
4545
}
4646

4747
module.exports = {
48-
buildAndPushContainers() {
48+
async buildAndPushContainers() {
49+
// used for pushing
4950
const auth = {
5051
username: 'any',
5152
password: this.provider.scwToken,
5253
};
5354

55+
// used for building: see https://docs.docker.com/engine/api/v1.37/#tag/Image/operation/ImageBuild
56+
const registryAuth = {};
57+
registryAuth['rg.' + this.provider.scwRegion + '.scw.cloud'] = {
58+
username: 'any',
59+
password: this.provider.scwToken,
60+
};
61+
62+
try {
63+
await docker.checkAuth(registryAuth);
64+
} catch (err) {
65+
throw new Error(`Authentication to registry failed`);
66+
}
67+
5468
const containerNames = Object.keys(this.containers);
5569
const promises = containerNames.map((containerName) => {
5670
const container = this.containers[containerName];
@@ -60,7 +74,7 @@ module.exports = {
6074
this.serverless.cli.log(`Building and pushing container ${container.name} to: ${imageName} ...`);
6175

6276
return new Promise(async (resolve, reject) => {
63-
const buildStream = await docker.buildImage(tarStream, { t: imageName })
77+
const buildStream = await docker.buildImage(tarStream, { t: imageName, registryconfig: registryAuth })
6478
const buildStreamEvents = await extractStreamContents(buildStream, this.provider.options.verbose);
6579

6680
const buildError = findErrorInBuildOutput(buildStreamEvents);

shared/api/registry.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ class RegistryApi {
2121
return this.apiManager.delete(`namespaces/${namespaceId}`)
2222
.catch(manageError);
2323
}
24+
25+
createRegistryNamespace(params) {
26+
return this.apiManager.post("namespaces", params)
27+
.then(response => response.data)
28+
.catch(manageError);
29+
}
2430
}
2531

2632
module.exports = RegistryApi;
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict';
2+
3+
const crypto = require('crypto');
4+
const Docker = require('dockerode');
5+
6+
const docker = new Docker();
7+
8+
const path = require('path');
9+
const fs = require('fs');
10+
const { expect } = require('chai');
11+
12+
const { getTmpDirPath, replaceTextInFile } = require('../utils/fs');
13+
const { getServiceName, sleep, serverlessDeploy, serverlessRemove} = require('../utils/misc');
14+
const { ContainerApi, RegistryApi } = require('../../shared/api');
15+
const { CONTAINERS_API_URL, REGISTRY_API_URL } = require('../../shared/constants');
16+
const { execSync, execCaptureOutput } = require('../../shared/child-process');
17+
18+
const serverlessExec = path.join('serverless');
19+
20+
describe('Build and deploy on container with a base image private', () => {
21+
const templateName = path.resolve(__dirname, '..', '..', 'examples', 'container');
22+
const tmpDir = getTmpDirPath();
23+
let oldCwd;
24+
let serviceName;
25+
const scwRegion = process.env.SCW_REGION;
26+
const scwProject = process.env.SCW_DEFAULT_PROJECT_ID || process.env.SCW_PROJECT;
27+
const scwToken = process.env.SCW_SECRET_KEY || process.env.SCW_TOKEN;
28+
const apiUrl = `${CONTAINERS_API_URL}/${scwRegion}`;
29+
const registryApiUrl = `${REGISTRY_API_URL}/${scwRegion}/`;
30+
let api;
31+
let registryApi;
32+
let namespace;
33+
let containerName;
34+
35+
const originalImageRepo = 'python';
36+
const imageTag = '3-alpine';
37+
let privateRegistryImageRepo;
38+
let privateRegistryNamespaceId;
39+
40+
beforeAll(async () => {
41+
oldCwd = process.cwd();
42+
serviceName = getServiceName();
43+
api = new ContainerApi(apiUrl, scwToken);
44+
registryApi = new RegistryApi(registryApiUrl, scwToken);
45+
46+
// pull the base image, create a private registry, push it into that registry, and remove the image locally
47+
// to check that the image is pulled at build time
48+
const registryName = `private-registry-${crypto.randomBytes(16).toString('hex')}`;
49+
const privateRegistryNamespace = await registryApi.createRegistryNamespace({name: registryName, project_id: scwProject});
50+
privateRegistryNamespaceId = privateRegistryNamespace.id;
51+
52+
privateRegistryImageRepo = `rg.${scwRegion}.scw.cloud/${registryName}/python`;
53+
54+
await docker.pull(`${originalImageRepo}:${imageTag}`);
55+
const originalImage = docker.getImage(`${originalImageRepo}:${imageTag}`);
56+
await originalImage.tag({repo: privateRegistryImageRepo, tag: imageTag});
57+
const privateRegistryImage = docker.getImage(`${privateRegistryImageRepo}:${imageTag}`);
58+
await privateRegistryImage.push({
59+
stream: false,
60+
username: 'nologin',
61+
password: scwToken
62+
});
63+
await privateRegistryImage.remove();
64+
});
65+
66+
afterAll(async () => {
67+
await registryApi.deleteRegistryNamespace(privateRegistryNamespaceId);
68+
process.chdir(oldCwd);
69+
});
70+
71+
it('should create service in tmp directory', () => {
72+
execSync(`${serverlessExec} create --template-path ${templateName} --path ${tmpDir}`);
73+
process.chdir(tmpDir);
74+
execSync(`npm link ${oldCwd}`);
75+
replaceTextInFile('serverless.yml', 'scaleway-container', serviceName);
76+
replaceTextInFile('serverless.yml', '<scw-token>', scwToken);
77+
replaceTextInFile('serverless.yml', '<scw-project-id>', scwProject);
78+
replaceTextInFile(path.join('my-container', 'Dockerfile'), 'FROM python:3-alpine', `FROM ${privateRegistryImageRepo}:${imageTag}`);
79+
expect(fs.existsSync(path.join(tmpDir, 'serverless.yml'))).to.be.equal(true);
80+
expect(fs.existsSync(path.join(tmpDir, 'my-container'))).to.be.equal(true);
81+
});
82+
83+
it('should deploy service/container to scaleway', async () => {
84+
serverlessDeploy();
85+
namespace = await api.getNamespaceFromList(serviceName);
86+
namespace.containers = await api.listContainers(namespace.id);
87+
containerName = namespace.containers[0].name;
88+
});
89+
90+
it('should invoke container from scaleway', async () => {
91+
// TODO query function status instead of having an arbitrary sleep
92+
await sleep(30000);
93+
94+
let output = execCaptureOutput(serverlessExec, ['invoke', '--function', containerName]);
95+
expect(output).to.be.equal('{"message":"Hello, World from Scaleway Container !"}');
96+
});
97+
98+
it('should remove service from scaleway', async () => {
99+
serverlessRemove();
100+
try {
101+
await api.getNamespace(namespace.id);
102+
} catch (err) {
103+
expect(err.response.status).to.be.equal(404);
104+
}
105+
});
106+
107+
it('should remove registry namespace properly', async () => {
108+
const response = await registryApi.deleteRegistryNamespace(namespace.registry_namespace_id);
109+
expect(response.status).to.be.equal(200);
110+
});
111+
});

0 commit comments

Comments
 (0)