clever-tools 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,6 +88,7 @@ Where `type` is one of:
88
88
  Where region is one of:
89
89
 
90
90
  - `par` (Paris, [Clever Cloud](https://www.clever-cloud.com/infrastructure/))
91
+ - `grahds` (Gravelines, HDS servers, OVHcloud)
91
92
  - `rbx` (Roubaix, OVHcloud)
92
93
  - `rbxhds` (Roubaix, HDS servers, OVHcloud)
93
94
  - `scw` (Paris, [Scaleway DC5](https://www.clever-cloud.com/blog/press/2023/01/17/clever-cloud-and-scaleway-join-forces-to-unveil-a-sovereign-european-paas-offering/))
package/bin/clever.js CHANGED
@@ -90,7 +90,10 @@ function run () {
90
90
  description: 'Application ID (or name, if unambiguous)',
91
91
  parser: Parsers.appIdOrName,
92
92
  }),
93
- appNameCreation: cliparse.argument('app-name', { description: 'Application name' }),
93
+ appNameCreation: cliparse.argument('app-name', {
94
+ description: 'Application name (optional, current directory name is used if not specified)',
95
+ default: '',
96
+ }),
94
97
  backupId: cliparse.argument('backup-id', { description: 'A Database backup ID (format: UUID)' }),
95
98
  databaseId: cliparse.argument('database-id', { description: 'Any database ID (format: addon_UUID, postgresql_UUID, mysql_UUID, ...)' }),
96
99
  drainId: cliparse.argument('drain-id', { description: 'Drain ID' }),
@@ -132,6 +135,7 @@ function run () {
132
135
  sourceableEnvVarsList: cliparse.flag('add-export', { description: 'Display sourceable env variables setting' }),
133
136
  accesslogsFormat: getOutputFormatOption(['simple', 'extended', 'clf']),
134
137
  addonEnvFormat: getOutputFormatOption(['shell']),
138
+ logsFormat: getOutputFormatOption(['json-stream']),
135
139
  accesslogsFollow: cliparse.flag('follow', {
136
140
  aliases: ['f'],
137
141
  description: 'Display access logs continuously (ignores before/until, after/since)',
@@ -180,6 +184,12 @@ function run () {
180
184
  metavar: 'commit id',
181
185
  description: 'Restart the application with a specific commit ID',
182
186
  }),
187
+ gitTag: cliparse.option('tag', {
188
+ aliases: ['t'],
189
+ default: '',
190
+ metavar: 'tag',
191
+ description: 'Tag to push (none by default)',
192
+ }),
183
193
  databaseId: cliparse.option('database-id', {
184
194
  metavar: 'database_id',
185
195
  description: 'The Database ID (e.g.: postgresql_xxx)',
@@ -345,7 +355,7 @@ function run () {
345
355
  parser: Parsers.commaSeparated,
346
356
  }),
347
357
  showAllActivity: cliparse.flag('show-all', { description: 'Show all activity' }),
348
- showAll: cliparse.flag('show-all', { description: 'Show all available dependencies' }),
358
+ showAll: cliparse.flag('show-all', { description: 'Show all available add-ons and applications' }),
349
359
  loginToken: cliparse.option('token', {
350
360
  metavar: 'token',
351
361
  description: 'Directly give an existing token',
@@ -559,7 +569,7 @@ function run () {
559
569
  const addonCreateCommand = cliparse.command('create', {
560
570
  description: 'Create an addon',
561
571
  args: [args.addonProvider, args.addonName],
562
- options: [opts.linkAddon, opts.confirmAddonCreation, opts.addonPlan, opts.addonRegion, opts.addonVersion, opts.addonOptions],
572
+ options: [opts.linkAddon, opts.confirmAddonCreation, opts.addonPlan, opts.addonRegion, opts.addonVersion, opts.addonOptions, opts.humanJsonOutputFormat],
563
573
  }, addon('create'));
564
574
  const addonDeleteCommand = cliparse.command('delete', {
565
575
  description: 'Delete an addon',
@@ -643,7 +653,7 @@ function run () {
643
653
  const deploy = lazyRequirePromiseModule('../src/commands/deploy.js');
644
654
  const deployCommand = cliparse.command('deploy', {
645
655
  description: 'Deploy an application',
646
- options: [opts.alias, opts.branch, opts.quiet, opts.forceDeploy, opts.followDeployLogs, opts.sameCommitPolicy],
656
+ options: [opts.alias, opts.branch, opts.gitTag, opts.quiet, opts.forceDeploy, opts.followDeployLogs, opts.sameCommitPolicy],
647
657
  }, deploy('deploy'));
648
658
 
649
659
  // DIAG COMMAND
@@ -754,7 +764,7 @@ function run () {
754
764
  const logs = lazyRequirePromiseModule('../src/commands/logs.js');
755
765
  const logsCommand = cliparse.command('logs', {
756
766
  description: 'Fetch application logs, continuously',
757
- options: [opts.alias, opts.before, opts.after, opts.search, opts.deploymentId, opts.addonId],
767
+ options: [opts.alias, opts.before, opts.after, opts.search, opts.deploymentId, opts.addonId, opts.logsFormat],
758
768
  }, logs('appLogs'));
759
769
 
760
770
  // MAKE DEFAULT COMMAND
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "main": "bin/clever.js",
6
6
  "keywords": [
@@ -25,14 +25,14 @@
25
25
  "scripts/*.sh"
26
26
  ],
27
27
  "dependencies": {
28
- "@clevercloud/client": "^8.1.0",
28
+ "@clevercloud/client": "^8.1.2",
29
29
  "clf-date": "^0.2.0",
30
30
  "cliparse": "^0.3.3",
31
31
  "colors": "1.4.0",
32
32
  "common-env": "^6.4.0",
33
33
  "curlconverter": "^3.21.0",
34
34
  "eventsource": "^1.1.0",
35
- "isomorphic-git": "^1.25.0",
35
+ "isomorphic-git": "^1.25.3",
36
36
  "linux-release-info": "^3.0.0",
37
37
  "lodash": "^4.17.21",
38
38
  "mkdirp": "^1.0.4",
@@ -34,7 +34,14 @@ async function list (params) {
34
34
 
35
35
  async function create (params) {
36
36
  const [providerName, name] = params.args;
37
- const { link: linkedAppAlias, plan: planName, region, yes: skipConfirmation, org: orgaIdOrName } = params.options;
37
+ const {
38
+ link: linkedAppAlias,
39
+ plan: planName,
40
+ region,
41
+ yes: skipConfirmation,
42
+ org: orgaIdOrName,
43
+ format,
44
+ } = params.options;
38
45
  const version = params.options['addon-version'];
39
46
  const addonOptions = parseAddonOptions(params.options.option);
40
47
 
@@ -42,36 +49,55 @@ async function create (params) {
42
49
  ? await Organisation.getId(orgaIdOrName)
43
50
  : await User.getCurrentId();
44
51
 
52
+ const addonToCreate = {
53
+ ownerId,
54
+ name,
55
+ providerName,
56
+ planName,
57
+ region,
58
+ skipConfirmation,
59
+ version,
60
+ addonOptions,
61
+ };
62
+
45
63
  if (linkedAppAlias != null) {
46
64
  const linkedAppData = await AppConfig.getAppDetails({ alias: linkedAppAlias });
47
- if (orgaIdOrName != null && linkedAppData.ownerId !== ownerId) {
65
+ if (orgaIdOrName != null && linkedAppData.ownerId !== ownerId && format === 'human') {
48
66
  Logger.warn('The specified application does not belong to the specified organisation. Ignoring the `--org` option');
49
67
  }
50
68
  const newAddon = await Addon.create({
69
+ ...addonToCreate,
51
70
  ownerId: linkedAppData.ownerId,
52
- name,
53
- providerName,
54
- planName,
55
- region,
56
- skipConfirmation,
57
- version,
58
- addonOptions,
59
71
  });
60
72
  await Addon.link(linkedAppData.ownerId, linkedAppData.appId, { addon_id: newAddon.id });
61
- Logger.println(`Addon ${name} (id: ${newAddon.id}) successfully created and linked to the application`);
73
+ displayAddon(format, newAddon, `Add-on created and linked to application ${linkedAppAlias} successfully!`);
62
74
  }
63
75
  else {
64
- const newAddon = await Addon.create({
65
- ownerId,
66
- name,
67
- providerName,
68
- planName,
69
- region,
70
- skipConfirmation,
71
- version,
72
- addonOptions,
73
- });
74
- Logger.println(`Addon ${name} (id: ${newAddon.id}) successfully created`);
76
+ const newAddon = await Addon.create(addonToCreate);
77
+ displayAddon(format, newAddon, 'Add-on created successfully!');
78
+ }
79
+ }
80
+
81
+ function displayAddon (format, addon, message) {
82
+ switch (format) {
83
+
84
+ case 'json': {
85
+ Logger.printJson({
86
+ id: addon.id,
87
+ realId: addon.realId,
88
+ name: addon.name,
89
+ });
90
+ break;
91
+ }
92
+
93
+ case 'human':
94
+ default:
95
+ Logger.println([
96
+ message,
97
+ `ID: ${addon.id}`,
98
+ `Real ID: ${addon.realId}`,
99
+ `Name: ${addon.name}`,
100
+ ].join('\n'));
75
101
  }
76
102
  }
77
103
 
@@ -1,15 +1,19 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
3
4
  const Application = require('../models/application.js');
4
5
  const AppConfig = require('../models/app_configuration.js');
5
6
  const Logger = require('../logger.js');
6
7
 
7
8
  async function create (params) {
8
9
  const { type: typeName } = params.options;
9
- const [name] = params.args;
10
+ const [rawName] = params.args;
10
11
  const { org: orgaIdOrName, alias, region, github: githubOwnerRepo, format } = params.options;
11
12
  const { apps } = await AppConfig.loadApplicationConf();
12
13
 
14
+ // Application name is optionnal, use current directory name if not specified (empty string)
15
+ const name = (rawName !== '') ? rawName : getCurrentDirectoryName();
16
+
13
17
  AppConfig.checkAlreadyLinked(apps, name, alias);
14
18
 
15
19
  const github = getGithubDetails(githubOwnerRepo);
@@ -19,13 +23,19 @@ async function create (params) {
19
23
  switch (format) {
20
24
 
21
25
  case 'json': {
22
- console.log(JSON.stringify(app, null, 2));
26
+ Logger.printJson({
27
+ id: app.id,
28
+ name: app.name,
29
+ deployUrl: app.deployUrl,
30
+ });
23
31
  break;
24
32
  }
25
33
 
26
34
  case 'human':
27
35
  default:
28
- Logger.println('Your application has been successfully created!');
36
+ Logger.println('Application created successfully!');
37
+ Logger.println(`ID: ${app.id}`);
38
+ Logger.println(`Name: ${name}`);
29
39
  }
30
40
  };
31
41
 
@@ -36,4 +46,8 @@ function getGithubDetails (githubOwnerRepo) {
36
46
  }
37
47
  }
38
48
 
49
+ function getCurrentDirectoryName () {
50
+ return path.basename(process.cwd());
51
+ }
52
+
39
53
  module.exports = { create };
@@ -13,12 +13,12 @@ const { sendToApi } = require('../models/send-to-api.js');
13
13
  // Once the API call to redeploy() has been triggered successfully,
14
14
  // the rest (waiting for deployment state to evolve and displaying logs) is done with auto retry (resilient to network failures)
15
15
  async function deploy (params) {
16
- const { alias, branch: branchName, quiet, force, follow, 'same-commit-policy': sameCommitPolicy } = params.options;
16
+ const { alias, branch: branchName, tag: tagName, quiet, force, follow, 'same-commit-policy': sameCommitPolicy } = params.options;
17
17
 
18
18
  const appData = await AppConfig.getAppDetails({ alias });
19
19
  const { ownerId, appId } = appData;
20
- const branchRefspec = await git.getFullBranch(branchName);
21
20
 
21
+ const branchRefspec = await getBranchToDeploy(branchName, tagName);
22
22
  const commitIdToPush = await git.getBranchCommit(branchRefspec);
23
23
  const remoteHeadCommitId = await git.getRemoteCommit(appData.deployUrl);
24
24
  const deployedCommitId = await Application.get(ownerId, appId)
@@ -66,7 +66,8 @@ async function deploy (params) {
66
66
  const knownDeployments = await getAllDeployments({ id: ownerId, appId, limit: 5 }).then(sendToApi);
67
67
 
68
68
  Logger.println('Pushing source code to Clever Cloud…');
69
- await git.push(appData.deployUrl, branchRefspec, force)
69
+
70
+ await git.push(appData.deployUrl, commitIdToPush, force)
70
71
  .catch(async (e) => {
71
72
  const isShallow = await git.isShallow();
72
73
  if (isShallow) {
@@ -86,4 +87,20 @@ async function restartOnSameCommit (ownerId, appId, commitIdToPush, quiet, follo
86
87
  return Log.watchDeploymentAndDisplayLogs({ ownerId, appId, deploymentId: restart.deploymentId, quiet, follow });
87
88
  }
88
89
 
90
+ async function getBranchToDeploy (branchName, tagName) {
91
+ if (tagName) {
92
+ const useTag = await git.isExistingTag(tagName);
93
+ if (useTag) {
94
+ const tagRefspec = await git.getFullBranch(tagName);
95
+ return tagRefspec;
96
+ }
97
+ else {
98
+ throw new Error(`Tag ${tagName} doesn't exist locally`);
99
+ }
100
+ }
101
+ else {
102
+ return await git.getFullBranch(branchName);
103
+ }
104
+ }
105
+
89
106
  module.exports = { deploy };
@@ -5,26 +5,35 @@ const LogV2 = require('../models/log.js');
5
5
  const Log = require('../models/log-v4.js');
6
6
  const Logger = require('../logger.js');
7
7
  const { Deferred } = require('../models/utils.js');
8
+ const colors = require('colors/safe');
8
9
 
9
10
  async function appLogs (params) {
10
- const { alias, after: since, before: until, search, 'deployment-id': deploymentId } = params.options;
11
+ const { alias, addon: addonId, after: since, before: until, search, 'deployment-id': deploymentId, format } = params.options;
11
12
 
12
13
  // ignore --search ""
13
14
  const filter = (search !== '') ? search : null;
14
15
 
15
16
  const { appId, ownerId } = await AppConfig.getAppDetails({ alias });
16
17
 
17
- Logger.println('Waiting for application logs…');
18
+ const isForHuman = (format === 'human');
18
19
 
19
20
  // TODO: drop when addons are migrated to the v4 API
20
- if (params.addon) {
21
- const { addon: addonId } = params.options;
22
- const appAddonId = addonId || await AppConfig.getAppDetails({ alias }).then(({ appId }) => appId);
23
- return LogV2.displayLogs({ appAddonId, since, until, filter, deploymentId });
21
+ if (addonId) {
22
+ if (isForHuman) {
23
+ Logger.println(colors.blue('Waiting for addon logs…'));
24
+ }
25
+ else {
26
+ throw new Error(`"${format}" format is not yet available for add-on logs`);
27
+ }
28
+ return LogV2.displayLogs({ appAddonId: addonId, since, until, filter, deploymentId });
29
+ }
30
+
31
+ if (isForHuman) {
32
+ Logger.println(colors.blue('Waiting for application logs…'));
24
33
  }
25
34
 
26
35
  const deferred = new Deferred();
27
- await Log.displayLogs({ ownerId, appId, since, until, filter, deploymentId, deferred });
36
+ await Log.displayLogs({ ownerId, appId, since, until, filter, deploymentId, format, deferred });
28
37
  return deferred.promise;
29
38
  }
30
39
 
package/src/logger.js CHANGED
@@ -52,6 +52,11 @@ const Logger = _(['debug', 'info', 'warn', 'error'])
52
52
  // No decoration for Logger.println
53
53
  Logger.println = console.log;
54
54
 
55
+ // No decoration for Logger.println
56
+ Logger.printJson = (obj) => {
57
+ console.log(JSON.stringify(obj, null, 2));
58
+ };
59
+
55
60
  // No decoration for Logger.printErrorLine
56
61
  Logger.printErrorLine = console.error;
57
62
 
@@ -17,7 +17,7 @@ function listAvailableTypes () {
17
17
  return autocomplete.words(['docker', 'elixir', 'go', 'gradle', 'haskell', 'jar', 'maven', 'meteor', 'node', 'php', 'play1', 'play2', 'python', 'ruby', 'rust', 'sbt', 'static-apache', 'war']);
18
18
  };
19
19
 
20
- const AVAILABLE_ZONES = ['par', 'rbx', 'rbxhds', 'scw', 'jed', 'mtl', 'sgp', 'syd', 'wsw'];
20
+ const AVAILABLE_ZONES = ['par', 'grahds', 'rbx', 'rbxhds', 'scw', 'jed', 'mtl', 'sgp', 'syd', 'wsw'];
21
21
 
22
22
  function listAvailableZones () {
23
23
  return autocomplete.words(AVAILABLE_ZONES);
package/src/models/git.js CHANGED
@@ -78,7 +78,19 @@ async function getFullBranch (branchName) {
78
78
 
79
79
  async function getBranchCommit (refspec) {
80
80
  const repo = await getRepo();
81
- return git.resolveRef({ ...repo, ref: refspec });
81
+ const oid = await git.resolveRef({ ...repo, ref: refspec });
82
+ // When a refspec refers to an annotated tag, the OID ref represents the annotation and not the commit directly,
83
+ // that's why we need a call to `readCommit`.
84
+ const res = await git.readCommit({ ...repo, ref: refspec, oid });
85
+ return res.oid;
86
+ }
87
+
88
+ async function isExistingTag (tag) {
89
+ const repo = await getRepo();
90
+ const tags = await git.listTags({
91
+ ...repo,
92
+ });
93
+ return tags.includes(tag);
82
94
  }
83
95
 
84
96
  async function push (remoteUrl, branchRefspec, force) {
@@ -131,4 +143,5 @@ module.exports = {
131
143
  push,
132
144
  completeBranches,
133
145
  isShallow,
146
+ isExistingTag,
134
147
  };
@@ -18,7 +18,11 @@ async function displayLogs (params) {
18
18
 
19
19
  const deferred = params.deferred || new Deferred();
20
20
  const { apiHost, tokens } = await getHostAndTokens();
21
- const { ownerId, appId, filter, since, until, deploymentId } = params;
21
+ const { ownerId, appId, filter, since, until, deploymentId, format } = params;
22
+
23
+ if (format === 'json' && until == null) {
24
+ throw new Error('"json" format is only applicable with a limiting parameter such as `--until`');
25
+ }
22
26
 
23
27
  const logStream = new ApplicationLogStream({
24
28
  apiHost,
@@ -37,21 +41,40 @@ async function displayLogs (params) {
37
41
 
38
42
  // Properly close the stream
39
43
  process.once('SIGINT', (signal) => logStream.close(signal));
44
+ const jsonArray = new JsonArray();
40
45
 
41
46
  logStream
42
47
  .on('open', (event) => {
43
48
  Logger.debug(colors.blue(`Logs stream (open) ${JSON.stringify({ appId, filter, deploymentId })}`));
49
+ if (format === 'json') {
50
+ jsonArray.open();
51
+ }
44
52
  })
45
53
  .on('error', (event) => {
46
54
  Logger.debug(colors.red(`Logs stream (error) ${event.error.message}`));
47
55
  })
48
56
  .onLog((log) => {
49
- Logger.println(formatLogLine(log));
57
+ switch (format) {
58
+ case 'json':
59
+ jsonArray.push(log);
60
+ return;
61
+ case 'json-stream':
62
+ Logger.printJson(log);
63
+ return;
64
+ case 'human':
65
+ default:
66
+ Logger.println(formatLogLine(log));
67
+ }
50
68
  });
51
69
 
52
70
  // start() is blocking until end of stream
53
71
  logStream.start()
54
- .then((reason) => deferred.resolve())
72
+ .then((reason) => {
73
+ if (format === 'json') {
74
+ jsonArray.close();
75
+ }
76
+ return deferred.resolve();
77
+ })
55
78
  .catch(processError)
56
79
  .catch((error) => deferred.reject(error));
57
80
 
@@ -142,3 +165,30 @@ function isBuildSucessMessage (log) {
142
165
  };
143
166
 
144
167
  module.exports = { displayLogs, watchDeploymentAndDisplayLogs };
168
+
169
+ /**
170
+ * Helper to print a real JSON array with starting `[` and ending `]`
171
+ */
172
+ class JsonArray {
173
+ constructor () {
174
+ this._isFirst = true;
175
+ }
176
+
177
+ open () {
178
+ process.stdout.write('[\n');
179
+ }
180
+
181
+ push (log) {
182
+ if (this._isFirst) {
183
+ this._isFirst = false;
184
+ }
185
+ else {
186
+ process.stdout.write(',\n');
187
+ }
188
+ process.stdout.write(` ${JSON.stringify(log)}`);
189
+ }
190
+
191
+ close () {
192
+ process.stdout.write('\n]');
193
+ }
194
+ }