Skip to content
This repository was archived by the owner on Nov 23, 2022. It is now read-only.

Commit 9f397f1

Browse files
committed
Merge branch 'develop', prepare 3.3.0
2 parents 7b60608 + 2b2df4d commit 9f397f1

File tree

8 files changed

+282
-4
lines changed

8 files changed

+282
-4
lines changed

src/db/secrets.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// npm packages
2+
const path = require('path');
3+
const Loki = require('lokijs');
4+
5+
// our packages
6+
const {baseFolder} = require('../config');
7+
8+
// init in-memory adapter for login requests
9+
const memAdapter = new Loki.LokiMemoryAdapter();
10+
const fsAdapter = new Loki.LokiFsAdapter();
11+
12+
// init persistent secrets db
13+
let secretsCollection = {};
14+
let secretResolve = () => {};
15+
const secretsInited = new Promise(r => {
16+
secretResolve = r;
17+
});
18+
const secretDb = new Loki(path.join(baseFolder, 'secrets.db'), {
19+
adapter: process.env.NODE_ENV !== 'testing' ? fsAdapter : memAdapter,
20+
autoload: true,
21+
autoloadCallback() {
22+
// get of create secrets collection
23+
secretsCollection = secretDb.getCollection('secrets');
24+
if (secretsCollection === null) {
25+
secretsCollection = secretDb.addCollection('secrets');
26+
}
27+
secretResolve();
28+
},
29+
autosave: process.env.NODE_ENV !== 'testing',
30+
});
31+
32+
exports.secretDb = secretDb;
33+
exports.getSecretsCollection = () => secretsCollection;
34+
exports.secretsInited = secretsInited;

src/docker/start.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@
22
const docker = require('./docker');
33
const {initNetwork, createNetwork} = require('../docker/network');
44
const {getProjectConfig, nameFromImage, projectFromConfig, writeStatus} = require('../util');
5+
const {getSecretsCollection} = require('../db/secrets');
56
const {getConfig} = require('../config');
67

