clever-tools 4.11.0 → 5.0.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/bin/clever.js +5 -10
- package/package.json +27 -27
- package/src/commands/accesslogs/accesslogs.command.js +27 -19
- package/src/commands/accesslogs/accesslogs.docs.md +1 -1
- package/src/commands/addon/addon.docs.md +2 -2
- package/src/commands/config-provider/config-provider.docs.md +5 -5
- package/src/commands/database/database.docs.md +2 -2
- package/src/commands/deploy/deploy.docs.md +7 -7
- package/src/commands/drain/drain.args.js +6 -0
- package/src/commands/drain/drain.create.betterstack.command.js +36 -0
- package/src/commands/drain/drain.create.command.js +0 -132
- package/src/commands/drain/drain.create.datadog.command.js +27 -0
- package/src/commands/drain/drain.create.elasticsearch.command.js +48 -0
- package/src/commands/drain/drain.create.newrelic.command.js +36 -0
- package/src/commands/drain/drain.create.ovh-tcp.command.js +29 -0
- package/src/commands/drain/drain.create.raw-http.command.js +30 -0
- package/src/commands/drain/drain.create.splunk.command.js +61 -0
- package/src/commands/drain/drain.create.syslog-tcp.command.js +29 -0
- package/src/commands/drain/drain.create.syslog-udp.command.js +29 -0
- package/src/commands/drain/drain.docs.md +273 -8
- package/src/commands/drain/drain.options.js +26 -0
- package/src/commands/features/features.list.command.js +6 -13
- package/src/commands/global.commands.js +23 -1
- package/src/commands/global.options.js +1 -1
- package/src/commands/k8s/k8s.docs.md +17 -17
- package/src/commands/keycloak/keycloak.docs.md +11 -11
- package/src/commands/kv/kv.docs.md +1 -1
- package/src/commands/link/link.docs.md +1 -1
- package/src/commands/login/login.command.js +11 -1
- package/src/commands/matomo/matomo.docs.md +6 -6
- package/src/commands/metabase/metabase.docs.md +9 -9
- package/src/commands/ng/ng.docs.md +10 -10
- package/src/commands/oauth-consumers/oauth-consumers.docs.md +4 -4
- package/src/commands/otoroshi/otoroshi.docs.md +13 -13
- package/src/commands/service/service.docs.md +4 -4
- package/src/commands/ssh/ssh.command.js +6 -0
- package/src/config/features.js +61 -29
- package/src/format-table.js +14 -10
- package/src/initial-setup.js +13 -0
- package/src/initial-update-notifier.js +2 -5
- package/src/lib/access-logs-transport.js +29 -0
- package/src/lib/cliparse-patched.js +1 -1
- package/src/models/drain.js +39 -13
- package/src/models/git-system.js +2 -2
- package/src/models/git.js +1 -1
package/src/config/features.js
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import z from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import { readJsonSync, writeJson } from '../lib/fs.js';
|
|
4
4
|
import { Logger } from '../logger.js';
|
|
5
5
|
import { config } from './config.js';
|
|
6
6
|
import { getConfigPath } from './paths.js';
|
|
7
7
|
|
|
8
8
|
const EXPERIMENTAL_FEATURES_FILEPATH = getConfigPath('clever-tools-experimental-features.json');
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} ExperimentalFeature
|
|
12
|
+
* @property {'beta'|'stable'} status
|
|
13
|
+
* @property {boolean} defaultValue - Value used when the feature is not explicitly set by the user
|
|
14
|
+
* @property {string} description
|
|
15
|
+
* @property {string} [instructions]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @type {Record<string, ExperimentalFeature>} */
|
|
10
19
|
export const EXPERIMENTAL_FEATURES = {
|
|
11
20
|
'system-git': {
|
|
12
|
-
status: '
|
|
13
|
-
|
|
21
|
+
status: 'stable',
|
|
22
|
+
defaultValue: true,
|
|
23
|
+
description: 'Use system git instead of the pure JS implementation for git operations',
|
|
14
24
|
instructions: dedent`
|
|
15
|
-
This feature switches from the
|
|
16
|
-
the git installed on your system.
|
|
25
|
+
This feature switches from the pure JS implementation to using
|
|
26
|
+
the git installed on your system. It is enabled by default since v5.0.0.
|
|
17
27
|
|
|
18
28
|
Requirements:
|
|
19
29
|
- git must be installed and available in your PATH
|
|
30
|
+
|
|
31
|
+
Disable it to fall back to the previous pure JS implementation:
|
|
32
|
+
clever features disable system-git
|
|
20
33
|
`,
|
|
21
34
|
},
|
|
22
35
|
k8s: {
|
|
23
36
|
status: 'beta',
|
|
37
|
+
defaultValue: false,
|
|
24
38
|
description: 'Deploy and manage Kubernetes clusters on Clever Cloud',
|
|
25
39
|
instructions: dedent`
|
|
26
40
|
- Create a Kubernetes cluster:
|
|
@@ -48,6 +62,7 @@ export const EXPERIMENTAL_FEATURES = {
|
|
|
48
62
|
},
|
|
49
63
|
kv: {
|
|
50
64
|
status: 'beta',
|
|
65
|
+
defaultValue: false,
|
|
51
66
|
description:
|
|
52
67
|
'Send commands to databases such as Materia KV or Redis® directly from Clever Tools, without other dependencies',
|
|
53
68
|
instructions: dedent`
|
|
@@ -64,6 +79,7 @@ export const EXPERIMENTAL_FEATURES = {
|
|
|
64
79
|
},
|
|
65
80
|
ng: {
|
|
66
81
|
status: 'beta',
|
|
82
|
+
defaultValue: false,
|
|
67
83
|
description: 'Manage Network Groups to manage applications, add-ons, external peers through a WireGuard network',
|
|
68
84
|
instructions: dedent`
|
|
69
85
|
- Create a Network Group:
|
|
@@ -92,6 +108,7 @@ export const EXPERIMENTAL_FEATURES = {
|
|
|
92
108
|
},
|
|
93
109
|
operators: {
|
|
94
110
|
status: 'beta',
|
|
111
|
+
defaultValue: false,
|
|
95
112
|
description: 'Manage operators and their features such as Keycloak, Matomo, Metabase, Otoroshi',
|
|
96
113
|
instructions: dedent`
|
|
97
114
|
clever keycloak
|
|
@@ -116,52 +133,67 @@ const FeaturesConfigSchema = z
|
|
|
116
133
|
*/
|
|
117
134
|
|
|
118
135
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* @returns {Promise<FeaturesConfig>} The features configuration object
|
|
136
|
+
* The features explicitly set by the user, loaded synchronously at startup.
|
|
137
|
+
* @type {FeaturesConfig}
|
|
122
138
|
*/
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
let userFeatures = loadFeatures();
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Reads and parses the features file.
|
|
143
|
+
* Returns an empty object if the file doesn't exist or is invalid.
|
|
144
|
+
* @returns {FeaturesConfig} The features configuration object
|
|
145
|
+
*/
|
|
146
|
+
function loadFeatures() {
|
|
147
|
+
Logger.debug(`Load features configuration from ${EXPERIMENTAL_FEATURES_FILEPATH}`);
|
|
148
|
+
const rawFeatures = readJsonSync(EXPERIMENTAL_FEATURES_FILEPATH);
|
|
149
|
+
if (rawFeatures == null) {
|
|
150
|
+
return {};
|
|
151
|
+
}
|
|
152
|
+
const parsed = FeaturesConfigSchema.safeParse(rawFeatures);
|
|
153
|
+
if (!parsed.success) {
|
|
154
|
+
Logger.info(`Invalid features format in ${EXPERIMENTAL_FEATURES_FILEPATH}`);
|
|
137
155
|
return {};
|
|
138
156
|
}
|
|
157
|
+
return parsed.data;
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
/**
|
|
142
161
|
* Sets an experimental feature to the specified value.
|
|
143
|
-
* Creates the configuration directory and features file if they don't exist
|
|
162
|
+
* Creates the configuration directory and features file if they don't exist,
|
|
163
|
+
* then reloads the in-memory configuration.
|
|
144
164
|
* @param {string} feature - The name of the feature to set
|
|
145
165
|
* @param {boolean} value - The value to set for the feature
|
|
146
166
|
* @returns {Promise<void>}
|
|
147
167
|
* @throws {Error} If the features file cannot be written
|
|
148
168
|
*/
|
|
149
169
|
export async function setFeature(feature, value) {
|
|
150
|
-
const
|
|
151
|
-
const newFeatures = { ...currentFeatures, [feature]: value };
|
|
170
|
+
const newFeatures = { ...userFeatures, [feature]: value };
|
|
152
171
|
try {
|
|
153
172
|
await writeJson(EXPERIMENTAL_FEATURES_FILEPATH, newFeatures, { mode: 0o700 });
|
|
154
173
|
} catch (error) {
|
|
155
174
|
throw new Error(`Cannot write experimental features configuration to ${EXPERIMENTAL_FEATURES_FILEPATH}`);
|
|
156
175
|
}
|
|
176
|
+
userFeatures = loadFeatures();
|
|
157
177
|
}
|
|
158
178
|
|
|
159
179
|
/**
|
|
160
180
|
* Checks if an experimental feature is enabled.
|
|
181
|
+
* Falls back to the feature's `defaultValue` when the user hasn't set it explicitly.
|
|
161
182
|
* @param {string} feature - The name of the feature to check
|
|
162
|
-
* @returns {
|
|
183
|
+
* @returns {boolean} True if the feature is enabled
|
|
184
|
+
*/
|
|
185
|
+
export function isFeatureEnabled(feature) {
|
|
186
|
+
return userFeatures[feature] ?? EXPERIMENTAL_FEATURES[feature]?.defaultValue ?? false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Lists all experimental features with their current status.
|
|
191
|
+
* @returns {Array<{ id: string, status: string, description: string, instructions?: string, enabled: boolean }>}
|
|
163
192
|
*/
|
|
164
|
-
export
|
|
165
|
-
|
|
166
|
-
|
|
193
|
+
export function getAllFeatures() {
|
|
194
|
+
return Object.entries(EXPERIMENTAL_FEATURES).map(([id, feature]) => ({
|
|
195
|
+
id,
|
|
196
|
+
...feature,
|
|
197
|
+
enabled: isFeatureEnabled(id),
|
|
198
|
+
}));
|
|
167
199
|
}
|
package/src/format-table.js
CHANGED
|
@@ -36,16 +36,20 @@ export const formatTable = (data, columnWidth = []) => {
|
|
|
36
36
|
return data
|
|
37
37
|
.map((row) => row.map((cell) => String(cell)))
|
|
38
38
|
.map((row) => {
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
return (
|
|
40
|
+
row
|
|
41
|
+
.map((cell, index) => {
|
|
42
|
+
const isLastColumn = index === row.length - 1;
|
|
43
|
+
if (isLastColumn) {
|
|
44
|
+
return cell;
|
|
45
|
+
}
|
|
46
|
+
const rightPaddingLength = columnSizes[index] - stringLength(cell) ?? 0;
|
|
47
|
+
return cell + ' '.repeat(rightPaddingLength);
|
|
48
|
+
})
|
|
49
|
+
.join(SEPARATOR)
|
|
50
|
+
// empty trailing cells would otherwise pad the line with meaningless spaces
|
|
51
|
+
.trimEnd()
|
|
52
|
+
);
|
|
49
53
|
})
|
|
50
54
|
.join('\n');
|
|
51
55
|
};
|
package/src/initial-setup.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici';
|
|
1
2
|
import { hasParam } from './lib/has-param.js';
|
|
2
3
|
|
|
4
|
+
// Node's global fetch() ignores the http_proxy/https_proxy environment variables,
|
|
5
|
+
// unlike most CLI tools (curl, kubectl, s3cmd…). When such a variable is set, install
|
|
6
|
+
// an EnvHttpProxyAgent as the global dispatcher so every fetch() (API calls, update
|
|
7
|
+
// checks…) routes through the proxy and honors no_proxy for exclusions. We only swap
|
|
8
|
+
// the dispatcher when a proxy is actually configured, leaving the default behavior
|
|
9
|
+
// untouched for everyone else.
|
|
10
|
+
const hasProxyConfig =
|
|
11
|
+
process.env.http_proxy || process.env.https_proxy || process.env.HTTP_PROXY || process.env.HTTPS_PROXY;
|
|
12
|
+
if (hasProxyConfig) {
|
|
13
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
14
|
+
}
|
|
15
|
+
|
|
3
16
|
// These need to be set before Logger and other stuffs
|
|
4
17
|
if (hasParam('-v') || hasParam('--verbose')) {
|
|
5
18
|
process.env.CLEVER_VERBOSE = '1';
|
|
@@ -5,14 +5,11 @@ import { hasParam } from './lib/has-param.js';
|
|
|
5
5
|
// These need to be set before Logger and other stuffs
|
|
6
6
|
const updateNotifierExplicitFalse = hasParam('--no-update-notifier') || hasParam('--update-notifier', 'false');
|
|
7
7
|
if (!updateNotifierExplicitFalse) {
|
|
8
|
+
const docsUrl = 'https://github.com/CleverCloud/clever-tools/blob/master/docs/update.md';
|
|
8
9
|
updateNotifierModule({
|
|
9
10
|
pkg,
|
|
10
|
-
tagsUrl: 'https://api.github.com/repos/CleverCloud/clever-tools/tags',
|
|
11
11
|
}).notify({
|
|
12
12
|
isGlobal: true,
|
|
13
|
-
|
|
14
|
-
const docsUrl = 'https://github.com/CleverCloud/clever-tools/tree/master/docs#how-to-use-clever-tools';
|
|
15
|
-
return `\nPlease follow this link to update your clever-tools:\n${docsUrl}`;
|
|
16
|
-
},
|
|
13
|
+
message: `Update available {currentVersion} -> {latestVersion}\nPlease follow this link to update your clever-tools:\n${docsUrl}`,
|
|
17
14
|
});
|
|
18
15
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport used by the connection an access log describes.
|
|
3
|
+
* @typedef {'HTTP'|'TCP'|'SSH'} AccessLogTransport
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guess the transport of an access log.
|
|
8
|
+
*
|
|
9
|
+
* The v4 access log payload has no explicit transport field yet, so it is inferred from the
|
|
10
|
+
* sections the API did send:
|
|
11
|
+
*
|
|
12
|
+
* - an `http` section is only emitted for connections that went through the HTTP reverse proxy,
|
|
13
|
+
* - TCP redirections go through the TCP proxy, which assigns a `requestId` but no `http` section,
|
|
14
|
+
* - direct SSH connections to the instance go through neither, so they carry neither field.
|
|
15
|
+
*
|
|
16
|
+
* Drop this function once the API exposes the transport itself.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} log an access log, as emitted by `ApplicationAccessLogStream`
|
|
19
|
+
* @returns {AccessLogTransport}
|
|
20
|
+
*/
|
|
21
|
+
export function guessAccessLogTransport(log) {
|
|
22
|
+
if (log.http != null) {
|
|
23
|
+
return 'HTTP';
|
|
24
|
+
}
|
|
25
|
+
if (log.requestId != null) {
|
|
26
|
+
return 'TCP';
|
|
27
|
+
}
|
|
28
|
+
return 'SSH';
|
|
29
|
+
}
|
|
@@ -179,7 +179,7 @@ function wrapParser(parser) {
|
|
|
179
179
|
// instead of cliparse.autocomplete.words(array)
|
|
180
180
|
function wrapComplete(complete) {
|
|
181
181
|
return (word) => {
|
|
182
|
-
// Support both functions and static arrays (e.g. complete:
|
|
182
|
+
// Support both functions and static arrays (e.g. complete: ['human', 'json'])
|
|
183
183
|
const result = typeof complete === 'function' ? complete(word) : complete;
|
|
184
184
|
return Promise.resolve(result).then(cliparse.autocomplete.words);
|
|
185
185
|
};
|
package/src/models/drain.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createDrain } from '../clever-client/drains.js';
|
|
1
2
|
import * as Application from './application.js';
|
|
2
3
|
import { resolveAddon } from './ids-resolver.js';
|
|
4
|
+
import { sendToApi } from './send-to-api.js';
|
|
3
5
|
|
|
4
6
|
export async function resolveDrainResource(alias, appIdOrName, addonIdOrRealId) {
|
|
5
7
|
if (addonIdOrRealId != null && (appIdOrName != null || alias != null)) {
|
|
@@ -15,18 +17,37 @@ export async function resolveDrainResource(alias, appIdOrName, addonIdOrRealId)
|
|
|
15
17
|
return { ownerId, resourceId: appId };
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
20
|
+
/**
|
|
21
|
+
* Creates a log drain, the recipient options that are not set are left out of the payload.
|
|
22
|
+
* @param {string} type - Drain type, as expected by the API
|
|
23
|
+
* @param {string} ownerId
|
|
24
|
+
* @param {string} resourceId
|
|
25
|
+
* @param {string} url - Drain URL
|
|
26
|
+
* @param {Record<string, unknown>} [recipientOptions] - Extra recipient fields, `null` and `undefined` ones are ignored
|
|
27
|
+
*/
|
|
28
|
+
export function createLogDrain(type, ownerId, resourceId, url, recipientOptions = {}) {
|
|
29
|
+
const body = { kind: 'LOG', recipient: { type, url } };
|
|
30
|
+
|
|
31
|
+
for (const key in recipientOptions) {
|
|
32
|
+
if (recipientOptions[key] != null) {
|
|
33
|
+
body.recipient[key] = recipientOptions[key];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return createDrain({ ownerId, resourceId, body }).then(sendToApi);
|
|
38
|
+
}
|
|
28
39
|
|
|
29
|
-
export const
|
|
40
|
+
export const DRAIN_TYPE_LABELS = {
|
|
41
|
+
BETTERSTACK: 'Better Stack',
|
|
42
|
+
DATADOG: 'Datadog',
|
|
43
|
+
ELASTICSEARCH: 'Elasticsearch',
|
|
44
|
+
NEWRELIC: 'New Relic',
|
|
45
|
+
OVH_TCP: 'OVH TCP',
|
|
46
|
+
RAW_HTTP: 'Raw HTTP',
|
|
47
|
+
SPLUNK: 'Splunk',
|
|
48
|
+
SYSLOG_TCP: 'Syslog TCP',
|
|
49
|
+
SYSLOG_UDP: 'Syslog UDP',
|
|
50
|
+
};
|
|
30
51
|
|
|
31
52
|
function formatRate(messagesPerSecond) {
|
|
32
53
|
if (messagesPerSecond < 1) {
|
|
@@ -49,14 +70,19 @@ function formatThroughput(bytesPerSecond) {
|
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
export function formatDrain(rawDrain) {
|
|
52
|
-
const drainType = DRAIN_TYPES[rawDrain.recipient.type];
|
|
53
73
|
const drainDetails = [
|
|
54
74
|
['ID', rawDrain.id],
|
|
55
75
|
['Status', rawDrain.status.status],
|
|
56
76
|
['Execution status', rawDrain.execution.status],
|
|
57
77
|
['URL', rawDrain.recipient.url],
|
|
58
|
-
['Type',
|
|
78
|
+
['Type', DRAIN_TYPE_LABELS[rawDrain.recipient.type]],
|
|
59
79
|
['Custom index', rawDrain.recipient.index],
|
|
80
|
+
['Sourcetype', rawDrain.recipient.sourcetype],
|
|
81
|
+
// DEFAULT is the implicit norm, only a relaxed verification is worth showing
|
|
82
|
+
[
|
|
83
|
+
'TLS verification',
|
|
84
|
+
rawDrain.recipient.tlsVerification === 'TRUSTFUL' ? 'Trustful (certificate not verified)' : null,
|
|
85
|
+
],
|
|
60
86
|
['SD parameters', rawDrain.recipient.rfc5424StructuredDataParameters],
|
|
61
87
|
['Message output rate', formatRate(rawDrain.backlog.msgRateOut)],
|
|
62
88
|
['Message throughput', formatThroughput(rawDrain.backlog.msgThroughputOut)],
|
package/src/models/git-system.js
CHANGED
|
@@ -167,8 +167,8 @@ export class GitSystem extends Git {
|
|
|
167
167
|
class GitNotFoundError extends Error {
|
|
168
168
|
constructor() {
|
|
169
169
|
super(
|
|
170
|
-
'
|
|
171
|
-
'Either install git or
|
|
170
|
+
'git was not found in your PATH\n' +
|
|
171
|
+
'Either install git, or fall back to the previous JS implementation with: clever features disable system-git',
|
|
172
172
|
);
|
|
173
173
|
this.name = 'GitNotFoundError';
|
|
174
174
|
}
|
package/src/models/git.js
CHANGED
|
@@ -30,7 +30,7 @@ export class Git {
|
|
|
30
30
|
*/
|
|
31
31
|
static async get() {
|
|
32
32
|
if (Git.#instance == null) {
|
|
33
|
-
const useSystemGit =
|
|
33
|
+
const useSystemGit = isFeatureEnabled('system-git');
|
|
34
34
|
if (useSystemGit) {
|
|
35
35
|
const { GitSystem } = await import('./git-system.js');
|
|
36
36
|
Git.#instance = new GitSystem();
|