clever-tools 4.5.2 → 4.6.1

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.
Files changed (69) hide show
  1. package/README.md +10 -0
  2. package/bin/clever.js +25 -22
  3. package/package.json +2 -2
  4. package/src/clever-client/auth-bridge.js +0 -25
  5. package/src/commands/README.md +1 -0
  6. package/src/commands/accesslogs/accesslogs.command.js +4 -1
  7. package/src/commands/accesslogs/accesslogs.docs.md +1 -1
  8. package/src/commands/addon/addon.create.command.js +23 -20
  9. package/src/commands/config/config.set.command.js +3 -3
  10. package/src/commands/config-provider/config-provider.args.js +8 -0
  11. package/src/commands/config-provider/config-provider.command.js +10 -0
  12. package/src/commands/config-provider/config-provider.docs.md +109 -0
  13. package/src/commands/config-provider/config-provider.get.command.js +36 -0
  14. package/src/commands/config-provider/config-provider.import.command.js +40 -0
  15. package/src/commands/config-provider/config-provider.list.command.js +45 -0
  16. package/src/commands/config-provider/config-provider.open.command.js +17 -0
  17. package/src/commands/config-provider/config-provider.rm.command.js +25 -0
  18. package/src/commands/config-provider/config-provider.set.command.js +37 -0
  19. package/src/commands/create/create.command.js +7 -6
  20. package/src/commands/curl/curl.command.js +18 -19
  21. package/src/commands/deploy/deploy.command.js +14 -8
  22. package/src/commands/deploy/deploy.docs.md +20 -0
  23. package/src/commands/diag/diag.command.js +52 -39
  24. package/src/commands/features/features.disable.command.js +1 -2
  25. package/src/commands/features/features.enable.command.js +1 -2
  26. package/src/commands/features/features.info.command.js +1 -1
  27. package/src/commands/features/features.list.command.js +1 -2
  28. package/src/commands/global.commands.js +22 -0
  29. package/src/commands/global.options.js +1 -1
  30. package/src/commands/login/login.command.js +119 -20
  31. package/src/commands/login/login.docs.md +9 -2
  32. package/src/commands/logout/logout.command.js +39 -7
  33. package/src/commands/logout/logout.docs.md +7 -1
  34. package/src/commands/logs/logs.command.js +7 -8
  35. package/src/commands/logs/logs.docs.md +1 -1
  36. package/src/commands/ng/ng.get-config.command.js +3 -3
  37. package/src/commands/profile/profile.command.js +13 -37
  38. package/src/commands/profile/profile.docs.md +28 -0
  39. package/src/commands/profile/profile.list.command.js +44 -0
  40. package/src/commands/profile/profile.switch.command.js +80 -0
  41. package/src/commands/restart/restart.command.js +3 -2
  42. package/src/commands/ssh/ssh.command.js +2 -2
  43. package/src/commands/tokens/tokens.command.js +3 -3
  44. package/src/commands/tokens/tokens.create.command.js +4 -4
  45. package/src/config/cache.js +60 -0
  46. package/src/config/config.js +233 -0
  47. package/src/config/features.js +167 -0
  48. package/src/config/paths.js +23 -0
  49. package/src/lib/date-utils.js +48 -0
  50. package/src/lib/fs.js +49 -0
  51. package/src/lib/operator-commands.js +4 -4
  52. package/src/lib/profile.js +116 -0
  53. package/src/logger.js +123 -69
  54. package/src/logger.types.d.ts +5 -0
  55. package/src/models/app_configuration.js +42 -39
  56. package/src/models/application_configuration.js +30 -30
  57. package/src/models/config-provider.js +34 -0
  58. package/src/models/git-isomorphic.js +153 -0
  59. package/src/models/git-system.js +182 -0
  60. package/src/models/git.js +150 -128
  61. package/src/models/ids-resolver.js +1 -1
  62. package/src/models/log.js +141 -90
  63. package/src/models/send-to-api.js +58 -33
  64. package/src/models/user.js +0 -11
  65. package/src/models/utils.js +2 -2
  66. package/src/experimental-features.js +0 -91
  67. package/src/lib/format-date.js +0 -3
  68. package/src/models/configuration.js +0 -140
  69. package/src/models/log-v4.js +0 -189