8+
// try to find secret with current value name and return secret value if present
9+
const valueOrSecret = (value, secrets) => {
10+
const secret = secrets.find(s => `@${s.name}` === value);
11+
if (secret) {
12+
return secret.value;
13+
}
14+
return value;
15+
};
16+
717
exports.startFromParams = async ({
818
image,
919
deploymentName,
@@ -181,8 +191,10 @@ exports.start = async ({image, username, resultStream, existing = []}) => {
181191
// construct host
182192
const host = config.domain === undefined ? defaultDomain : config.domain;
183193

184-
// generate env vars
185-
const Env = config.env ? Object.keys(config.env).map(key => `${key}=${config.env[key]}`) : [];
194+
// replace env vars values with secrets if needed
195+
const secrets = getSecretsCollection().find({user: username});
196+
// generate env vars (with secrets)
197+
const Env = config.env ? Object.keys(config.env).map(key => `${key}=${valueOrSecret(config.env[key], secrets)}`) : [];
186198

187199
// generate project name
188200
const project = projectFromConfig({username, config});
@@ -252,7 +264,7 @@ exports.start = async ({image, username, resultStream, existing = []}) => {
252264

253265
// if basic auth is set - add it to config
254266
if (config.basicAuth && config.basicAuth.length) {
255-
Labels['traefik.frontend.auth.basic.users'] = config.basicAuth
267+
Labels['traefik.frontend.auth.basic.users'] = config.basicAuth;
256268
}
257269

258270
// if running in swarm mode - run traefik as swarm service

src/routes/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const update = require('./update');
77
const version = require('./version');
88
const templates = require('./templates');
99
const setup = require('./setup');
10+
const secrets = require('./secrets');
1011

1112
module.exports = (fastify, opts, next) => {
1213
// enable auth for all routes
@@ -20,6 +21,7 @@ module.exports = (fastify, opts, next) => {
2021
version(fastify);
2122
templates(fastify);
2223
setup(fastify);
24+
secrets(fastify);
2325

2426
next();
2527
};

src/routes/secrets.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// our modules
2+
const {secretsInited, getSecretsCollection} = require('../db/secrets');
3+
4+
module.exports = fastify => {
5+
fastify.route({
6+
method: 'GET',
7+
path: '/secrets',
8+
async handler(request, reply) {
9+
// get username
10+
const {username} = request.user;
11+
12+
// wait for db to init if required
13+
await secretsInited;
14+
// find user secrets
15+
const secrets = getSecretsCollection()
16+
.find({user: username})
17+
.map(({value, ...s}) => s);
18+
19+
reply.send({secrets});
20+
},
21+
});
22+
23+
fastify.route({
24+
method: 'POST',
25+
path: '/secrets',
26+
async handler(request, reply) {
27+
// get username
28+
const {username} = request.user;
29+
// get secret data
30+
const {secretName, secretValue} = request.body;
31+
32+
// wait for db to init if required
33+
await secretsInited;
34+
// create new secret for current user
35+
const secret = {user: username, name: secretName, value: secretValue};
36+
getSecretsCollection().insert(secret);
37+
38+
reply.send(secret);
39+
},
40+
});
41+
42+
fastify.route({
43+
method: 'DELETE',
44+
path: '/secrets',
45+
async handler(request, reply) {
46+
// generate new deploy token
47+
const {secretName} = request.body;
48+
const {user} = request;
49+
const existingSecret = getSecretsCollection().findOne({user: user.username, name: secretName});
50+
if (!existingSecret) {
51+
reply.code(200).send({removed: false, reason: 'Secret does not exist'});
52+
return;
53+
}
54+
// wait for db to init if required
55+
await secretsInited;
56+
// remove token from collection
57+
getSecretsCollection().remove(existingSecret);
58+
// send back to user
59+
reply.code(204).send();
60+
},
61+
});
62+
};

test/deploy.test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ test('Should deploy simple docker project', async done => {
9595

9696
const containerData = docker.getContainer(containerInfo.Id);
9797
const container = await containerData.inspect();
98-
// console.log(JSON.stringify(container));
9998
expect(container.NetworkSettings.Networks.exoframe.Aliases.includes('test')).toBeTruthy();
10099
expect(container.HostConfig.RestartPolicy).toMatchObject({Name: 'no', MaximumRetryCount: 0});
101100

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM busybox
2+
3+
CMD ["sleep", "300"]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "test-secrets-deploy",
3+
"restart": "no",
4+
"project": "secrets-project",
5+
"env": {
6+
"test": "@test-secret"
7+
},
8+
"hostname": "secrets"
9+
}

test/secrets.test.js

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/* eslint-env jest */
2+
// mock config for testing
3+
jest.mock('../src/config', () => require('./__mocks__/config'));
4+
const config = require('../src/config');
5+
// switch config to normal
6+
config.__load('normal');
7+
8+
// npm packages
9+
const getPort = require('get-port');
10+
const path = require('path');
11+
const tar = require('tar-fs');
12+
13+
// our packages
14+
const authToken = require('./fixtures/authToken');
15+
const {startServer} = require('../src');
16+
const {getSecretsCollection} = require('../src/db/secrets');
17+
const docker = require('../src/docker/docker');
18+
19+
// create tar streams
20+
const streamDocker = tar.pack(path.join(__dirname, 'fixtures', 'secrets-project'));
21+
22+
// test secret
23+
const testSecret = {
24+
secretName: 'test-secret',
25+
secretValue: 'test-secret-value',
26+
};
27+
28+
// container vars
29+
let fastify;
30+
31+
// set timeout to 60s
32+
jest.setTimeout(60000);
33+
34+
beforeAll(async () => {
35+
// start server
36+
const port = await getPort();
37+
fastify = await startServer(port);
38+
return fastify;
39+
});
40+
41+
afterAll(() => fastify.close());
42+
43+
test('Should create new secret', async done => {
44+
// options base
45+
const options = {
46+
method: 'POST',
47+
url: '/secrets',
48+
headers: {
49+
Authorization: `Bearer ${authToken}`,
50+
},
51+
payload: testSecret,
52+
};
53+
54+
const response = await fastify.inject(options);
55+
const result = JSON.parse(response.payload);
56+
57+
// check response
58+
expect(response.statusCode).toEqual(200);
59+
expect(result.name).toEqual(testSecret.secretName);
60+
expect(result.value).toEqual(testSecret.secretValue);
61+
expect(result.user).toEqual('admin');
62+
63+
done();
64+
});
65+
66+
test('Should get list with new secret', async done => {
67+
// options base
68+
const options = {
69+
method: 'GET',
70+
url: '/secrets',
71+
headers: {
72+
Authorization: `Bearer ${authToken}`,
73+
},
74+
};
75+
76+
const response = await fastify.inject(options);
77+
const result = JSON.parse(response.payload);
78+
79+
// check response
80+
expect(response.statusCode).toEqual(200);
81+
expect(result.secrets).toBeDefined();
82+
expect(result.secrets.length).toEqual(1);
83+
expect(result.secrets[0].user).toEqual('admin');
84+
expect(result.secrets[0].name).toEqual(testSecret.secretName);
85+
expect(result.secrets[0].value).toBeUndefined();
86+
87+
done();
88+
});
89+
90+
test('Should deploy simple docker project with secret', async done => {
91+
const options = {
92+
method: 'POST',
93+
url: '/deploy',
94+
headers: {
95+
Authorization: `Bearer ${authToken}`,
96+
'Content-Type': 'application/octet-stream',
97+
},
98+
payload: streamDocker,
99+
};
100+
101+
const response = await fastify.inject(options);
102+
// parse result into lines
103+
const result = response.payload
104+
.split('\n')
105+
.filter(l => l && l.length)
106+
.map(line => JSON.parse(line));
107+
108+
// find deployments
109+
const completeDeployments = result.find(it => it.deployments && it.deployments.length).deployments;
110+
111+
// check response
112+
expect(response.statusCode).toEqual(200);
113+
expect(completeDeployments.length).toEqual(1);
114+
expect(completeDeployments[0].Name.startsWith('/exo-admin-test-secrets-deploy-')).toBeTruthy();
115+
116+
// check docker services
117+
const allContainers = await docker.listContainers();
118+
const containerInfo = allContainers.find(c => c.Names.includes(completeDeployments[0].Name));
119+
expect(containerInfo).toBeDefined();
120+
121+
const containerData = docker.getContainer(containerInfo.Id);
122+
const container = await containerData.inspect();
123+
124+
// check secrets replacement in env vars
125+
const [key, value] = container.Config.Env.map(v => v.split('=')).find(([key]) => key === 'test');
126+
expect(key).toEqual('test');
127+
expect(value).toEqual(testSecret.secretValue);
128+
129+
// cleanup
130+
const instance = docker.getContainer(containerInfo.Id);
131+
await instance.remove({force: true});
132+
133+
done();
134+
});
135+
136+
test('Should delete new secret', async done => {
137+
// options base
138+
const options = {
139+
method: 'DELETE',
140+
url: '/secrets',
141+
headers: {
142+
Authorization: `Bearer ${authToken}`,
143+
},
144+
payload: {
145+
secretName: testSecret.secretName,
146+
},
147+
};
148+
149+
// check response
150+
const response = await fastify.inject(options);
151+
expect(response.statusCode).toEqual(204);
152+
153+
// make sure it's no longer in db
154+
expect(getSecretsCollection().find()).toEqual([]);
155+
156+
done();
157+
});

0 commit comments

Comments
 (0)