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.
- package/README.md +10 -0
- package/bin/clever.js +25 -22
- package/package.json +2 -2
- package/src/clever-client/auth-bridge.js +0 -25
- package/src/commands/README.md +1 -0
- package/src/commands/accesslogs/accesslogs.command.js +4 -1
- package/src/commands/accesslogs/accesslogs.docs.md +1 -1
- package/src/commands/addon/addon.create.command.js +23 -20
- package/src/commands/config/config.set.command.js +3 -3
- package/src/commands/config-provider/config-provider.args.js +8 -0
- package/src/commands/config-provider/config-provider.command.js +10 -0
- package/src/commands/config-provider/config-provider.docs.md +109 -0
- package/src/commands/config-provider/config-provider.get.command.js +36 -0
- package/src/commands/config-provider/config-provider.import.command.js +40 -0
- package/src/commands/config-provider/config-provider.list.command.js +45 -0
- package/src/commands/config-provider/config-provider.open.command.js +17 -0
- package/src/commands/config-provider/config-provider.rm.command.js +25 -0
- package/src/commands/config-provider/config-provider.set.command.js +37 -0
- package/src/commands/create/create.command.js +7 -6
- package/src/commands/curl/curl.command.js +18 -19
- package/src/commands/deploy/deploy.command.js +14 -8
- package/src/commands/deploy/deploy.docs.md +20 -0
- package/src/commands/diag/diag.command.js +52 -39
- package/src/commands/features/features.disable.command.js +1 -2
- package/src/commands/features/features.enable.command.js +1 -2
- package/src/commands/features/features.info.command.js +1 -1
- package/src/commands/features/features.list.command.js +1 -2
- package/src/commands/global.commands.js +22 -0
- package/src/commands/global.options.js +1 -1
- package/src/commands/login/login.command.js +119 -20
- package/src/commands/login/login.docs.md +9 -2
- package/src/commands/logout/logout.command.js +39 -7
- package/src/commands/logout/logout.docs.md +7 -1
- package/src/commands/logs/logs.command.js +7 -8
- package/src/commands/logs/logs.docs.md +1 -1
- package/src/commands/ng/ng.get-config.command.js +3 -3
- package/src/commands/profile/profile.command.js +13 -37
- package/src/commands/profile/profile.docs.md +28 -0
- package/src/commands/profile/profile.list.command.js +44 -0
- package/src/commands/profile/profile.switch.command.js +80 -0
- package/src/commands/restart/restart.command.js +3 -2
- package/src/commands/ssh/ssh.command.js +2 -2
- package/src/commands/tokens/tokens.command.js +3 -3
- package/src/commands/tokens/tokens.create.command.js +4 -4
- package/src/config/cache.js +60 -0
- package/src/config/config.js +233 -0
- package/src/config/features.js +167 -0
- package/src/config/paths.js +23 -0
- package/src/lib/date-utils.js +48 -0
- package/src/lib/fs.js +49 -0
- package/src/lib/operator-commands.js +4 -4
- package/src/lib/profile.js +116 -0
- package/src/logger.js +123 -69
- package/src/logger.types.d.ts +5 -0
- package/src/models/app_configuration.js +42 -39
- package/src/models/application_configuration.js +30 -30
- package/src/models/config-provider.js +34 -0
- package/src/models/git-isomorphic.js +153 -0
- package/src/models/git-system.js +182 -0
- package/src/models/git.js +150 -128
- package/src/models/ids-resolver.js +1 -1
- package/src/models/log.js +141 -90
- package/src/models/send-to-api.js +58 -33
- package/src/models/user.js +0 -11
- package/src/models/utils.js +2 -2
- package/src/experimental-features.js +0 -91
- package/src/lib/format-date.js +0 -3
- package/src/models/configuration.js +0 -140
- package/src/models/log-v4.js +0 -189
package/src/models/log.js
CHANGED
|
@@ -1,108 +1,116 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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 {
|
|
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
|
-
|
|
12
|
-
return line._source.syslog_program === '/home/bas/rubydeployer/deployer.rb';
|
|
13
|
-
}
|
|
13
|
+
const RESET_COLOR = '\x1B[0m';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// 2000 logs per 100ms maximum
|
|
16
|
+
const THROTTLE_ELEMENTS = 2000;
|
|
17
|
+
const THROTTLE_PER_IN_MILLISECONDS = 100;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const retryConfiguration = {
|
|
20
|
+
enabled: true,
|
|
21
|
+
initRetryTimeout: 3000,
|
|
22
|
+
maxRetryCount: 10,
|
|
23
|
+
};
|
|
22
24
|
|
|
23
|
-
function
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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('
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
101
|
+
return logStream;
|
|
60
102
|
}
|
|
61
103
|
|
|
62
|
-
export async function
|
|
63
|
-
const
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
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
|
|
129
|
+
logsStream = await displayLogs({ ownerId, appId, deploymentId: deployment.uuid, since: redeployDate, deferred });
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
|
|
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(
|
|
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
|
|
5
|
-
import {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
46
|
-
.then(
|
|
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
|
|
75
|
-
const tokens = await loadTokens();
|
|
95
|
+
export function getHostAndTokens() {
|
|
76
96
|
return {
|
|
77
|
-
apiHost:
|
|
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
|
}
|
package/src/models/user.js
CHANGED
|
@@ -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
|
-
}
|
package/src/models/utils.js
CHANGED
|
@@ -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('/') ? `${
|
|
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
|
-
};
|
package/src/lib/format-date.js
DELETED
|
@@ -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
|
-
);
|