package/src/models/log.js CHANGED
@@ -1,108 +1,116 @@
1
- import { getOldLogs } from '@clevercloud/client/esm/api/v2/log.js';
2
- import { LogsStream } from '@clevercloud/client/esm/streams/logs.node.js';
3
- import _ from 'lodash';
1
+ import { ApplicationLogStream } from '@clevercloud/client/esm/streams/application-logs.js';
2
+ import { ResourceLogStream } from '@clevercloud/client/esm/streams/resource-logs.js';
3
+ import { config } from '../config/config.js';
4
4
  import { styleText } from '../lib/style-text.js';
5
5
  import { Logger } from '../logger.js';
6
6
  import { waitForDeploymentEnd, waitForDeploymentStart } from './deployments.js';
7
+ import { getBest } from './domain.js';
7
8
  import * as ExitStrategy from './exit-strategy-option.js';
8
- import { getHostAndTokens, sendToApi } from './send-to-api.js';
9
+ import { JsonArray } from './json-array.js';
10
+ import { getHostAndTokens, processError } from './send-to-api.js';
9
11
  import { Deferred } from './utils.js';
10
12
 
11
- function isCleverMessage(line) {
12
- return line._source.syslog_program === '/home/bas/rubydeployer/deployer.rb';
13
- }
13
+ const RESET_COLOR = '\x1B[0m';
14
14
 
15
- function isDeploymentSuccessMessage(line) {
16
- return isCleverMessage(line) && _.startsWith(line._source['@message'].toLowerCase(), 'successfully deployed in');
17
- }
15
+ // 2000 logs per 100ms maximum
16
+ const THROTTLE_ELEMENTS = 2000;
17
+ const THROTTLE_PER_IN_MILLISECONDS = 100;
18
18
 
19
- function isDeploymentFailedMessage(line) {
20
- return isCleverMessage(line) && _.startsWith(line._source['@message'].toLowerCase(), 'deploy failed in');
21
- }
19
+ const retryConfiguration = {
20
+ enabled: true,
21
+ initRetryTimeout: 3000,
22
+ maxRetryCount: 10,
23
+ };
22
24
 
