Skip to content

Commit 45dc71c

Browse files
committed
Merge branch 'develop', prepare 0.16.0
2 parents 594fd1a + 5daa70f commit 45dc71c

File tree

11 files changed

+217
-40
lines changed

11 files changed

+217
-40
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,19 @@ Make sure you have [Exoframe server](https://github.com/exoframejs/exoframe-serv
3838
exoframe endpoint http://you.server.url
3939
```
4040

41-
Then use it:
41+
Then login using:
4242

4343
```
44-
exoframe <command> [options]
44+
exoframe login
4545
```
4646

47-
You can find a list of all commands and options below.
47+
Then deploy your project by simply running:
48+
49+
```
50+
exoframe
51+
```
52+
53+
You can find a list of all commands and options in the [docs](./docs/README.md).
4854

4955
## Docs
5056

docs/Basics.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
| log [id] | Get logs for existing deployment or project |
1717
| token | Generate new deployment token |
1818
| login | Login into Exoframe server |
19-
| endpoint [url] | Gets or sets the endpoint of Exoframe server |
19+
| endpoint [url] | Selects or adds the endpoint of Exoframe server |
2020
| completion | Generates bash completion script |
2121

2222
## Project config file
@@ -57,7 +57,7 @@ Config file has the following structure:
5757
## CLI Configuration
5858

5959
Exoframe stores its config in `~/.exoframe/cli.config.yml`.
60-
Currently it contains endpoint URL and list of template plugins:
60+
Currently it contains list of endpoint URLs with associated usernames and authentication tokens:
6161

6262
```yaml
6363
endpoint: 'http://localhost:8080' # your endpoint URL, defaults to localhost
@@ -71,3 +71,15 @@ For this cases you can use deployment tokens. Here's how it works:
7171
1. Make sure you are logged in to your Exoframe server
7272
2. Generate new deployment token using `exoframe token` command
7373
3. Use the new token to deploy your service without need to authenticate: `exoframe deploy -t $TOKEN`
74+
75+
## Updating deployed project
76+
77+
Exoframe provides a way to easily update already deployed projects.
78+
This can be done by passing `--update` (or `-u`) flag to deploy command.
79+
The way it works is quite simple:
80+
81+
1. Exoframe deploys new version of the given project
82+
2. Exoframe then waits for them to start up
83+
3. Exoframe removes the old running deployments for current project
84+
85+
This can be used together with deployment tokens to achieve simple continuous deployment for your projects.

src/commands/config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ const path = require('path');
44
const chalk = require('chalk');
55
const inquirer = require('inquirer');
66

7-
const util = require('util');
8-
97
const validate = input => input && input.length > 0;
108
const filter = input => input.trim();
119

src/commands/deploy.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const Table = require('cli-table');
1111
// my modules
1212
const {userConfig, isLoggedIn, logout} = require('../config');
1313
const {tableBorder, tableStyle} = require('../config/table');
14+
const formatServices = require('../util/formatServices');
1415

1516
const ignores = ['.git', 'node_modules'];
1617

@@ -40,6 +41,11 @@ exports.describe = 'deploy current folder';
4041
exports.builder = {
4142
token: {
4243
alias: 't',
44+
description: 'Deployment token to be used for authentication',
45+
},
46+
update: {
47+
alias: 'u',
48+
description: 'Update current project instead of simple deployment',
4349
},
4450
};
4551
exports.handler = async (args = {}) => {
@@ -51,13 +57,17 @@ exports.handler = async (args = {}) => {
5157
}
5258

5359
const folder = args._ ? args._.filter(arg => arg !== 'deploy').shift() : undefined;
60+
const update = args.update;
5461

55-
console.log(chalk.bold(`Deploying ${folder || 'current project'} to endpoint:`), userConfig.endpoint);
62+
console.log(
63+
chalk.bold(`${update ? 'Updating' : 'Deploying'} ${folder || 'current project'} to endpoint:`),
64+
userConfig.endpoint
65+
);
5666

5767
// create config vars
5868
const workdir = folder ? path.join(process.cwd(), folder) : process.cwd();
5969
const folderName = path.basename(workdir);
60-
const remoteUrl = `${userConfig.endpoint}/deploy`;
70+
const remoteUrl = `${userConfig.endpoint}/${update ? 'update' : 'deploy'}`;
6171

6272
// make sure workdir exists
6373
if (!fs.existsSync(workdir)) {
@@ -120,15 +130,8 @@ exports.handler = async (args = {}) => {
120130
});
121131

122132
// process deployments
123-
res.deployments.forEach(deployment => {
124-
const name = deployment.Name.slice(1);
125-
const domain = deployment.Config.Labels['traefik.frontend.rule']
126-
? `http://${deployment.Config.Labels['traefik.frontend.rule'].replace('Host:', '')}`
127-
: 'Not set';
128-
const aliases = deployment.NetworkSettings.Networks.exoframe.Aliases
129-
? deployment.NetworkSettings.Networks.exoframe.Aliases.filter(alias => !deployment.Id.startsWith(alias))
130-
: [];
131-
const host = aliases.shift() || 'Not set';
133+
const formattedServices = formatServices(res.deployments);
134+
formattedServices.forEach(({name, domain, host}) => {
132135
resultTable.push([name, domain, host]);
133136
});
134137

src/commands/endpoint.js

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,65 @@
11
// npm packages
22
const chalk = require('chalk');
3+
const inquirer = require('inquirer');
34

45
// our packages
56
const {userConfig, updateConfig} = require('../config');
67

78
exports.command = 'endpoint [url]';
8-
exports.describe = 'get or set exoframe server URL';
9+
exports.describe = 'switch or add exoframe server URL';
910
exports.builder = {
1011
url: {
1112
alias: 'u',
1213
default: '',
14+
description: 'URL of a new endpoint',
1315
},
1416
};
15-
exports.handler = ({url}) => {
16-
const endpoint = url;
17+
exports.handler = async ({url}) => {
18+
let endpoint = url;
1719
if (!endpoint || !endpoint.length) {
18-
console.log(chalk.bold('Current endpoint URL:'), userConfig.endpoint);
19-
return;
20+
// if one endpoint only - show this
21+
if (!userConfig.endpoints || !userConfig.endpoints.length) {
22+
console.log(chalk.bold('Current endpoint URL:'), userConfig.endpoint);
23+
return;
24+
}
25+
26+
// if multiple - show selector
27+
const prompts = [];
28+
prompts.push({
29+
type: 'list',
30+
name: 'newEndpoint',
31+
message: 'Choose endpoint:',
32+
default: userConfig.endpoint,
33+
choices: [userConfig.endpoint].concat(userConfig.endpoints.map(entry => entry.endpoint)),
34+
});
35+
const {newEndpoint} = await inquirer.prompt(prompts);
36+
// if user selected current - just exit
37+
if (newEndpoint === userConfig.endpoint) {
38+
return;
39+
}
40+
// assign new selected as entered endpoint
41+
endpoint = newEndpoint;
2042
}
2143

44+
// if current endpoint set - move it to endpoints
45+
if (userConfig.endpoint) {
46+
// init array if needed
47+
if (!userConfig.endpoints) {
48+
userConfig.endpoints = [];
49+
}
50+
// push data
51+
userConfig.endpoints.push({
52+
endpoint: userConfig.endpoint,
53+
user: userConfig.user,
54+
token: userConfig.token,
55+
});
56+
}
57+
// then write new endpoint to current one and remove user/token
2258
console.log(chalk.bold('Updating endpoint URL to:'), endpoint);
23-
updateConfig({endpoint});
59+
const newData = userConfig.endpoints.find(e => e.endpoint === endpoint);
60+
const user = newData ? newData.user : null;
61+
const token = newData ? newData.token : null;
62+
const endpoints = userConfig.endpoints.filter(e => e.endpoint !== endpoint);
63+
updateConfig({endpoint, user, token, endpoints});
2464
console.log(chalk.green('Endpoint URL updated!'));
2565
};

src/commands/list.js

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const Table = require('cli-table');
77
// our packages
88
const {userConfig, isLoggedIn, logout} = require('../config');
99
const {tableBorder, tableStyle} = require('../config/table');
10+
const formatServices = require('../util/formatServices');
1011

1112
exports.command = ['list', 'ls'];
1213
exports.describe = 'list deployments';
@@ -50,19 +51,7 @@ exports.handler = async () => {
5051
console.log(chalk.green(`${services.length} deployments found on ${userConfig.endpoint}:\n`));
5152

5253
// populate table
53-
const formattedServices = services.map(svc => {
54-
const name = svc.Name.slice(1);
55-
const domain = svc.Config.Labels['traefik.frontend.rule']
56-
? svc.Config.Labels['traefik.frontend.rule'].replace('Host:', '')
57-
: 'Not set';
58-
const aliases = svc.NetworkSettings.Networks.exoframe.Aliases
59-
? svc.NetworkSettings.Networks.exoframe.Aliases.filter(alias => !svc.Id.startsWith(alias))
60-
: [];
61-
const project = svc.Config.Labels['exoframe.project'];
62-
const host = aliases.shift() || 'Not set';
63-
const status = svc.State.Status;
64-
return {name, domain, host, status, project};
65-
});
54+
const formattedServices = formatServices(services);
6655

6756
// create table
6857
const resultTable = new Table({

src/commands/login.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ exports.describe = 'login into exoframe server';
1818
exports.builder = {
1919
key: {
2020
alias: 'k',
21+
description: 'User private key used for authentication',
2122
},
2223
passphrase: {
2324
alias: 'p',
25+
description: 'Passphrase for user private key (if set)',
2426
},
2527
};
2628
exports.handler = async ({key, passphrase}) => {

src/util/formatServices.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = services =>
2+
services.map(svc => {
3+
const name = svc.Name.slice(1);
4+
const domain = svc.Config.Labels['traefik.frontend.rule']
5+
? svc.Config.Labels['traefik.frontend.rule'].replace('Host:', '')
6+
: 'Not set';
7+
const aliases = svc.NetworkSettings.Networks.exoframe.Aliases
8+
? svc.NetworkSettings.Networks.exoframe.Aliases.filter(alias => !svc.Id.startsWith(alias))
9+
: [];
10+
const project = svc.Config.Labels['exoframe.project'];
11+
const host = aliases.shift() || 'Not set';
12+
const status = svc.State ? svc.State.Status : '';
13+
return {name, domain, host, status, project};
14+
});

test/deploy.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ module.exports = () => {
5959
[
6060
[`Deploying ${folderPath} to endpoint:`, 'http://localhost:8080'],
6161
['Your project is now deployed as:\n'],
62-
[' ID URL Hostname \n test http://localhost test '],
62+
[' ID URL Hostname \n test localhost test '],
6363
],
6464
'Correct log output'
6565
);
@@ -92,7 +92,7 @@ module.exports = () => {
9292
[
9393
['Deploying current project to endpoint:', 'http://localhost:8080'],
9494
['Your project is now deployed as:\n'],
95-
[' ID URL Hostname \n test http://localhost test '],
95+
[' ID URL Hostname \n test localhost test '],
9696
],
9797
'Correct log output'
9898
);
@@ -131,7 +131,7 @@ module.exports = () => {
131131
['Deploying current project to endpoint:', 'http://localhost:8080'],
132132
['Deploying using given token..'],
133133
['Your project is now deployed as:\n'],
134-
[' ID URL Hostname \n test http://localhost test '],
134+
[' ID URL Hostname \n test localhost test '],
135135
],
136136
'Correct log output'
137137
);
@@ -145,6 +145,39 @@ module.exports = () => {
145145
});
146146
});
147147

148+
// test
149+
tap.test('Should execute update', t => {
150+
// spy on console
151+
const consoleSpy = sinon.spy(console, 'log');
152+
153+
// handle correct request
154+
const updateServer = nock('http://localhost:8080').post('/update').reply((uri, requestBody, cb) => {
155+
cb(null, [200, {status: 'success', deployments}]);
156+
});
157+
158+
// execute login
159+
deploy({update: true}).then(() => {
160+
// make sure log in was successful
161+
// check that server was called
162+
t.ok(updateServer.isDone());
163+
// first check console output
164+
t.deepEqual(
165+
consoleSpy.args,
166+
[
167+
['Updating current project to endpoint:', 'http://localhost:8080'],
168+
['Your project is now deployed as:\n'],
169+
[' ID URL Hostname \n test localhost test '],
170+
],
171+
'Correct log output'
172+
);
173+
// restore console
174+
console.log.restore();
175+
// tear down nock
176+
updateServer.done();
177+
t.end();
178+
});
179+
});
180+
148181
// test
149182
tap.test('Should not deploy with broken config', t => {
150183
// spy on console

test/endpoint.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// npm packages
2+
const tap = require('tap');
3+
const fs = require('fs');
4+
const path = require('path');
5+
const yaml = require('js-yaml');
6+
const sinon = require('sinon');
7+
const inquirer = require('inquirer');
8+
9+
// our packages
10+
const {handler: updateEndpoint} = require('../src/commands/endpoint');
11+
12+
module.exports = () => {
13+
const configPath = path.join(__dirname, 'fixtures', 'cli.config.yml');
14+
const mockEndpoint = 'http://test.endpoint';
15+
16+
// load original config
17+
const origCfg = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'));
18+
19+
// test config generation
20+
tap.test('Should add new endpoint', t => {
21+
// spy on console
22+
const consoleSpy = sinon.spy(console, 'log');
23+
// execute login
24+
updateEndpoint({url: mockEndpoint}).then(() => {
25+
// first check console output
26+
t.deepEqual(
27+
consoleSpy.args,
28+
[['Updating endpoint URL to:', 'http://test.endpoint'], ['Endpoint URL updated!']],
29+
'Correct log output'
30+
);
31+
// then check config changes
32+
const cfg = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'));
33+
t.equal(cfg.endpoint, mockEndpoint, 'Correct endpoint');
34+
t.equal(cfg.user, null, 'Correct new user');
35+
t.equal(cfg.token, null, 'Correct token');
36+
t.equal(cfg.endpoints.length, 1, 'Correct new endpoints list');
37+
t.equal(cfg.endpoints[0].endpoint, origCfg.endpoint, 'Correct endpoint in list');
38+
t.equal(cfg.endpoints[0].user.username, origCfg.user.username, 'Correct user in list');
39+
t.equal(cfg.endpoints[0].token, origCfg.token, 'Correct token in list');
40+
// restore console
41+
console.log.restore();
42+
t.end();
43+
});
44+
});
45+
46+
// test config generation
47+
tap.test('Should select old endpoint', t => {
48+
// spy on console
49+
const consoleSpy = sinon.spy(console, 'log');
50+
// stup inquirer answers
51+
sinon.stub(inquirer, 'prompt').callsFake(() => Promise.resolve({newEndpoint: origCfg.endpoint}));
52+
// execute login
53+
updateEndpoint({}).then(() => {
54+
// first check console output
55+
t.deepEqual(
56+
consoleSpy.args,
57+
[['Updating endpoint URL to:', 'http://localhost:8080'], ['Endpoint URL updated!']],
58+
'Correct log output'
59+
);
60+
// then check config changes
61+
const cfg = yaml.safeLoad(fs.readFileSync(configPath, 'utf8'));
62+
t.equal(cfg.endpoint, origCfg.endpoint, 'Correct endpoint');
63+
t.equal(cfg.user.username, origCfg.user.username, 'Correct new user');
64+
t.equal(cfg.token, origCfg.token, 'Correct token');
65+
t.equal(cfg.endpoints.length, 1, 'Correct new endpoints list');
66+
t.equal(cfg.endpoints[0].endpoint, mockEndpoint, 'Correct endpoint in list');
67+
t.notOk(cfg.endpoints[0].user, 'Correct user in list');
68+
t.notOk(cfg.endpoints[0].token, 'Correct token in list');
69+
// restore console
70+
console.log.restore();
71+
// restore inquirer
72+
inquirer.prompt.restore();
73+
// restore original config
74+
fs.writeFileSync(configPath, yaml.safeDump(origCfg), 'utf8');
75+
t.end();
76+
});
77+
});
78+
};

0 commit comments

Comments
 (0)