Skip to content

Commit 267dbcd

Browse files
committed
Merge branch 'develop', prepare 3.3.0
2 parents dcc3b59 + 5818d30 commit 267dbcd

File tree

8 files changed

+496
-18
lines changed

8 files changed

+496
-18
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ Exoframe is a self-hosted tool that allows simple one-command deployments using
1515
- SSH key based auth
1616
- Rolling updates
1717
- Deploy tokens (e.g. to deploy from CI)
18+
- Deploy secrets (e.g. to hide sensitive env vars)
1819
- Automated HTTPS setup via letsencrypt \*
1920
- Automated gzip compression \*
2021
- Rate-limit support \*
22+
- Basic HTTP Auth support \*
2123
- Simple access to the logs of deployments
2224
- Docker-compose support
2325
- Multiple deployment endpoints and multi-user support

docs/Advanced.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,26 @@ This will define how many requests (`average`) over given time (`period`) can be
4949
For the example above - an average of 5 requests every 3 seconds is allowed with busts of up to 10 requests.
5050

5151
For more information, see [Traefik rate-limiting docs](https://docs.traefik.io/configuration/commons/#rate-limiting).
52+
53+
## Secrets
54+
55+
Exoframe allows you to create server-side secret values that can be used during service deployments.
56+
To use secrets you first need to create one. This can be done by running:
57+
58+
```
59+
$ exoframe secret new
60+
```
61+
62+
Once you specify the name and value, Exoframe server will create new secret _for your current user_.
63+
After creation the secret can be used in `exoframe.json` config file by using secret name and prefixing it with `@`, like so (in this example the secret was name `my-secret`):
64+
65+
```json
66+
"env": {
67+
"SECRET_KEY": "@my-secret"
68+
},
69+
```
70+
71+
Current caveats:
72+
73+
- Currently secrets only work for environment variables
74+
- Currently secrets work only for normal deployments (any template or recipe that uses `startFromParams` won't have secrets expanded)

docs/Basics.md

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,22 @@ You can find the list of available recipes [on npm](https://www.npmjs.com/search
4545

4646
## Commands
4747

48-
| Command | Description |
49-
| ----------------- | -------------------------------------------------------------------- |
50-
| deploy [path] | Deploy specified path |
51-
| config | Generate or update project config for current path |
52-
| list | List currently deployed projects |
53-
| rm <id> | Remove existing deployment or project |
54-
| log <id> | Get logs for existing deployment or project |
55-
| template [ls, rm] | Add, list or remove deployment templates from the server |
56-
| setup [recipe] | Setup a complex recipe deployment |
57-
| token [ls, rm] | Generate, list or remove deployment tokens |
58-
| login | Login into Exoframe server |
59-
| endpoint [url] | Selects or adds the endpoint of Exoframe server |
60-
| rm-endpoint [url] | Removes an existing endpoint of Exoframe server |
61-
| update [target] | Gets current versions or updates given target (server, traefik, all) |
62-
| completion | Generates bash completion script |
48+
| Command | Description |
49+
| -------------------- | -------------------------------------------------------------------- |
50+
| deploy [path] | Deploy specified path |
51+
| config | Generate or update project config for current path |
52+
| list | List currently deployed projects |
53+
| rm <id> | Remove existing deployment or project |
54+
| log <id> | Get logs for existing deployment or project |
55+
| template [ls, rm] | Add, list or remove deployment templates from the server |
56+
| setup [recipe] | Setup a complex recipe deployment |
57+
| token [ls, rm] | Generate, list or remove deployment tokens |
58+
| secret [new, ls, rm] | Create, list or remove deployment secrets |
59+
| login | Login into Exoframe server |
60+
| endpoint [url] | Selects or adds the endpoint of Exoframe server |
61+
| rm-endpoint [url] | Removes an existing endpoint of Exoframe server |
62+
| update [target] | Gets current versions or updates given target (server, traefik, all) |
63+
| completion | Generates bash completion script |
6364

6465
## Project config file
6566

@@ -88,7 +89,9 @@ Config file has the following structure:
8889
// object of key-values for env vars [optional]
8990
// no env vars are assigned by default
9091
"env": {
91-
"ENV_VAR": "123"
92+
"ENV_VAR": "123",
93+
// you can use secrets to hide sensitive values from env vars
94+
"OTHER_VAR": "@my-secret"
9295
},
9396
// internal hostname for container [optional]
9497
// see docker docs for more info

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "exoframe",
3-
"version": "3.2.0",
3+
"version": "3.3.0-dev",
44
"description": "Exoframe is a self-hosted tool that allows simple one-command deployments using Docker",
55
"main": "index.js",
66
"repository": "[email protected]:exoframejs/exoframe.git",

src/commands/secrets.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// npm packages
2+
const got = require('got');
3+
const chalk = require('chalk');
4+
const inquirer = require('inquirer');
5+
6+
// our packages
7+
const {userConfig, isLoggedIn, logout} = require('../config');
8+
9+
exports.command = ['secret [cmd]'];
10+
exports.describe = 'create, list or remove deployment secrets';
11+
exports.builder = {
12+
cmd: {
13+
default: 'new',
14+
description: 'command to execute [new | ls | rm]',
15+
},
16+
};
17+
exports.handler = async args => {
18+
if (!isLoggedIn()) {
19+
return;
20+
}
21+
22+
// services request url
23+
const remoteUrl = `${userConfig.endpoint}/secrets`;
24+
// get command
25+
const {cmd} = args;
26+
// if remove or ls - fetch secrets from remote, then do work
27+
if (cmd === 'ls' || cmd === 'rm') {
28+
console.log(
29+
chalk.bold(`${cmd === 'ls' ? 'Listing' : 'Removing'} deployment secret${cmd === 'ls' ? 's' : ''} for:`),
30+
userConfig.endpoint
31+
);
32+
33+
// get secrets from server
34+
// construct shared request params
35+
const options = {
36+
method: 'GET',
37+
headers: {
38+
Authorization: `Bearer ${userConfig.token}`,
39+
},
40+
json: true,
41+
};
42+
// try sending request
43+
let secrets = [];
44+
try {
45+
const {body} = await got(remoteUrl, options);
46+
secrets = body.secrets;
47+
} catch (e) {
48+
// if authorization is expired/broken/etc
49+
if (e.statusCode === 401) {
50+
logout(userConfig);
51+
console.log(chalk.red('Error: authorization expired!'), 'Please, relogin and try again.');
52+
return;
53+
}
54+
55+
console.log(chalk.red('Error getting deployment secrets:'), e.toString());
56+
return;
57+
}
58+
59+
if (cmd === 'ls') {
60+
console.log(chalk.bold('Got saved secrets:'));
61+
console.log('');
62+
secrets.map(t =>
63+
console.log(` > ${chalk.green(`@${t.name}`)} ${chalk.gray(`[${new Date(t.meta.created).toLocaleString()}]`)}`)
64+
);
65+
if (!secrets.length) {
66+
console.log(' > No deployment secrets available!');
67+
}
68+
return;
69+
}
70+
71+
const prompts = [];
72+
prompts.push({
73+
type: 'list',
74+
name: 'rmSecret',
75+
message: 'Choose secret to remove:',
76+
choices: secrets.map(t => t.name),
77+
});
78+
const {rmSecret} = await inquirer.prompt(prompts);
79+
80+
// construct shared request params
81+
const rmOptions = {
82+
method: 'DELETE',
83+
headers: {
84+
Authorization: `Bearer ${userConfig.token}`,
85+
},
86+
json: true,
87+
body: {
88+
secretName: rmSecret,
89+
},
90+
};
91+
try {
92+
const {body, statusCode} = await got(remoteUrl, rmOptions);
93+
if (statusCode !== 204) {
94+
console.log(chalk.red('Error removing deployment secret!'), body.reason || 'Please try again!');
95+
return;
96+
}
97+
console.log(chalk.green('Deployment secret successfully removed!'));
98+
} catch (e) {
99+
// if authorization is expired/broken/etc
100+
if (e.statusCode === 401) {
101+
logout(userConfig);
102+
console.log(chalk.red('Error: authorization expired!'), 'Please, relogin and try again.');
103+
return;
104+
}
105+
106+
console.log(chalk.red('Error removing secret:'), e.toString());
107+
return;
108+
}
109+
110+
return;
111+
}
112+
113+
console.log(chalk.bold('Generating new deployment secret for:'), userConfig.endpoint);
114+
115+
// ask for secret name and value
116+
const prompts = [];
117+
prompts.push({
118+
type: 'input',
119+
name: 'secretName',
120+
message: 'Secret name:',
121+
validate: input => input && input.length > 0,
122+
filter: input => input.trim(),
123+
});
124+
prompts.push({
125+
type: 'input',
126+
name: 'secretValue',
127+
message: 'Secret value:',
128+
validate: input => input && input.length > 0,
129+
filter: input => input.trim(),
130+
});
131+
const {secretName, secretValue} = await inquirer.prompt(prompts);
132+
133+
// construct shared request params
134+
const options = {
135+
method: 'POST',
136+
headers: {
137+
Authorization: `Bearer ${userConfig.token}`,
138+
},
139+
json: true,
140+
body: {
141+
secretName,
142+
secretValue,
143+
},
144+
};
145+
// try sending request
146+
try {
147+
const {body} = await got(remoteUrl, options);
148+
console.log(chalk.bold('New secret generated:'));
149+
console.log('');
150+
console.log(`Name: ${body.name}`);
151+
console.log(`Value: ${body.value}`);
152+
console.log('');
153+
console.log(chalk.yellow('WARNING!'), `Make sure to write it down, you will not be able to get it's value again!`);
154+
} catch (e) {
155+
// if authorization is expired/broken/etc
156+
if (e.statusCode === 401) {
157+
logout(userConfig);
158+
console.log(chalk.red('Error: authorization expired!'), 'Please, relogin and try again.');
159+
return;
160+
}
161+
162+
console.log(chalk.red('Error generating deployment secret:'), e.toString());
163+
}
164+
};

src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const token = require('./commands/token');
2323
const update = require('./commands/update');
2424
const template = require('./commands/template');
2525
const setup = require('./commands/setup');
26+
const secrets = require('./commands/secrets');
2627

2728
// init program
2829
yargs
@@ -41,4 +42,5 @@ yargs
4142
.command(config)
4243
.command(update)
4344
.command(template)
44-
.command(setup).argv;
45+
.command(setup)
46+
.command(secrets).argv;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Should create new secret 1`] = `
4+
Array [
5+
Array [
6+
"Generating new deployment secret for:",
7+
"http://localhost:8080",
8+
],
9+
Array [
10+
"New secret generated:",
11+
],
12+
Array [
13+
"",
14+
],
15+
Array [
16+
"Name: test",
17+
],
18+
Array [
19+
"Value: 12345",
20+
],
21+
Array [
22+
"",
23+
],
24+
Array [
25+
"WARNING!",
26+
"Make sure to write it down, you will not be able to get it's value again!",
27+
],
28+
]
29+
`;
30+
31+
exports[`Should deauth on 401 on creation 1`] = `
32+
Array [
33+
Array [
34+
"Generating new deployment secret for:",
35+
"http://localhost:8080",
36+
],
37+
Array [
38+
"Error: authorization expired!",
39+
"Please, relogin and try again.",
40+
],
41+
]
42+
`;
43+
44+
exports[`Should deauth on 401 on list 1`] = `
45+
Array [
46+
Array [
47+
"Listing deployment secrets for:",
48+
"http://localhost:8080",
49+
],
50+
Array [
51+
"Error: authorization expired!",
52+
"Please, relogin and try again.",
53+
],
54+
]
55+
`;
56+
57+
exports[`Should list secrets 1`] = `
58+
Array [
59+
Array [
60+
"Listing deployment secrets for:",
61+
"http://localhost:8080",
62+
],
63+
Array [
64+
"Got saved secrets:",
65+
],
66+
Array [
67+
"",
68+
],
69+
Array [
70+
" > @test []",
71+
],
72+
]
73+
`;
74+
75+
exports[`Should list zero secrets 1`] = `
76+
Array [
77+
Array [
78+
"Listing deployment secrets for:",
79+
"http://localhost:8080",
80+
],
81+
Array [
82+
"Got saved secrets:",
83+
],
84+
Array [
85+
"",
86+
],
87+
Array [
88+
" > No deployment secrets available!",
89+
],
90+
]
91+
`;
92+
93+
exports[`Should remove secret 1`] = `
94+
Array [
95+
Array [
96+
"Removing deployment secret for:",
97+
"http://localhost:8080",
98+
],
99+
Array [
100+
"Deployment secret successfully removed!",
101+
],
102+
]
103+
`;

0 commit comments

Comments
 (0)