23
- function isBuildSucessMessage(line) {
24
- return isCleverMessage(line) && _.startsWith(line._source['@message'].toLowerCase(), 'build succeeded in');
25
- }
25
+ export async function displayLogs(params) {
26
+ const deferred = params.deferred || new Deferred();
27
+ const { apiHost, tokens } = await getHostAndTokens();
28
+ const { ownerId, appId, addonId, filter, since, until, format } = params;
29
+ // deploymentId only applies to apps
30
+ const deploymentId = addonId != null ? undefined : params.deploymentId;
26
31
 
27
- function formatLogLine(line) {
28
- const { '@timestamp': timestamp, '@message': message } = line._source;
29
- if (isDeploymentSuccessMessage(line)) {
30
- return `${timestamp}: ${styleText(['bold', 'green'], message)}`;
31
- } else if (isDeploymentFailedMessage(line)) {
32
- return `${timestamp}: ${styleText(['bold', 'red'], message)}`;
33
- } else if (isBuildSucessMessage(line)) {
34
- return `${timestamp}: ${styleText(['bold', 'blue'], message)}`;
32
+ if (format === 'json' && until == null) {
33
+ throw new Error('"json" format is only applicable with a limiting parameter such as `--until`');
35
34
  }
36
- return `${timestamp}: ${message}`;
37
- }
38
35
 
39
- async function displayLiveLogs({ appId, filter, until, deploymentId }, deferred) {
40
- const { apiHost, tokens } = await getHostAndTokens();
41
- const logsStream = new LogsStream({ apiHost, tokens, appId, filter, deploymentId });
42
-
43
- logsStream
44
- .on('open', () => Logger.debug('SSE for logs (open) ' + JSON.stringify({ appId, filter, deploymentId })))
45
- .on('log', (line) => {
46
- const { '@timestamp': timestamp } = line._source;
47
- if (until != null && new Date(timestamp) > until) {
48
- logsStream.close();
49
- } else {
50
- Logger.println(formatLogLine(line));
36
+ const commonStreamParams = {
37
+ apiHost,
38
+ tokens,
39
+ ownerId,
40
+ connectionTimeout: 10_000,
41
+ retryConfiguration,
42
+ since,
43
+ until,
44
+ deploymentId,
45
+ filter,
46
+ throttleElements: THROTTLE_ELEMENTS,
47
+ throttlePerInMilliseconds: THROTTLE_PER_IN_MILLISECONDS,
48
+ };
49
+
50
+ const logStream =
51
+ addonId != null
52
+ ? new ResourceLogStream({ ...commonStreamParams, addonId })
53
+ : new ApplicationLogStream({ ...commonStreamParams, appId });
54
+
55
+ // Properly close the stream
56
+ process.once('SIGINT', (signal) => {
57
+ logStream.close(signal);
58
+ process.kill(process.pid, 'SIGINT');
59
+ });
60
+ const jsonArray = new JsonArray();
61
+
62
+ logStream
63
+ .on('open', () => {
64
+ Logger.debug(styleText('blue', `Logs stream (open) ${JSON.stringify({ appId, addonId, filter, deploymentId })}`));
65
+ if (format === 'json') {
66
+ jsonArray.open();
51
67
  }
52
68
  })
53
- .on('ping', () => Logger.debug('SSE for logs (ping)'))
54
- .on('close', ({ reason }) => Logger.debug('SSE for logs (close) ' + reason))
55
- .on('error', (error) => deferred.reject(error));
56
-
57
- logsStream.open({ autoRetry: true, maxRetryCount: 6 });
69
+ .on('error', (event) => {
70
+ Logger.debug(styleText('red', `Logs stream (error) ${event.error.message}`));
71
+ })
72
+ .onLog((log) => {
73
+ switch (format) {
74
+ case 'json':
75
+ jsonArray.push(log);
76
+ return;
77
+ case 'json-stream':
78
+ Logger.println(JSON.stringify(log));
79
+ return;
80
+ case 'human':
81
+ default:
82
+ if (log.message === RESET_COLOR) {
83
+ return;
84
+ }
85
+ Logger.println(formatLogLine(log));
86
+ }
87
+ });
88
+
89
+ // start() is blocking until end of stream
90
+ logStream
91
+ .start()
92
+ .then(() => {
93
+ if (format === 'json') {
94
+ jsonArray.close();
95
+ }
96
+ return deferred.resolve();
97
+ })
98
+ .catch(processError)
99
+ .catch((error) => deferred.reject(error));
58
100
 
59
- return logsStream;
101
+ return logStream;
60
102
  }
61
103
 
62
- export async function displayLogs({ appAddonId, until, since, filter, deploymentId }) {
63
- const now = new Date();
64
-
65
- const fetchOldLogs = since == null || since < now;
66
- if (fetchOldLogs) {
67
- const oldLogs = await getOldLogs({
68
- appId: appAddonId,
69
- before: until != null ? until.toISOString() : null,
70
- after: since != null ? since.toISOString() : null,
71
- filter,
72
- deployment_id: deploymentId,
73
- }).then(sendToApi);
74
-
75
- for (const line of oldLogs.reverse()) {
76
- Logger.println(formatLogLine(line));
77
- }
78
- }
79
-
80
- // No need to fetch live logs if until date is in the past
81
- if (until != null && until < now) {
82
- return;
83
- }
84
-
85
- const deferred = new Deferred();
104
+ export async function watchDeploymentAndDisplayLogs(options) {
105
+ const { ownerId, appId, deploymentId, commitId, knownDeployments, quiet, redeployDate, exitStrategy } = options;
86
106
 
87
- await displayLiveLogs({ appId: appAddonId, filter, deploymentId, until }, deferred);
88
-
89
- return deferred.promise;
90
- }
91
-
92
- export async function watchDeploymentAndDisplayLogs({
93
- ownerId,
94
- appId,
95
- deploymentId,
96
- commitId,
97
- knownDeployments,
98
- quiet,
99
- exitStrategy,
100
- }) {
101
107
  ExitStrategy.plotQuietWarning(exitStrategy, quiet);
102
108
  // If in quiet mode, we only log start/finished deployment messages
103
- Logger.println('Waiting for deployment to start…');
109
+ if (!quiet) {
110
+ Logger.println(` ${styleText('blue', '→ Waiting for deployment to start…')}`);
111
+ }
104
112
  const deployment = await waitForDeploymentStart({ ownerId, appId, deploymentId, commitId, knownDeployments });
105
- Logger.println(styleText(['bold', 'blue'], `Deployment started (${deployment.uuid})`));
113
+ Logger.println(` ${styleText('green', `✓ Deployment started ${styleText('grey', `(${deployment.uuid})`)}`)}`);
106
114
 
107
115
  if (exitStrategy === 'deploy-start') {
108
116
  return;
@@ -113,15 +121,17 @@ export async function watchDeploymentAndDisplayLogs({
113
121
 
114
122
  if (!quiet) {
115
123
  // About the deferred…
116
- // If displayLiveLogs() throws an error,
124
+ // If displayLogs() throws an error,
117
125
  // the async function we're in (watchDeploymentAndDisplayLogs) will stop here and the error will be passed to the parent.
118
- // displayLiveLogs() defines callback listeners so if it catches error in those callbacks,
126
+ // displayLogs() defines callback listeners so if it catches error in those callbacks,
119
127
  // it has no proper way to bubble up the error here.
120
128
  // Using the deferred enables this.
121
- logsStream = await displayLiveLogs({ appId, deploymentId: deployment.uuid }, deferred);
129
+ logsStream = await displayLogs({ ownerId, appId, deploymentId: deployment.uuid, since: redeployDate, deferred });
122
130
  }
123
131
 
124
- Logger.println('Waiting for application logs…');
132
+ if (!quiet) {
133
+ Logger.println(` ${styleText('blue', '→ Waiting for application logs…')}`);
134
+ }
125
135
 
126
136
  // Wait for deployment end (or an error thrown by logs with the deferred)
127
137
  const deploymentEnded = await Promise.race([
@@ -130,7 +140,7 @@ export async function watchDeploymentAndDisplayLogs({
130
140
  ]);
131
141
 
132
142
  if (!quiet && exitStrategy !== 'never') {
133
- logsStream.close();
143
+ logsStream.close(quiet ? 'quiet' : 'follow');
134
144
  }
135
145
 
136
146
  // deploymentEnded can be undefined if deferred resolved (e.g., stream closed via SIGINT)
@@ -139,10 +149,51 @@ export async function watchDeploymentAndDisplayLogs({
139
149
  }
140
150
 
141
151
  if (deploymentEnded.state === 'OK') {
142
- Logger.println(styleText(['bold', 'green'], 'Deployment successful'));
152
+ Logger.println('');
153
+
154
+ // There can be applications without any domain, so we don't fail if we can't get one
155
+ const favouriteDomain = await getBest(appId, ownerId).catch(() => null);
156
+
157
+ if (favouriteDomain) {
158
+ Logger.println(
159
+ `${styleText(['bold', 'green'], '✓ Access your application:')} ${styleText(['underline', 'bold'], `https://${favouriteDomain.fqdn}`)}`,
160
+ );
161
+ }
162
+
163
+ Logger.println(
164
+ `${styleText(['bold', 'blue'], '→ Manage your application:')} ${styleText(['underline', 'bold'], `${config.GOTO_URL}/${appId}`)}`,
165
+ );
143
166
  } else if (deploymentEnded.state === 'CANCELLED') {
144
167
  throw new Error('Deployment was cancelled. Please check the activity');
145
168
  } else {
146
169
  throw new Error('Deployment failed. Please check the logs');
147
170
  }
148
171
  }
172
+
173
+ function formatLogLine(log) {
174
+ const { date, message } = log;
175
+ if (isDeploymentSuccessMessage(log)) {
176
+ return `${date.toISOString()}: ${styleText(['bold', 'green'], message)}`;
177
+ } else if (isDeploymentFailedMessage(log)) {
178
+ return `${date.toISOString()}: ${styleText(['bold', 'red'], message)}`;
179
+ } else if (isBuildSuccessMessage(log)) {
180
+ return `${date.toISOString()}: ${styleText(['bold', 'blue'], message)}`;
181
+ }
182
+ return `${date.toISOString()}: ${message}${RESET_COLOR}`;
183
+ }
184
+
185
+ function isCleverMessage(log) {
186
+ return log.service !== 'bas-deploy.service';
187
+ }
188
+
189
+ function isDeploymentSuccessMessage(log) {
190
+ return isCleverMessage(log) && log.message.toLowerCase().startsWith('successfully deployed in');
191
+ }
192
+
193
+ function isDeploymentFailedMessage(log) {
194
+ return isCleverMessage(log) && log.message.toLowerCase().startsWith('deploy failed in');
195
+ }
196
+
197
+ function isBuildSuccessMessage(log) {
198
+ return isCleverMessage(log) && log.message.toLowerCase().startsWith('build succeeded in');
199
+ }
@@ -1,49 +1,70 @@
1
1
  import { addOauthHeader } from '@clevercloud/client/esm/oauth.js';
2
2
  import { prefixUrl } from '@clevercloud/client/esm/prefix-url.js';
3
3
  import { request } from '@clevercloud/client/esm/request.fetch.js';
4
- import { subtle as cryptoSuble } from 'node:crypto';
5
- import { addOauthHeaderPlaintext } from '../clever-client/auth-bridge.js';
4
+ import { subtle as cryptoSubtle } from 'node:crypto';
5
+ import { config } from '../config/config.js';
6
6
  import { styleText } from '../lib/style-text.js';
7
7
  import { Logger } from '../logger.js';
8
- import { conf, loadOAuthConf } from './configuration.js';
9
8
 
10
9
  // Required for @clevercloud/client with "old" Node.js
11
10
  if (globalThis.crypto == null) {
12
11
  globalThis.crypto = {
13
- subtle: cryptoSuble,
14
- };
15
- }
16
-
17
- async function loadTokens() {
18
- const tokens = await loadOAuthConf();
19
- return {
20
- OAUTH_CONSUMER_KEY: conf.OAUTH_CONSUMER_KEY,
21
- OAUTH_CONSUMER_SECRET: conf.OAUTH_CONSUMER_SECRET,
22
- API_OAUTH_TOKEN: tokens.token,
23
- API_OAUTH_TOKEN_SECRET: tokens.secret,
12
+ subtle: cryptoSubtle,
24
13
  };
25
14
  }
26
15
 
27
16
  export async function sendToApi(requestParams) {
28
- const tokens = await loadTokens();
29
- return Promise.resolve(requestParams)
30
- .then(prefixUrl(conf.API_HOST))
31
- .then(addOauthHeader(tokens))
32
- .then((requestParams) => {
33
- Logger.debug(
34
- `${requestParams.method.toUpperCase()} ${requestParams.url} ? ${JSON.stringify(requestParams.queryParams)}`,
35
- );
36
- return requestParams;
37
- })
38
- .then(request)
39
- .catch(processError);
17
+ return executeRequest(requestParams);
40
18
  }
41
19
 
42
20
  export async function sendToAuthBridge(requestParams) {
43
- const tokens = await loadTokens();
21
+ return executeRequest(requestParams, { API_HOST: config.AUTH_BRIDGE_HOST });
22
+ }
23
+
24
+ /**
25
+ * Creates an API request function with a custom config.
26
+ * @param {object} customConfig
27
+ * @param {string} customConfig.token - OAuth token
28
+ * @param {string} customConfig.secret - OAuth secret
29
+ * @param {string} customConfig.apiHost - API host
30
+ * @param {string} customConfig.consumerKey - OAuth consumer key
31
+ * @param {string} customConfig.consumerSecret - OAuth consumer secret
32
+ * @returns {(requestParams: object) => Promise<unknown>}
33
+ */
34
+ export function sendToApiWithConfig(customConfig) {
35
+ return (requestParams) =>
36
+ executeRequest(requestParams, {
37
+ API_HOST: customConfig.apiHost,
38
+ OAUTH_CONSUMER_KEY: customConfig.consumerKey,
39
+ OAUTH_CONSUMER_SECRET: customConfig.consumerSecret,
40
+ API_OAUTH_TOKEN: customConfig.token,
41
+ API_OAUTH_TOKEN_SECRET: customConfig.secret,
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Executes the request pipeline with the given configuration.
47
+ * @param {object} requestParams - The request parameters
48
+ * @param {object} [customConfig] - Optional configuration overrides
49
+ * @param {string} [customConfig.API_HOST] - API host URL
50
+ * @param {string} [customConfig.OAUTH_CONSUMER_KEY] - OAuth consumer key
51
+ * @param {string} [customConfig.OAUTH_CONSUMER_SECRET] - OAuth consumer secret
52
+ * @param {string} [customConfig.API_OAUTH_TOKEN] - OAuth token
53
+ * @param {string} [customConfig.API_OAUTH_TOKEN_SECRET] - OAuth token secret
54
+ * @returns {Promise<unknown>}
55
+ */
56
+ async function executeRequest(requestParams, customConfig = {}) {
57
+ const host = customConfig.API_HOST ?? config.API_HOST;
58
+ const tokens = {
59
+ OAUTH_CONSUMER_KEY: customConfig.OAUTH_CONSUMER_KEY ?? config.OAUTH_CONSUMER_KEY,
60
+ OAUTH_CONSUMER_SECRET: customConfig.OAUTH_CONSUMER_SECRET ?? config.OAUTH_CONSUMER_SECRET,
61
+ API_OAUTH_TOKEN: customConfig.API_OAUTH_TOKEN ?? config.token,
62
+ API_OAUTH_TOKEN_SECRET: customConfig.API_OAUTH_TOKEN_SECRET ?? config.secret,
63
+ };
64
+
44
65
  return Promise.resolve(requestParams)
45
- .then(prefixUrl(conf.AUTH_BRIDGE_HOST))
46
- .then(addOauthHeaderPlaintext(tokens))
66
+ .then(prefixUrl(host))
67
+ .then(addOauthHeader(tokens))
47
68
  .then((requestParams) => {
48
69
  Logger.debug(
49
70
  `${requestParams.method.toUpperCase()} ${requestParams.url} ? ${JSON.stringify(requestParams.queryParams)}`,
@@ -71,10 +92,14 @@ export function processError(error) {
71
92
  throw error;
72
93
  }
73
94
 
74
- export async function getHostAndTokens() {
75
- const tokens = await loadTokens();
95
+ export function getHostAndTokens() {
76
96
  return {
77
- apiHost: conf.API_HOST,
78
- tokens,
97
+ apiHost: config.API_HOST,
98
+ tokens: {
99
+ OAUTH_CONSUMER_KEY: config.OAUTH_CONSUMER_KEY,
100
+ OAUTH_CONSUMER_SECRET: config.OAUTH_CONSUMER_SECRET,
101
+ API_OAUTH_TOKEN: config.token,
102
+ API_OAUTH_TOKEN_SECRET: config.secret,
103
+ },
79
104
  };
80
105
  }
@@ -10,14 +10,3 @@ export function getCurrentId() {
10
10
  .then(sendToApi)
11
11
  .then(({ id }) => id);
12
12
  }
13
-
14
- // TODO move to clever client
15
- export function getCurrentToken() {
16
- return Promise.resolve({
17
- method: 'get',
18
- url: '/v2/self/tokens/current',
19
- headers: { Accept: 'application/json' },
20
- // no query params
21
- // no body
22
- }).then(sendToApi);
23
- }
@@ -1,6 +1,6 @@
1
1
  import openPage from 'open';
2
+ import { config } from '../config/config.js';
2
3
  import { Logger } from '../logger.js';
3
- import { conf } from './configuration.js';
4
4
 
5
5
  // Inspirations:
6
6
  // https://github.com/sindresorhus/p-defer/blob/master/index.js
@@ -23,7 +23,7 @@ export class Deferred {
23
23
  * @returns {Promise<void>} A promise that resolves when the URL is opened
24
24
  */
25
25
  export function openBrowser(urlOrPath, message) {
26
- const url = urlOrPath.startsWith('/') ? `${conf.CONSOLE_URL}${urlOrPath}` : urlOrPath;
26
+ const url = urlOrPath.startsWith('/') ? `${config.CONSOLE_URL}${urlOrPath}` : urlOrPath;
27
27
 
28
28
  Logger.debug(`Opening URL "${url}" in browser`);
29
29
  Logger.println(message);
@@ -1,91 +0,0 @@
1
- import dedent from 'dedent';
2
- import { conf } from './models/configuration.js';
3
-
4
- export const EXPERIMENTAL_FEATURES = {
5
- k8s: {
6
- status: 'beta',
7
- description: 'Deploy and manage Kubernetes clusters on Clever Cloud',
8
- instructions: dedent`
9
- - Create a Kubernetes cluster:
10
- clever k8s create my-cluster
11
- clever k8s create my-cluster --org myOrg
12
-
13
- - List Kubernetes clusters:
14
- clever k8s list
15
-
16
- - Get details about a Kubernetes cluster:
17
- clever k8s get my-cluster
18
-
19
- - Get kubeconfig file for a Kubernetes cluster:
20
- clever k8s get-kubeconfig my-cluster
21
- clever k8s get-kubeconfig my-cluster > ~/.kube/config
22
-
23
- - Activate persistent storage on a Kubernetes cluster:
24
- clever k8s add-persistent-storage my-cluster
25
-
26
- - Delete a Kubernetes cluster:
27
- clever k8s delete my-cluster
28
-
29
- Learn more about Clever Kubernetes: ${conf.DOC_URL}/kubernetes/
30
- `,
31
- },
32
- kv: {
33
- status: 'beta',
34
- description:
35
- 'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
36
- instructions: dedent`
37
- Target any compatible add-on by its name or ID (with an org ID if needed) and send commands to it:
38
-
39
- clever kv myMateriaKV SET myKey myValue
40
- clever kv kv_xxxxxxxx GET myKey -F json
41
- clever kv addon_xxxxx SET myTempKey myTempValue EX 120
42
- clever kv myMateriaKV -o myOrg TTL myTempKey
43
- clever kv redis_xxxxx --org org_xxxxx PING
44
-
45
- Learn more about Materia KV: ${conf.DOC_URL}/addons/materia-kv/
46
- `,
47
- },
48
- ng: {
49
- status: 'beta',
50
- description: 'Manage Network Groups to manage applications, add-ons, external peers through a WireGuard network',
51
- instructions: dedent`
52
- - Create a Network Group:
53
- clever ng create myNG
54
- - Create a Network Group with members (application, database add-on):
55
- clever ng create myNG --link app_xxx,postgresql_xxx
56
- - List Network Groups:
57
- clever ng
58
- - Delete a Network Group:
59
- clever ng delete myNG
60
- - (Un)Link an application or a database add-on to an existing Network Group:
61
- clever ng link app_xxx myNG
62
- clever ng unlink postgresql_xxx myNG
63
- - Get the WireGuard configuration of a peer:
64
- clever ng get-config peerIdOrLabel myNG
65
- - Get details about a Network Group, a member or a peer:
66
- clever ng get myNg
67
- clever ng get app_xxx
68
- clever ng get peerId
69
- clever ng get memberLabel
70
- - Search Network Groups, members or peers:
71
- clever ng search myQuery
72
-
73
- Learn more about Network Groups: ${conf.DOC_URL}/develop/network-groups/
74
- `,
75
- },
76
- operators: {
77
- status: 'beta',
78
- description: 'Manage operators and their features such as Keycloak, Matomo, Metabase, Otoroshi',
79
- instructions: dedent`
80
- clever keycloak
81
- clever keycloak get keycloak_xxx
82
- clever keycloak ng enable myKeycloak
83
-
84
- clever metabase version check myMetabase
85
- clever metabase version update myMetabase 0.53
86
-
87
- clever matomo open myMatomo
88
- clever otoroshi open logs myOtoroshi
89
- `,
90
- },
91
- };
@@ -1,3 +0,0 @@
1
- export function formatDate(dateInput) {
2
- return new Date(dateInput).toISOString().substring(0, 16).replace('T', ' ');
3
- }
@@ -1,140 +0,0 @@
1
- import { promises as fs } from 'node:fs';
2
- import path from 'node:path';
3
- import xdg from 'xdg';
4
- import { Logger } from '../logger.js';
5
-
6
- const CONFIG_FILES = {
7
- MAIN: 'clever-tools.json',
8
- IDS_CACHE: 'ids-cache.json',
9
- EXPERIMENTAL_FEATURES_FILE: 'clever-tools-experimental-features.json',
10
- };
11
-
12
- function getConfigDir() {
13
- return process.platform === 'win32'
14
- ? path.resolve(process.env.APPDATA, 'clever-cloud')
15
- : xdg.basedir.configPath('clever-cloud');
16
- }
17
-
18
- function getConfigPath(configFile) {
19
- return path.resolve(getConfigDir(), configFile);
20
- }
21
-
22
- // Every function which need 'clever-cloud' directory, need to call it before
23
- async function ensureConfigDirExists() {
24
- await fs.mkdir(getConfigDir(), { mode: 0o700, recursive: true });
25
- }
26
-
27
- export async function loadOAuthConf() {
28
- Logger.debug('Load configuration from environment variables');
29
- if (process.env.CLEVER_TOKEN != null && process.env.CLEVER_SECRET != null) {
30
- return {
31
- source: 'environment variables',
32
- token: process.env.CLEVER_TOKEN,
33
- secret: process.env.CLEVER_SECRET,
34
- };
35
- }
36
- Logger.debug('Load configuration from ' + conf.CONFIGURATION_FILE);
37
- try {
38
- const rawFile = await fs.readFile(conf.CONFIGURATION_FILE);
39
- const { token, secret } = JSON.parse(rawFile);
40
- return {
41
- source: 'configuration file',
42
- token,
43
- secret,
44
- };
45
- } catch (error) {
46
- Logger.info(`Cannot load configuration from ${conf.CONFIGURATION_FILE}\n${error.message}`);
47
- return {
48
- source: 'none',
49
- };
50
- }
51
- }
52
-
53
- export async function writeOAuthConf(oauthData) {
54
- Logger.debug('Write the tokens in the configuration file…');
55
- try {
56
- await ensureConfigDirExists();
57
- await fs.writeFile(conf.CONFIGURATION_FILE, JSON.stringify(oauthData));
58
- } catch (error) {
59
- throw new Error(`Cannot write configuration to ${conf.CONFIGURATION_FILE}\n${error.message}`);
60
- }
61
- }
62
-
63
- export async function loadIdsCache() {
64
- const cachePath = getConfigPath(CONFIG_FILES.IDS_CACHE);
65
- try {
66
- const rawFile = await fs.readFile(cachePath);
67
- return JSON.parse(rawFile);
68
- } catch (error) {
69
- Logger.info(`Cannot load IDs cache from ${cachePath}\n${error.message}`);
70
- return {
71
- owners: {},
72
- addons: {},
73
- };
74
- }
75
- }
76
-
77
- export async function writeIdsCache(ids) {
78
- const cachePath = getConfigPath(CONFIG_FILES.IDS_CACHE);
79
- const idsJson = JSON.stringify(ids);
80
- try {
81
- await ensureConfigDirExists();
82
- await fs.writeFile(cachePath, idsJson);
83
- } catch (error) {
84
- throw new Error(`Cannot write IDs cache to ${cachePath}\n${error.message}`);
85
- }
86
- }
87
-
88
- export async function getFeatures() {
89
- Logger.debug('Get features configuration from ' + conf.EXPERIMENTAL_FEATURES_FILE);
90
- try {
91
- const rawFile = await fs.readFile(conf.EXPERIMENTAL_FEATURES_FILE);
92
- return JSON.parse(rawFile);
93
- } catch (error) {
94
- if (error.code !== 'ENOENT') {
95
- throw new Error(`Cannot get experimental features configuration from ${conf.EXPERIMENTAL_FEATURES_FILE}`);
96
- }
97
- return {};
98
- }
99
- }
100
-
101
- export async function setFeature(feature, value) {
102
- const currentFeatures = await getFeatures();
103
- const newFeatures = { ...currentFeatures, ...{ [feature]: value } };
104
-
105
- try {
106
- await ensureConfigDirExists();
107
- await fs.writeFile(conf.EXPERIMENTAL_FEATURES_FILE, JSON.stringify(newFeatures, null, 2));
108
- } catch {
109
- throw new Error(`Cannot write experimental features configuration to ${conf.EXPERIMENTAL_FEATURES_FILE}`);
110
- }
111
- }
112
-
113
- const defaultConf = {
114
- API_HOST: 'https://api.clever-cloud.com',
115
- AUTH_BRIDGE_HOST: 'https://api-bridge.clever-cloud.com',
116
- SSH_GATEWAY: 'ssh@sshgateway-clevercloud-customers.services.clever-cloud.com',
117
-
118
- // the disclosure of these tokens is not considered as a vulnerability. Do not report this to our security service.
119
- OAUTH_CONSUMER_KEY: 'T5nFjKeHH4AIlEveuGhB5S3xg8T19e',
120
- OAUTH_CONSUMER_SECRET: 'MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT',
121
-
122
- APP_CONFIGURATION_FILE: path.resolve('.', '.clever.json'),
123
- CONFIGURATION_FILE: getConfigPath(CONFIG_FILES.MAIN),
124
- EXPERIMENTAL_FEATURES_FILE: getConfigPath(CONFIG_FILES.EXPERIMENTAL_FEATURES_FILE),
125
-
126
- API_DOC_URL: 'https://www.clever.cloud/developers/api',
127
- DOC_URL: 'https://www.clever.cloud/developers/doc',
128
- CONSOLE_URL: 'https://console.clever-cloud.com',
129
- CONSOLE_TOKEN_URL: 'https://console.clever-cloud.com/cli-oauth',
130
- GOTO_URL: 'https://console.clever-cloud.com/goto',
131
- };
132
-
133
- export const conf = Object.fromEntries(
134
- Object.entries(defaultConf).map(([name, value]) => {
135
- if (process.env[name] != null) {
136
- return [name, process.env[name]];
137
- }
138
- return [name, value];
139
- }),
140
- );