clever-tools 5.0.2 → 5.1.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/package.json +1 -1
- package/src/commands/README.md +1 -0
- package/src/commands/drain/drain.command.js +1 -0
- package/src/commands/drain/drain.create.betterstack.command.js +4 -2
- package/src/commands/drain/drain.create.datadog.command.js +4 -2
- package/src/commands/drain/drain.create.elasticsearch.command.js +4 -3
- package/src/commands/drain/drain.create.newrelic.command.js +4 -2
- package/src/commands/drain/drain.create.ovh-tcp.command.js +4 -3
- package/src/commands/drain/drain.create.raw-http.command.js +4 -3
- package/src/commands/drain/drain.create.splunk.command.js +4 -2
- package/src/commands/drain/drain.create.syslog-tcp.command.js +6 -3
- package/src/commands/drain/drain.create.syslog-udp.command.js +6 -3
- package/src/commands/drain/drain.docs.md +9 -0
- package/src/commands/drain/drain.options.js +8 -0
- package/src/commands/global.commands.js +2 -0
- package/src/commands/instances/instances.command.js +108 -0
- package/src/commands/instances/instances.docs.md +22 -0
- package/src/commands/kv/kv.command.js +7 -4
- package/src/commands/ssh/ssh.command.js +88 -15
- package/src/commands/ssh/ssh.docs.md +10 -0
- package/src/lib/ambiguous-name-error.js +15 -0
- package/src/lib/k8s.js +3 -8
- package/src/models/drain.js +7 -3
- package/src/models/instances.js +75 -0
- package/src/models/ng.js +3 -2
- package/src/models/oauth-consumer.js +3 -2
- package/src/models/operator.js +3 -5
- package/src/parsers.js +3 -3
package/package.json
CHANGED
package/src/commands/README.md
CHANGED
|
@@ -36,6 +36,7 @@ These options are available for all commands:
|
|
|
36
36
|
|[`clever env`](./env/env.docs.md)|Manage environment variables of an application|
|
|
37
37
|
|[`clever features`](./features/features.docs.md)|Manage Clever Tools experimental features|
|
|
38
38
|
|[`clever help`](./help/help.docs.md)|Display help about the Clever Cloud CLI|
|
|
39
|
+
|[`clever instances`](./instances/instances.docs.md)|List instances of an application|
|
|
39
40
|
|[`clever k8s`](./k8s/k8s.docs.md)|Manage Kubernetes clusters|
|
|
40
41
|
|[`clever keycloak`](./keycloak/keycloak.docs.md)|Manage Clever Cloud Keycloak services|
|
|
41
42
|
|[`clever kv`](./kv/kv.docs.md)|Send a raw command to a Materia KV or Redis® add-on|
|
|
@@ -6,6 +6,7 @@ import { Logger } from '../../logger.js';
|
|
|
6
6
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
7
7
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
8
8
|
import { drainUrlArg } from './drain.args.js';
|
|
9
|
+
import { drainKindOption } from './drain.options.js';
|
|
9
10
|
|
|
10
11
|
export const drainCreateBetterstackCommand = defineCommand({
|
|
11
12
|
description: 'Create a Better Stack drain',
|
|
@@ -18,16 +19,17 @@ export const drainCreateBetterstackCommand = defineCommand({
|
|
|
18
19
|
aliases: ['t'],
|
|
19
20
|
placeholder: 'source-token',
|
|
20
21
|
}),
|
|
22
|
+
kind: drainKindOption,
|
|
21
23
|
alias: aliasOption,
|
|
22
24
|
appIdOrName: appIdOrNameOption,
|
|
23
25
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
24
26
|
},
|
|
25
27
|
args: [drainUrlArg],
|
|
26
28
|
async handler(options, url) {
|
|
27
|
-
const { alias, appIdOrName, addonIdOrRealId, sourceToken } = options;
|
|
29
|
+
const { kind, alias, appIdOrName, addonIdOrRealId, sourceToken } = options;
|
|
28
30
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
29
31
|
|
|
30
|
-
const drain = await createLogDrain('BETTERSTACK', ownerId, resourceId, url, { sourceToken });
|
|
32
|
+
const drain = await createLogDrain('BETTERSTACK', kind, ownerId, resourceId, url, { sourceToken });
|
|
31
33
|
|
|
32
34
|
Logger.printSuccess(
|
|
33
35
|
`Better Stack drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
|
|
@@ -4,21 +4,23 @@ import { Logger } from '../../logger.js';
|
|
|
4
4
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
5
5
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
6
6
|
import { drainUrlArg } from './drain.args.js';
|
|
7
|
+
import { drainKindOption } from './drain.options.js';
|
|
7
8
|
|
|
8
9
|
export const drainCreateDatadogCommand = defineCommand({
|
|
9
10
|
description: 'Create a Datadog drain',
|
|
10
11
|
since: '0.9.0',
|
|
11
12
|
options: {
|
|
13
|
+
kind: drainKindOption,
|
|
12
14
|
alias: aliasOption,
|
|
13
15
|
appIdOrName: appIdOrNameOption,
|
|
14
16
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
15
17
|
},
|
|
16
18
|
args: [drainUrlArg],
|
|
17
19
|
async handler(options, url) {
|
|
18
|
-
const { alias, appIdOrName, addonIdOrRealId } = options;
|
|
20
|
+
const { kind, alias, appIdOrName, addonIdOrRealId } = options;
|
|
19
21
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
20
22
|
|
|
21
|
-
const drain = await createLogDrain('DATADOG', ownerId, resourceId, url);
|
|
23
|
+
const drain = await createLogDrain('DATADOG', kind, ownerId, resourceId, url);
|
|
22
24
|
|
|
23
25
|
Logger.printSuccess(
|
|
24
26
|
`Datadog drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
|
|
@@ -6,7 +6,7 @@ import { styleText } from '../../lib/style-text.js';
|
|
|
6
6
|
import { Logger } from '../../logger.js';
|
|
7
7
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
8
8
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
9
|
-
import { drainPasswordOption, drainUsernameOption } from './drain.options.js';
|
|
9
|
+
import { drainKindOption, drainPasswordOption, drainUsernameOption } from './drain.options.js';
|
|
10
10
|
|
|
11
11
|
export const drainCreateElasticsearchCommand = defineCommand({
|
|
12
12
|
description: 'Create an Elasticsearch drain',
|
|
@@ -21,6 +21,7 @@ export const drainCreateElasticsearchCommand = defineCommand({
|
|
|
21
21
|
}),
|
|
22
22
|
username: drainUsernameOption,
|
|
23
23
|
password: drainPasswordOption,
|
|
24
|
+
kind: drainKindOption,
|
|
24
25
|
alias: aliasOption,
|
|
25
26
|
appIdOrName: appIdOrNameOption,
|
|
26
27
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
@@ -36,10 +37,10 @@ export const drainCreateElasticsearchCommand = defineCommand({
|
|
|
36
37
|
}),
|
|
37
38
|
],
|
|
38
39
|
async handler(options, url) {
|
|
39
|
-
const { alias, appIdOrName, addonIdOrRealId, index, username, password } = options;
|
|
40
|
+
const { kind, alias, appIdOrName, addonIdOrRealId, index, username, password } = options;
|
|
40
41
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
41
42
|
|
|
42
|
-
const drain = await createLogDrain('ELASTICSEARCH', ownerId, resourceId, url, { index, username, password });
|
|
43
|
+
const drain = await createLogDrain('ELASTICSEARCH', kind, ownerId, resourceId, url, { index, username, password });
|
|
43
44
|
|
|
44
45
|
Logger.printSuccess(
|
|
45
46
|
`Elasticsearch drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
|
|
@@ -6,6 +6,7 @@ import { Logger } from '../../logger.js';
|
|
|
6
6
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
7
7
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
8
8
|
import { drainUrlArg } from './drain.args.js';
|
|
9
|
+
import { drainKindOption } from './drain.options.js';
|
|
9
10
|
|
|
10
11
|
export const drainCreateNewRelicCommand = defineCommand({
|
|
11
12
|
description: 'Create a New Relic drain',
|
|
@@ -18,16 +19,17 @@ export const drainCreateNewRelicCommand = defineCommand({
|
|
|
18
19
|
aliases: ['k'],
|
|
19
20
|
placeholder: 'api-key',
|
|
20
21
|
}),
|
|
22
|
+
kind: drainKindOption,
|
|
21
23
|
alias: aliasOption,
|
|
22
24
|
appIdOrName: appIdOrNameOption,
|
|
23
25
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
24
26
|
},
|
|
25
27
|
args: [drainUrlArg],
|
|
26
28
|
async handler(options, url) {
|
|
27
|
-
const { alias, appIdOrName, addonIdOrRealId, apiKey } = options;
|
|
29
|
+
const { kind, alias, appIdOrName, addonIdOrRealId, apiKey } = options;
|
|
28
30
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
29
31
|
|
|
30
|
-
const drain = await createLogDrain('NEWRELIC', ownerId, resourceId, url, { apiKey });
|
|
32
|
+
const drain = await createLogDrain('NEWRELIC', kind, ownerId, resourceId, url, { apiKey });
|
|
31
33
|
|
|
32
34
|
Logger.printSuccess(
|
|
33
35
|
`New Relic drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
|
|
@@ -4,23 +4,24 @@ import { Logger } from '../../logger.js';
|
|
|
4
4
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
5
5
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
6
6
|
import { drainUrlArg } from './drain.args.js';
|
|
7
|
-
import { drainSdParamsOption } from './drain.options.js';
|
|
7
|
+
import { drainKindOption, drainSdParamsOption } from './drain.options.js';
|
|
8
8
|
|
|
9
9
|
export const drainCreateOvhTcpCommand = defineCommand({
|
|
10
10
|
description: 'Create an OVH TCP drain',
|
|
11
11
|
since: '0.9.0',
|
|
12
12
|
options: {
|
|
13
13
|
rfc5424StructuredDataParameters: drainSdParamsOption,
|
|
14
|
+
kind: drainKindOption,
|
|
14
15
|
alias: aliasOption,
|
|
15
16
|
appIdOrName: appIdOrNameOption,
|
|
16
17
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
17
18
|
},
|
|
18
19
|
args: [drainUrlArg],
|
|
19
20
|
async handler(options, url) {
|
|
20
|
-
const { alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
|
|
21
|
+
const { kind, alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
|
|
21
22
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
22
23
|
|
|
23
|
-
const drain = await createLogDrain('OVH_TCP', ownerId, resourceId, url, { rfc5424StructuredDataParameters });
|
|
24
|
+
const drain = await createLogDrain('OVH_TCP', kind, ownerId, resourceId, url, { rfc5424StructuredDataParameters });
|
|
24
25
|
|
|
25
26
|
Logger.printSuccess(
|
|
26
27
|
`OVH TCP drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
|
|
@@ -4,7 +4,7 @@ import { Logger } from '../../logger.js';
|
|
|
4
4
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
5
5
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
6
6
|
import { drainUrlArg } from './drain.args.js';
|
|
7
|
-
import { drainPasswordOption, drainUsernameOption } from './drain.options.js';
|
|
7
|
+
import { drainKindOption, drainPasswordOption, drainUsernameOption } from './drain.options.js';
|
|
8
8
|
|
|
9
9
|
export const drainCreateRawHttpCommand = defineCommand({
|
|
10
10
|
description: 'Create a raw HTTP drain',
|
|
@@ -12,16 +12,17 @@ export const drainCreateRawHttpCommand = defineCommand({
|
|
|
12
12
|
options: {
|
|
13
13
|
username: drainUsernameOption,
|
|
14
14
|
password: drainPasswordOption,
|
|
15
|
+
kind: drainKindOption,
|
|
15
16
|
alias: aliasOption,
|
|
16
17
|
appIdOrName: appIdOrNameOption,
|
|
17
18
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
18
19
|
},
|
|
19
20
|
args: [drainUrlArg],
|
|
20
21
|
async handler(options, url) {
|
|
21
|
-
const { alias, appIdOrName, addonIdOrRealId, username, password } = options;
|
|
22
|
+
const { kind, alias, appIdOrName, addonIdOrRealId, username, password } = options;
|
|
22
23
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
23
24
|
|
|
24
|
-
const drain = await createLogDrain('RAW_HTTP', ownerId, resourceId, url, { username, password });
|
|
25
|
+
const drain = await createLogDrain('RAW_HTTP', kind, ownerId, resourceId, url, { username, password });
|
|
25
26
|
|
|
26
27
|
Logger.printSuccess(
|
|
27
28
|
`Raw HTTP drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
|
|
@@ -6,6 +6,7 @@ import { Logger } from '../../logger.js';
|
|
|
6
6
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
7
7
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
8
8
|
import { drainUrlArg } from './drain.args.js';
|
|
9
|
+
import { drainKindOption } from './drain.options.js';
|
|
9
10
|
|
|
10
11
|
export const drainCreateSplunkCommand = defineCommand({
|
|
11
12
|
description: 'Create a Splunk HEC drain',
|
|
@@ -38,16 +39,17 @@ export const drainCreateSplunkCommand = defineCommand({
|
|
|
38
39
|
description: 'TLS verification mode, use `trustful` to accept a self-signed certificate',
|
|
39
40
|
placeholder: 'tls-verification',
|
|
40
41
|
}),
|
|
42
|
+
kind: drainKindOption,
|
|
41
43
|
alias: aliasOption,
|
|
42
44
|
appIdOrName: appIdOrNameOption,
|
|
43
45
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
44
46
|
},
|
|
45
47
|
args: [drainUrlArg],
|
|
46
48
|
async handler(options, url) {
|
|
47
|
-
const { alias, appIdOrName, addonIdOrRealId, token, index, sourcetype, tlsVerification } = options;
|
|
49
|
+
const { kind, alias, appIdOrName, addonIdOrRealId, token, index, sourcetype, tlsVerification } = options;
|
|
48
50
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
49
51
|
|
|
50
|
-
const drain = await createLogDrain('SPLUNK', ownerId, resourceId, url, {
|
|
52
|
+
const drain = await createLogDrain('SPLUNK', kind, ownerId, resourceId, url, {
|
|
51
53
|
token,
|
|
52
54
|
index,
|
|
53
55
|
sourcetype,
|
|
@@ -4,23 +4,26 @@ import { Logger } from '../../logger.js';
|
|
|
4
4
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
5
5
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
6
6
|
import { drainUrlArg } from './drain.args.js';
|
|
7
|
-
import { drainSdParamsOption } from './drain.options.js';
|
|
7
|
+
import { drainKindOption, drainSdParamsOption } from './drain.options.js';
|
|
8
8
|
|
|
9
9
|
export const drainCreateSyslogTcpCommand = defineCommand({
|
|
10
10
|
description: 'Create a Syslog TCP drain',
|
|
11
11
|
since: '0.9.0',
|
|
12
12
|
options: {
|
|
13
13
|
rfc5424StructuredDataParameters: drainSdParamsOption,
|
|
14
|
+
kind: drainKindOption,
|
|
14
15
|
alias: aliasOption,
|
|
15
16
|
appIdOrName: appIdOrNameOption,
|
|
16
17
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
17
18
|
},
|
|
18
19
|
args: [drainUrlArg],
|
|
19
20
|
async handler(options, url) {
|
|
20
|
-
const { alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
|
|
21
|
+
const { kind, alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
|
|
21
22
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
22
23
|
|
|
23
|
-
const drain = await createLogDrain('SYSLOG_TCP', ownerId, resourceId, url, {
|
|
24
|
+
const drain = await createLogDrain('SYSLOG_TCP', kind, ownerId, resourceId, url, {
|
|
25
|
+
rfc5424StructuredDataParameters,
|
|
26
|
+
});
|
|
24
27
|
|
|
25
28
|
Logger.printSuccess(
|
|
26
29
|
`Syslog TCP drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
|
|
@@ -4,23 +4,26 @@ import { Logger } from '../../logger.js';
|
|
|
4
4
|
import { createLogDrain, resolveDrainResource } from '../../models/drain.js';
|
|
5
5
|
import { addonIdOrRealIdOption, aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
6
6
|
import { drainUrlArg } from './drain.args.js';
|
|
7
|
-
import { drainSdParamsOption } from './drain.options.js';
|
|
7
|
+
import { drainKindOption, drainSdParamsOption } from './drain.options.js';
|
|
8
8
|
|
|
9
9
|
export const drainCreateSyslogUdpCommand = defineCommand({
|
|
10
10
|
description: 'Create a Syslog UDP drain',
|
|
11
11
|
since: '0.9.0',
|
|
12
12
|
options: {
|
|
13
13
|
rfc5424StructuredDataParameters: drainSdParamsOption,
|
|
14
|
+
kind: drainKindOption,
|
|
14
15
|
alias: aliasOption,
|
|
15
16
|
appIdOrName: appIdOrNameOption,
|
|
16
17
|
addonIdOrRealId: addonIdOrRealIdOption,
|
|
17
18
|
},
|
|
18
19
|
args: [drainUrlArg],
|
|
19
20
|
async handler(options, url) {
|
|
20
|
-
const { alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
|
|
21
|
+
const { kind, alias, appIdOrName, addonIdOrRealId, rfc5424StructuredDataParameters } = options;
|
|
21
22
|
const { ownerId, resourceId } = await resolveDrainResource(alias, appIdOrName, addonIdOrRealId);
|
|
22
23
|
|
|
23
|
-
const drain = await createLogDrain('SYSLOG_UDP', ownerId, resourceId, url, {
|
|
24
|
+
const drain = await createLogDrain('SYSLOG_UDP', kind, ownerId, resourceId, url, {
|
|
25
|
+
rfc5424StructuredDataParameters,
|
|
26
|
+
});
|
|
24
27
|
|
|
25
28
|
Logger.printSuccess(
|
|
26
29
|
`Syslog UDP drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`,
|
|
@@ -70,6 +70,7 @@ clever drain create betterstack --source-token <source-token> <drain-url> [optio
|
|
|
70
70
|
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
71
71
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
72
72
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
73
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
73
74
|
|
|
74
75
|
### Examples
|
|
75
76
|
|
|
@@ -100,6 +101,7 @@ clever drain create datadog <drain-url> [options]
|
|
|
100
101
|
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
101
102
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
102
103
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
104
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
103
105
|
|
|
104
106
|
### Examples
|
|
105
107
|
|
|
@@ -136,6 +138,7 @@ clever drain create elasticsearch --index-prefix <index-prefix> <drain-url> [opt
|
|
|
136
138
|
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
137
139
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
138
140
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
141
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
139
142
|
|`-p`, `--password` `<password>`|Basic auth password|
|
|
140
143
|
|`-u`, `--username` `<username>`|Basic auth username|
|
|
141
144
|
|
|
@@ -171,6 +174,7 @@ clever drain create newrelic --api-key <api-key> <drain-url> [options]
|
|
|
171
174
|
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
172
175
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
173
176
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
177
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
174
178
|
|
|
175
179
|
### Examples
|
|
176
180
|
|
|
@@ -201,6 +205,7 @@ clever drain create ovh-tcp <drain-url> [options]
|
|
|
201
205
|
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
202
206
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
203
207
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
208
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
204
209
|
|`-s`, `--sd-params` `<sd-params>`|RFC5424 structured data parameters, e.g.: `token=\"REDACTED\"`|
|
|
205
210
|
|
|
206
211
|
### Examples
|
|
@@ -232,6 +237,7 @@ clever drain create raw-http <drain-url> [options]
|
|
|
232
237
|
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
233
238
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
234
239
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
240
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
235
241
|
|`-p`, `--password` `<password>`|Basic auth password|
|
|
236
242
|
|`-u`, `--username` `<username>`|Basic auth username|
|
|
237
243
|
|
|
@@ -267,6 +273,7 @@ clever drain create splunk --hec-token <hec-token> <drain-url> [options]
|
|
|
267
273
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
268
274
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
269
275
|
|`--index` `<index>`|Optional target index, the HEC token's own index is used if not set|
|
|
276
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
270
277
|
|`--sourcetype` `<sourcetype>`|Optional sourcetype, the HEC token's own sourcetype is used if not set|
|
|
271
278
|
|`--tls-verification` `<tls-verification>`|TLS verification mode, use `trustful` to accept a self-signed certificate (default, trustful)|
|
|
272
279
|
|
|
@@ -291,6 +298,7 @@ clever drain create syslog-tcp <drain-url> [options]
|
|
|
291
298
|
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
292
299
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
293
300
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
301
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
294
302
|
|`-s`, `--sd-params` `<sd-params>`|RFC5424 structured data parameters, e.g.: `token=\"REDACTED\"`|
|
|
295
303
|
|
|
296
304
|
### Examples
|
|
@@ -323,6 +331,7 @@ clever drain create syslog-udp <drain-url> [options]
|
|
|
323
331
|
|`--addon` `<addon-id>`|Add-on ID or real ID|
|
|
324
332
|
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
325
333
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
334
|
+
|`--kind` `<kind>`|Kind of logs sent to the drain (LOG, ACCESSLOG) (default: LOG)|
|
|
326
335
|
|`-s`, `--sd-params` `<sd-params>`|RFC5424 structured data parameters, e.g.: `token=\"REDACTED\"`|
|
|
327
336
|
|
|
328
337
|
### Examples
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { defineOption } from '../../lib/define-option.js';
|
|
3
|
+
import { DRAIN_KINDS } from '../../models/drain.js';
|
|
3
4
|
|
|
4
5
|
export const drainUsernameOption = defineOption({
|
|
5
6
|
name: 'username',
|
|
@@ -24,3 +25,10 @@ export const drainSdParamsOption = defineOption({
|
|
|
24
25
|
aliases: ['s'],
|
|
25
26
|
placeholder: 'sd-params',
|
|
26
27
|
});
|
|
28
|
+
|
|
29
|
+
export const drainKindOption = defineOption({
|
|
30
|
+
name: 'kind',
|
|
31
|
+
schema: z.enum(DRAIN_KINDS).default('LOG'),
|
|
32
|
+
description: 'Kind of logs sent to the drain',
|
|
33
|
+
placeholder: 'kind',
|
|
34
|
+
});
|
|
@@ -72,6 +72,7 @@ import { featuresEnableCommand } from './features/features.enable.command.js';
|
|
|
72
72
|
import { featuresInfoCommand } from './features/features.info.command.js';
|
|
73
73
|
import { featuresListCommand } from './features/features.list.command.js';
|
|
74
74
|
import { helpCommand } from './help/help.command.js';
|
|
75
|
+
import { instancesCommand } from './instances/instances.command.js';
|
|
75
76
|
import { k8sActivityCommand } from './k8s/k8s.activity.command.js';
|
|
76
77
|
import { k8sAddPersistentStorageCommand } from './k8s/k8s.add-persistent-storage.command.js';
|
|
77
78
|
import { k8sCommand } from './k8s/k8s.command.js';
|
|
@@ -328,6 +329,7 @@ export const globalCommands = {
|
|
|
328
329
|
},
|
|
329
330
|
],
|
|
330
331
|
help: helpCommand,
|
|
332
|
+
instances: instancesCommand,
|
|
331
333
|
k8s: [
|
|
332
334
|
k8sCommand,
|
|
333
335
|
{
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatTable } from '../../format-table.js';
|
|
3
|
+
import { defineCommand } from '../../lib/define-command.js';
|
|
4
|
+
import { defineOption } from '../../lib/define-option.js';
|
|
5
|
+
import { styleText } from '../../lib/style-text.js';
|
|
6
|
+
import { Logger } from '../../logger.js';
|
|
7
|
+
import * as Application from '../../models/application.js';
|
|
8
|
+
import { listInstances } from '../../models/instances.js';
|
|
9
|
+
import { date } from '../../parsers.js';
|
|
10
|
+
import { aliasOption, appIdOrNameOption, humanJsonOutputFormatOption } from '../global.options.js';
|
|
11
|
+
|
|
12
|
+
export const instancesCommand = defineCommand({
|
|
13
|
+
description: 'List instances of an application',
|
|
14
|
+
since: '5.1.0',
|
|
15
|
+
options: {
|
|
16
|
+
all: defineOption({
|
|
17
|
+
name: 'all',
|
|
18
|
+
schema: z.boolean().default(false),
|
|
19
|
+
description: 'List instances in any state, including deleted ones (default: only running instances)',
|
|
20
|
+
}),
|
|
21
|
+
since: defineOption({
|
|
22
|
+
name: 'after',
|
|
23
|
+
schema: z.string().transform(date).optional(),
|
|
24
|
+
description:
|
|
25
|
+
'List instances that existed after this date/time, in any state (ISO8601 date, positive number in seconds or duration, e.g.: 1h)',
|
|
26
|
+
aliases: ['since'],
|
|
27
|
+
placeholder: 'after',
|
|
28
|
+
}),
|
|
29
|
+
until: defineOption({
|
|
30
|
+
name: 'before',
|
|
31
|
+
schema: z.string().transform(date).optional(),
|
|
32
|
+
description:
|
|
33
|
+
'List instances that existed before this date/time, in any state (ISO8601 date, positive number in seconds or duration, e.g.: 1h)',
|
|
34
|
+
aliases: ['until'],
|
|
35
|
+
placeholder: 'before',
|
|
36
|
+
}),
|
|
37
|
+
deploymentId: defineOption({
|
|
38
|
+
name: 'deployment-id',
|
|
39
|
+
schema: z.string().optional(),
|
|
40
|
+
description: 'List instances created by this deployment, in any state',
|
|
41
|
+
placeholder: 'deployment-id',
|
|
42
|
+
}),
|
|
43
|
+
limit: defineOption({
|
|
44
|
+
name: 'limit',
|
|
45
|
+
schema: z.coerce.number().int().min(1).max(1000).default(100),
|
|
46
|
+
description: 'Maximum number of instances to list, keeping the most recent ones (1 to 1000)',
|
|
47
|
+
placeholder: 'limit',
|
|
48
|
+
}),
|
|
49
|
+
alias: aliasOption,
|
|
50
|
+
app: appIdOrNameOption,
|
|
51
|
+
format: humanJsonOutputFormatOption,
|
|
52
|
+
},
|
|
53
|
+
async handler(options) {
|
|
54
|
+
const { alias, app: appIdOrName, all, since, until, deploymentId, limit, format } = options;
|
|
55
|
+
if (since != null && until != null && since > until) {
|
|
56
|
+
throw new Error('--after must be earlier than or equal to --before');
|
|
57
|
+
}
|
|
58
|
+
const { ownerId, appId } = await Application.resolveId(appIdOrName, alias);
|
|
59
|
+
|
|
60
|
+
// Date and deployment filters also include past instances
|
|
61
|
+
const hasFilters = since != null || until != null || deploymentId != null;
|
|
62
|
+
const onlyRunning = !all && !hasFilters;
|
|
63
|
+
|
|
64
|
+
const instances = await listInstances({
|
|
65
|
+
ownerId,
|
|
66
|
+
appId,
|
|
67
|
+
onlyRunning,
|
|
68
|
+
since,
|
|
69
|
+
until,
|
|
70
|
+
deploymentId,
|
|
71
|
+
limit,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
switch (format) {
|
|
75
|
+
case 'json': {
|
|
76
|
+
Logger.printJson(instances);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'human':
|
|
80
|
+
default: {
|
|
81
|
+
if (instances.length === 0) {
|
|
82
|
+
Logger.println(
|
|
83
|
+
styleText(
|
|
84
|
+
'blue',
|
|
85
|
+
onlyRunning
|
|
86
|
+
? 'No running instances found for this application'
|
|
87
|
+
: hasFilters
|
|
88
|
+
? 'No instances found matching the filters'
|
|
89
|
+
: 'No instances found for this application',
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const headers = ['CREATED', 'STATE', '#', 'FLAVOR', 'INSTANCE ID', 'DEPLOYMENT ID'];
|
|
96
|
+
const rows = instances.map((instance) => [
|
|
97
|
+
`${instance.createdAt.substring(0, 19).replace('T', ' ')}Z`,
|
|
98
|
+
instance.state,
|
|
99
|
+
instance.isBuildVm ? 'build' : String(instance.instanceNumber ?? '?'),
|
|
100
|
+
instance.flavor ?? '',
|
|
101
|
+
instance.id,
|
|
102
|
+
instance.deploymentId,
|
|
103
|
+
]);
|
|
104
|
+
Logger.println(formatTable([headers, ...rows]));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 📖 `clever instances` command reference
|
|
2
|
+
|
|
3
|
+
## ➡️ `clever instances` <kbd>Since 5.1.0</kbd>
|
|
4
|
+
|
|
5
|
+
List instances of an application
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
clever instances [options]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### ⚙️ Options
|
|
12
|
+
|
|
13
|
+
|Name|Description|
|
|
14
|
+
|---|---|
|
|
15
|
+
|`--after`, `--since` `<after>`|List instances that existed after this date/time, in any state (ISO8601 date, positive number in seconds or duration, e.g.: 1h)|
|
|
16
|
+
|`-a`, `--alias` `<alias>`|Short name for the application|
|
|
17
|
+
|`--all`|List instances in any state, including deleted ones (default: only running instances)|
|
|
18
|
+
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
19
|
+
|`--before`, `--until` `<before>`|List instances that existed before this date/time, in any state (ISO8601 date, positive number in seconds or duration, e.g.: 1h)|
|
|
20
|
+
|`--deployment-id` `<deployment-id>`|List instances created by this deployment, in any state|
|
|
21
|
+
|`-F`, `--format` `<format>`|Output format (human, json) (default: human)|
|
|
22
|
+
|`--limit` `<limit>`|Maximum number of instances to list, keeping the most recent ones (1 to 1000) (default: 100)|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getAllEnvVars } from '@clevercloud/client/esm/api/v2/addon.js';
|
|
2
2
|
import Redis from 'ioredis';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { ambiguousNameError } from '../../lib/ambiguous-name-error.js';
|
|
4
5
|
import { defineArgument } from '../../lib/define-argument.js';
|
|
5
6
|
import { defineCommand } from '../../lib/define-command.js';
|
|
6
7
|
import { styleText } from '../../lib/style-text.js';
|
|
@@ -70,10 +71,12 @@ export const kvCommand = defineCommand({
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
if (addons.length > 1) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
const candidates = addons.map(({ name, addonId, ownerId }) => ({
|
|
75
|
+
name,
|
|
76
|
+
id: addonId,
|
|
77
|
+
details: `owner ${ownerId}`,
|
|
78
|
+
}));
|
|
79
|
+
throw ambiguousNameError(addonIdOrRealIdOrName, candidates);
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
const { addonId, ownerId } = addons[0];
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getAllInstances } from '@clevercloud/client/esm/api/v2/application.js';
|
|
2
1
|
import { spawn } from 'node:child_process';
|
|
3
2
|
import { randomUUID } from 'node:crypto';
|
|
4
3
|
import { z } from 'zod';
|
|
@@ -6,10 +5,13 @@ import { config } from '../../config/config.js';
|
|
|
6
5
|
import { defineCommand } from '../../lib/define-command.js';
|
|
7
6
|
import { defineOption } from '../../lib/define-option.js';
|
|
8
7
|
import { selectAnswer } from '../../lib/prompts.js';
|
|
8
|
+
import { styleText } from '../../lib/style-text.js';
|
|
9
9
|
import * as Application from '../../models/application.js';
|
|
10
|
-
import {
|
|
10
|
+
import { listInstances } from '../../models/instances.js';
|
|
11
11
|
import { aliasOption, appIdOrNameOption } from '../global.options.js';
|
|
12
12
|
|
|
13
|
+
/** @typedef {import('../../models/instances.js').Instance} Instance */
|
|
14
|
+
|
|
13
15
|
export const sshCommand = defineCommand({
|
|
14
16
|
description: 'Connect to running instances through SSH',
|
|
15
17
|
since: '0.7.0',
|
|
@@ -28,33 +30,62 @@ export const sshCommand = defineCommand({
|
|
|
28
30
|
aliases: ['c'],
|
|
29
31
|
placeholder: 'command',
|
|
30
32
|
}),
|
|
33
|
+
instance: defineOption({
|
|
34
|
+
name: 'instance',
|
|
35
|
+
// Most specific first: z.union keeps the first match and an ID accepts any string
|
|
36
|
+
schema: z
|
|
37
|
+
.union([z.literal('any'), z.string().regex(/^\d+$/).transform(Number).pipe(z.int()), z.string().min(1)])
|
|
38
|
+
.optional(),
|
|
39
|
+
description:
|
|
40
|
+
'Instance to connect to, by ID or number, or `any` (skips interactive selection). Build VMs are only picked by ID, or by `any` when no other instance is running',
|
|
41
|
+
placeholder: 'instance-id|number|any',
|
|
42
|
+
}),
|
|
31
43
|
alias: aliasOption,
|
|
32
44
|
app: appIdOrNameOption,
|
|
33
45
|
},
|
|
34
46
|
args: [],
|
|
35
47
|
async handler(options) {
|
|
36
|
-
const { alias, app: appIdOrName, identityFile, command } = options;
|
|
48
|
+
const { alias, app: appIdOrName, identityFile, command, instance } = options;
|
|
37
49
|
const { appId, ownerId } = await Application.resolveId(appIdOrName, alias);
|
|
38
50
|
|
|
39
|
-
const instances = await
|
|
51
|
+
const instances = await listInstances({ ownerId, appId, onlyRunning: true });
|
|
40
52
|
|
|
41
53
|
if (instances.length === 0) {
|
|
42
54
|
throw new Error('No running instances found for this application');
|
|
43
55
|
}
|
|
44
56
|
|
|
57
|
+
// Build VMs also carry number 0, keep them last
|
|
58
|
+
// Instances have no number at the very beginning of booting
|
|
59
|
+
// Numbers restart at 0 on each deployment, instances sharing a number are ordered newest first
|
|
60
|
+
const sortedInstances = instances.toSorted((a, b) => {
|
|
61
|
+
return (
|
|
62
|
+
Number(a.isBuildVm) - Number(b.isBuildVm) ||
|
|
63
|
+
(a.instanceNumber ?? Infinity) - (b.instanceNumber ?? Infinity) ||
|
|
64
|
+
b.createdAt.localeCompare(a.createdAt)
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (instance == null && sortedInstances.length > 1 && !process.stdin.isTTY) {
|
|
69
|
+
throw new Error(`Multiple instances are running, pick one with --instance:\n${formatInstances(sortedInstances)}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
let sshTarget;
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
sshTarget =
|
|
73
|
+
if (instance != null) {
|
|
74
|
+
const selectedInstance = selectInstance(sortedInstances, instance);
|
|
75
|
+
if (selectedInstance == null) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`No instance ${styleText('red', String(instance))} on this application, pick one with --instance:\n${formatInstances(sortedInstances)}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
sshTarget = selectedInstance.id;
|
|
81
|
+
} else if (sortedInstances.length === 1) {
|
|
82
|
+
sshTarget = sortedInstances[0].id;
|
|
56
83
|
} else {
|
|
57
|
-
|
|
84
|
+
const choices = sortedInstances.map((i) => ({
|
|
85
|
+
name: `${i.name ?? '?'} - ${i.isBuildVm ? 'Build instance' : `Instance ${i.instanceNumber ?? '?'}`} - ${i.state} (${i.id})`,
|
|
86
|
+
value: i.id,
|
|
87
|
+
}));
|
|
88
|
+
sshTarget = await selectAnswer('Select an instance:', choices);
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
const sshParams = [];
|
|
@@ -126,3 +157,45 @@ export const sshCommand = defineCommand({
|
|
|
126
157
|
process.exit(exitCode);
|
|
127
158
|
},
|
|
128
159
|
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Pick the instance the caller asked for, by ID, by number, or `any`, or nothing when none match.
|
|
163
|
+
*
|
|
164
|
+
* Numbers are not unique: while a deployment rolls, the instance going away and the one coming up
|
|
165
|
+
* carry the same number. `UP` ones are preferred among them, the most recent first, and `any` prefers
|
|
166
|
+
* an `UP` one over the lowest number. Build VMs are also numbered 0, so a number never picks them,
|
|
167
|
+
* and `any` only falls back to them when no other instance is running.
|
|
168
|
+
*
|
|
169
|
+
* @param {Array<Instance>} sortedInstances - build VMs last, then sorted by instance number, most recent first
|
|
170
|
+
* @param {string | number | 'any'} wanted - an instance ID, an instance number or `any`
|
|
171
|
+
* @returns {Instance | null}
|
|
172
|
+
*/
|
|
173
|
+
function selectInstance(sortedInstances, wanted) {
|
|
174
|
+
const runtimeInstances = sortedInstances.filter((i) => !i.isBuildVm);
|
|
175
|
+
if (wanted === 'any') {
|
|
176
|
+
return getReadiestInstance(runtimeInstances) ?? getReadiestInstance(sortedInstances);
|
|
177
|
+
}
|
|
178
|
+
if (typeof wanted === 'number') {
|
|
179
|
+
return getReadiestInstance(runtimeInstances.filter((i) => i.instanceNumber === wanted));
|
|
180
|
+
}
|
|
181
|
+
return sortedInstances.find((i) => i.id === wanted) ?? null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The first serving instance, or the first one when none is serving.
|
|
186
|
+
* @param {Array<Instance>} sortedInstances - sorted by instance number, then most recent first
|
|
187
|
+
* @returns {Instance | null}
|
|
188
|
+
*/
|
|
189
|
+
function getReadiestInstance(sortedInstances) {
|
|
190
|
+
return sortedInstances.find((i) => i.state === 'UP') ?? sortedInstances[0] ?? null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* One line per instance, to help the caller pick one with `--instance`.
|
|
195
|
+
* @param {Array<Instance>} instances
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
function formatInstances(instances) {
|
|
199
|
+
const lines = instances.map((i) => ` - ${i.isBuildVm ? 'build' : (i.instanceNumber ?? '?')} ${i.id} (${i.state})`);
|
|
200
|
+
return styleText('grey', lines.join('\n'));
|
|
201
|
+
}
|
|
@@ -16,3 +16,13 @@ clever ssh [options]
|
|
|
16
16
|
|`--app` `<app-id\|app-name>`|Application to manage by its ID (or name, if unambiguous)|
|
|
17
17
|
|`-c`, `--command` `<command>`|Execute a command on the remote instance and exit|
|
|
18
18
|
|`-i`, `--identity-file` `<identity-file>`|SSH identity file|
|
|
19
|
+
|`--instance` `<instance-id\|number\|any>`|Instance to connect to, by ID or number, or `any` (skips interactive selection). Build VMs are only picked by ID, or by `any` when no other instance is running|
|
|
20
|
+
|
|
21
|
+
### Examples
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
clever ssh --instance 0 -c hostname # instance number 0
|
|
25
|
+
clever ssh --instance any -c hostname # any running instance, a build VM only if nothing else runs
|
|
26
|
+
clever instances # during a deployment, the build VM shows as `build`
|
|
27
|
+
clever ssh --instance <build-vm-id> # connect to a build VM
|
|
28
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { styleText } from './style-text.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds the error thrown when a name matches several resources, listing them so the user can pick one by ID
|
|
5
|
+
* @param {string} name - The name that matched several resources
|
|
6
|
+
* @param {Array<{ name: string, id: string, details?: string }>} candidates - The resources matching the name, with optional details to tell them apart
|
|
7
|
+
* @param {string} [idLabel] - How the identifier to use instead is called (e.g. "real ID", "key")
|
|
8
|
+
* @returns {Error}
|
|
9
|
+
*/
|
|
10
|
+
export function ambiguousNameError(name, candidates, idLabel = 'ID') {
|
|
11
|
+
const list = candidates
|
|
12
|
+
.map((c) => ` - ${c.name} ${styleText('grey', `(${[c.id, c.details].filter(Boolean).join(', ')})`)}`)
|
|
13
|
+
.join('\n');
|
|
14
|
+
return new Error(`Ambiguous name ${styleText('red', name)}, use the ${idLabel} instead:\n${list}`);
|
|
15
|
+
}
|
package/src/lib/k8s.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
|
+
import { ambiguousNameError } from './ambiguous-name-error.js';
|
|
2
3
|
import { ask, confirm, selectAnswer } from './prompts.js';
|
|
3
4
|
import { styleText } from './style-text.js';
|
|
4
5
|
|
|
@@ -244,10 +245,7 @@ export async function getClusterIdFromAddonIdOrName(addonIdOrName, ownerId) {
|
|
|
244
245
|
throw new Error(`No Kubernetes cluster found with the name ${styleText('red', name)}`);
|
|
245
246
|
}
|
|
246
247
|
if (matches.length > 1) {
|
|
247
|
-
|
|
248
|
-
throw new Error(
|
|
249
|
-
`Multiple Kubernetes clusters found with the name ${styleText('red', name)}, use the ID instead:\n${styleText('grey', listing)}`,
|
|
250
|
-
);
|
|
248
|
+
throw ambiguousNameError(name, matches);
|
|
251
249
|
}
|
|
252
250
|
return matches[0].id;
|
|
253
251
|
} else {
|
|
@@ -436,10 +434,7 @@ async function resolveNodeGroupId(ownerId, clusterId, nodeGroupIdOrName) {
|
|
|
436
434
|
throw new Error(`No node group found with name ${styleText('red', nodeGroupIdOrName)}`);
|
|
437
435
|
}
|
|
438
436
|
if (matches.length > 1) {
|
|
439
|
-
|
|
440
|
-
throw new Error(
|
|
441
|
-
`Multiple node groups found with the name ${styleText('red', nodeGroupIdOrName)}, use the ID instead:\n${styleText('grey', listing)}`,
|
|
442
|
-
);
|
|
437
|
+
throw ambiguousNameError(nodeGroupIdOrName, matches);
|
|
443
438
|
}
|
|
444
439
|
return matches[0].id;
|
|
445
440
|
}
|
package/src/models/drain.js
CHANGED
|
@@ -17,16 +17,19 @@ export async function resolveDrainResource(alias, appIdOrName, addonIdOrRealId)
|
|
|
17
17
|
return { ownerId, resourceId: appId };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export const DRAIN_KINDS = /** @type {const} */ (['LOG', 'ACCESSLOG']);
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
|
-
* Creates a
|
|
23
|
+
* Creates a drain, the recipient options that are not set are left out of the payload.
|
|
22
24
|
* @param {string} type - Drain type, as expected by the API
|
|
25
|
+
* @param {typeof DRAIN_KINDS[number]} kind - Kind of logs sent to the drain
|
|
23
26
|
* @param {string} ownerId
|
|
24
27
|
* @param {string} resourceId
|
|
25
28
|
* @param {string} url - Drain URL
|
|
26
29
|
* @param {Record<string, unknown>} [recipientOptions] - Extra recipient fields, `null` and `undefined` ones are ignored
|
|
27
30
|
*/
|
|
28
|
-
export function createLogDrain(type, ownerId, resourceId, url, recipientOptions = {}) {
|
|
29
|
-
const body = { kind
|
|
31
|
+
export function createLogDrain(type, kind, ownerId, resourceId, url, recipientOptions = {}) {
|
|
32
|
+
const body = { kind, recipient: { type, url } };
|
|
30
33
|
|
|
31
34
|
for (const key in recipientOptions) {
|
|
32
35
|
if (recipientOptions[key] != null) {
|
|
@@ -72,6 +75,7 @@ function formatThroughput(bytesPerSecond) {
|
|
|
72
75
|
export function formatDrain(rawDrain) {
|
|
73
76
|
const drainDetails = [
|
|
74
77
|
['ID', rawDrain.id],
|
|
78
|
+
['Kind', rawDrain.kind],
|
|
75
79
|
['Status', rawDrain.status.status],
|
|
76
80
|
['Execution status', rawDrain.execution.status],
|
|
77
81
|
['URL', rawDrain.recipient.url],
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getAllApplicationInstances } from '@clevercloud/client/esm/api/v4/instance.js';
|
|
2
|
+
import { toDate } from '../lib/date-utils.js';
|
|
3
|
+
import { sendToApi } from './send-to-api.js';
|
|
4
|
+
|
|
5
|
+
// States in which the VM is running
|
|
6
|
+
const RUNNING_STATES = [
|
|
7
|
+
'BOOTING',
|
|
8
|
+
'STARTING',
|
|
9
|
+
'READY',
|
|
10
|
+
'BUILDING',
|
|
11
|
+
'DEPLOYING',
|
|
12
|
+
'UP',
|
|
13
|
+
'TASK_IN_PROGRESS',
|
|
14
|
+
'MIGRATION_IN_PROGRESS',
|
|
15
|
+
'STOPPING',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} Instance
|
|
20
|
+
* @property {number} [instanceNumber] - Position of the instance within its deployment, starting at 0 (build VMs are also numbered 0), missing at the very beginning of booting
|
|
21
|
+
* @property {string} id
|
|
22
|
+
* @property {string|null} name
|
|
23
|
+
* @property {string} state
|
|
24
|
+
* @property {string|null} flavor
|
|
25
|
+
* @property {boolean} isBuildVm
|
|
26
|
+
* @property {string} deploymentId
|
|
27
|
+
* @property {string} createdAt - ISO 8601 date
|
|
28
|
+
* @property {string|null} deletedAt - ISO 8601 date
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Lists the instances of an application, sorted by creation date (oldest first).
|
|
33
|
+
* The API returns the most recent instances first, so `limit` keeps the N most recent ones.
|
|
34
|
+
* @param {object} params
|
|
35
|
+
* @param {string} params.ownerId
|
|
36
|
+
* @param {string} params.appId
|
|
37
|
+
* @param {boolean} [params.onlyRunning] - Only keep instances whose VM is running
|
|
38
|
+
* @param {Date} [params.since] - Only keep instances deleted at or after this date, or not deleted yet
|
|
39
|
+
* @param {Date} [params.until] - Only keep instances created at or before this date
|
|
40
|
+
* @param {string} [params.deploymentId] - Only keep instances created by this deployment
|
|
41
|
+
* @param {number} [params.limit]
|
|
42
|
+
* @returns {Promise<Array<Instance>>}
|
|
43
|
+
*/
|
|
44
|
+
export async function listInstances({ ownerId, appId, onlyRunning, since, until, deploymentId, limit }) {
|
|
45
|
+
const rawInstances = await getAllApplicationInstances({
|
|
46
|
+
ownerId,
|
|
47
|
+
applicationId: appId,
|
|
48
|
+
// The API rejects a repeated query parameter but accepts a comma-separated list
|
|
49
|
+
includeState: onlyRunning ? RUNNING_STATES.join(',') : undefined,
|
|
50
|
+
since: since?.toISOString(),
|
|
51
|
+
until: until?.toISOString(),
|
|
52
|
+
deploymentId,
|
|
53
|
+
limit,
|
|
54
|
+
}).then(sendToApi);
|
|
55
|
+
|
|
56
|
+
return rawInstances.map(normalizeInstance).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {object} rawInstance - Instance as returned by the v4 orchestration API
|
|
61
|
+
* @returns {Instance}
|
|
62
|
+
*/
|
|
63
|
+
function normalizeInstance(rawInstance) {
|
|
64
|
+
return {
|
|
65
|
+
instanceNumber: rawInstance.index,
|
|
66
|
+
id: rawInstance.id,
|
|
67
|
+
name: rawInstance.name ?? null,
|
|
68
|
+
state: rawInstance.state,
|
|
69
|
+
flavor: rawInstance.flavor ?? null,
|
|
70
|
+
isBuildVm: rawInstance.isBuildVm,
|
|
71
|
+
deploymentId: rawInstance.deploymentId,
|
|
72
|
+
createdAt: toDate(rawInstance.creationDate).toISOString(),
|
|
73
|
+
deletedAt: toDate(rawInstance.deletionDate)?.toISOString() ?? null,
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/models/ng.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@clevercloud/client/esm/api/v4/network-group.js';
|
|
8
8
|
import crypto from 'node:crypto';
|
|
9
9
|
import { searchNetworkGroupOrResource } from '../clever-client/ng.js';
|
|
10
|
+
import { ambiguousNameError } from '../lib/ambiguous-name-error.js';
|
|
10
11
|
import { styleText } from '../lib/style-text.js';
|
|
11
12
|
import { Logger } from '../logger.js';
|
|
12
13
|
import { getOwnerIdFromOrgIdOrName } from './ids-resolver.js';
|
|
@@ -189,8 +190,8 @@ export async function searchNgOrResource(idOrLabel, orgaIdOrName, type = 'all',
|
|
|
189
190
|
}
|
|
190
191
|
|
|
191
192
|
if (filtered.length > 1 && type !== 'all') {
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
const candidates = filtered.map((f) => ({ name: f.domainName || f.label, id: f.id, details: f.type }));
|
|
194
|
+
throw ambiguousNameError(query, candidates);
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
// Deduplicate results
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { get as getOauthConsumer } from '@clevercloud/client/esm/api/v2/oauth-consumer.js';
|
|
2
2
|
import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
|
|
3
|
+
import { ambiguousNameError } from '../lib/ambiguous-name-error.js';
|
|
3
4
|
import { promptCheckbox } from '../lib/prompts.js';
|
|
4
5
|
import { styleText } from '../lib/style-text.js';
|
|
5
6
|
import { findOauthConsumersByKeyOrName } from './ids-resolver.js';
|
|
@@ -67,8 +68,8 @@ export async function resolveOauthConsumer(keyOrName) {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
if (candidates.length > 1) {
|
|
70
|
-
const
|
|
71
|
-
throw
|
|
71
|
+
const consumers = candidates.map((c) => ({ name: c.name, id: c.key }));
|
|
72
|
+
throw ambiguousNameError(keyOrName, consumers, 'key');
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
const { ownerId, key } = candidates[0];
|
package/src/models/operator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import dedent from 'dedent';
|
|
2
1
|
import { getOperator } from '../clever-client/operators.js';
|
|
2
|
+
import { ambiguousNameError } from '../lib/ambiguous-name-error.js';
|
|
3
3
|
import { styleText } from '../lib/style-text.js';
|
|
4
4
|
import { findAddonsByNameOrId } from './ids-resolver.js';
|
|
5
5
|
import { sendToApi } from './send-to-api.js';
|
|
@@ -36,10 +36,8 @@ export async function getSingleRealId(operatorIdOrName) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
if (operators.length > 1) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
${styleText('grey', operators.map((otoroshi) => `- ${otoroshi.name} (${otoroshi.realId})`).join('\n'))}
|
|
42
|
-
`);
|
|
39
|
+
const candidates = operators.map((o) => ({ name: o.name, id: o.realId }));
|
|
40
|
+
throw ambiguousNameError(name, candidates, 'real ID');
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
return operators[0].realId;
|
package/src/parsers.js
CHANGED
|
@@ -138,11 +138,10 @@ export function durationInSeconds(durationStr = '') {
|
|
|
138
138
|
try {
|
|
139
139
|
return parseSimpleDuration(durationStr);
|
|
140
140
|
} catch {
|
|
141
|
-
|
|
142
|
-
if (isNaN(n) || n < 0) {
|
|
141
|
+
if (!/^\d+$/.test(durationStr)) {
|
|
143
142
|
throw new Error(errorMessage);
|
|
144
143
|
}
|
|
145
|
-
return
|
|
144
|
+
return Number(durationStr);
|
|
146
145
|
}
|
|
147
146
|
}
|
|
148
147
|
|
|
@@ -165,6 +164,7 @@ function parseSimpleDuration(durationStr) {
|
|
|
165
164
|
const d = ISO8601.parse(isoDuration);
|
|
166
165
|
return ISO8601.toSeconds(d);
|
|
167
166
|
}
|
|
167
|
+
throw new Error(`Unknown duration unit in "${durationStr}"`);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
// Network Groups parsers
|