clever-tools 4.3.0 → 4.4.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 CHANGED
@@ -7,7 +7,7 @@ The official CLI for [Clever Cloud](https://www.clever.cloud) - Deploy and manag
7
7
 
8
8
  ## Quick Start
9
9
 
10
- **Prerequisites:** Node.js 22+
10
+ **Prerequisites:** Node.js 22+
11
11
 
12
12
  ```bash
13
13
  # Install globally
@@ -38,7 +38,7 @@ For Node.js users, npm is the fastest way. For other installation methods includ
38
38
 
39
39
  - Docker images
40
40
  - Binary downloads
41
- - Native packages (RPM, DEB)
41
+ - Native packages (RPM, DEB)
42
42
  - Package managers (Homebrew, WinGet, AUR)
43
43
 
44
44
  See our complete [setup guide](docs/setup-systems.md).
@@ -51,15 +51,15 @@ Enable smart autocompletion for bash or zsh:
51
51
  # Bash
52
52
  clever --bash-autocomplete-script $(which clever) | sudo tee /usr/share/bash-completion/completions/clever
53
53
 
54
- # Zsh
54
+ # Zsh
55
55
  clever --zsh-autocomplete-script $(which clever) | sudo tee /usr/share/zsh/site-functions/_clever
56
56
  ```
57
57
 
58
58
  ## Documentation
59
59
 
60
- - **[Complete CLI Documentation](https://www.clever-cloud.com/developers/doc/cli/)** - Official user guide
61
- - **[CLI Reference](https://www.clever-cloud.com/developers/doc/reference/cli/)** - Complete command reference
62
- - **[Deployment Examples](https://www.clever-cloud.com/developers/guides/)** - Real-world tutorials
60
+ - **[Complete CLI Documentation](https://www.clever.cloud/developers/doc/cli/)** - Official user guide
61
+ - **[CLI Reference](https://www.clever.cloud/developers/doc/reference/cli/)** - Complete command reference
62
+ - **[Deployment Examples](https://www.clever.cloud/developers/guides/)** - Real-world tutorials
63
63
 
64
64
  ## Basic Usage
65
65
 
@@ -100,7 +100,7 @@ clever logs --since 1h
100
100
  clever restart --app <app_id>
101
101
  ```
102
102
 
103
- Learn more in our [Application Management Guide](https://www.clever-cloud.com/developers/doc/cli/applications/).
103
+ Learn more in our [Application Management Guide](https://www.clever.cloud/developers/doc/cli/applications/).
104
104
 
105
105
  ### Add-ons & Services Management
106
106
 
@@ -121,7 +121,7 @@ clever features enable operatos
121
121
  clever keycloak get myKetcloak
122
122
  ```
123
123
 
124
- Learn more in our [Add-ons & Services Guide](https://www.clever-cloud.com/developers/doc/cli/addons/).
124
+ Learn more in our [Add-ons & Services Guide](https://www.clever.cloud/developers/doc/cli/addons/).
125
125
 
126
126
  # Create and manage
127
127
 
@@ -144,7 +144,7 @@ clever tokens create myTokenName
144
144
  clever tokens create myTokenName --expiration 2w --format json
145
145
  ```
146
146
 
147
- Learn more about API integration in our [API How-to Guide](https://www.clever-cloud.com/developers/api/howto).
147
+ Learn more about API integration in our [API How-to Guide](https://www.clever.cloud/developers/api/howto).
148
148
 
149
149
  ### Get Help
150
150
 
package/bin/clever.js CHANGED
@@ -168,8 +168,8 @@ async function run() {
168
168
  }),
169
169
  drainId: cliparse.argument('drain-id', { description: 'Drain ID' }),
170
170
  drainType: cliparse.argument('drain-type', {
171
- description: 'Drain type',
172
- complete: Drain.listDrainTypes,
171
+ description: Drain.DRAIN_TYPE_CLI_CODES.join(', '),
172
+ complete: Drain.DRAIN_TYPE_CLI_CODES,
173
173
  }),
174
174
  drainUrl: cliparse.argument('drain-url', { description: 'Drain URL' }),
175
175
  fqdn: cliparse.argument('fqdn', { description: 'Domain name of the application' }),
@@ -206,9 +206,6 @@ async function run() {
206
206
  aliases: ['w'],
207
207
  description: 'Watch the deployment until the cluster is deployed',
208
208
  }),
209
- kubeConfigForceDownload: cliparse.flag('force', {
210
- description: 'Force the download of the kubeconfig file, even if it already exists',
211
- }),
212
209
  targetVersion: cliparse.option('target', {
213
210
  metavar: 'version',
214
211
  description: 'Target version to upgrade to (e.g.: 24, 2.4, 2.4.1)',
@@ -420,7 +417,7 @@ async function run() {
420
417
  drainPassword: cliparse.option('password', {
421
418
  aliases: ['p'],
422
419
  metavar: 'password',
423
- description: '(HTTP drains) basic auth password',
420
+ description: 'Basic auth password (for elasticsearch or raw-http)',
424
421
  }),
425
422
  addonPlan: cliparse.option('plan', {
426
423
  aliases: ['p'],
@@ -492,22 +489,22 @@ async function run() {
492
489
  drainUsername: cliparse.option('username', {
493
490
  aliases: ['u'],
494
491
  metavar: 'username',
495
- description: '(HTTP drains) basic auth username',
492
+ description: 'Basic auth username (for elasticsearch or raw-http)',
496
493
  }),
497
- drainAPIKey: cliparse.option('api-key', {
494
+ drainApiKey: cliparse.option('api-key', {
498
495
  aliases: ['k'],
499
496
  metavar: 'api_key',
500
- description: '(NewRelic drains) API key',
497
+ description: 'API key (for newrelic)',
501
498
  }),
502
499
  drainIndexPrefix: cliparse.option('index-prefix', {
503
500
  aliases: ['i'],
504
501
  metavar: 'index_prefix',
505
- description: '(ElasticSearch drains) optional index prefix. `logstash` value is used if not set',
502
+ description: 'Optional index prefix (for elasticsearch), `logstash` value is used if not set',
506
503
  }),
507
- drainSDParameters: cliparse.option('sd-params', {
504
+ drainSdParameters: cliparse.option('sd-params', {
508
505
  aliases: ['s'],
509
506
  metavar: 'sd_params',
510
- description: '(TCP and UDP drains) sd-params string (e.g.: `X-OVH-TOKEN=\\"REDACTED\\"`)',
507
+ description: 'RFC5424 structured data parameters (for ovh-tcp), e.g.: `X-OVH-TOKEN=\\"REDACTED\\"`',
511
508
  }),
512
509
  verbose: cliparse.flag('verbose', { aliases: ['v'], description: 'Verbose output' }),
513
510
  color: cliparse.flag('color', {
@@ -870,20 +867,29 @@ async function run() {
870
867
  options: [
871
868
  opts.drainUsername,
872
869
  opts.drainPassword,
873
- opts.drainAPIKey,
870
+ opts.drainApiKey,
874
871
  opts.drainIndexPrefix,
875
- opts.drainSDParameters,
872
+ opts.drainSdParameters,
876
873
  ],
877
874
  },
878
875
  drain.create,
879
876
  );
877
+ const drainGetCommand = cliparse.command(
878
+ 'get',
879
+ {
880
+ description: 'Get drain info',
881
+ args: [args.drainId],
882
+ privateOptions: [opts.humanJsonOutputFormat],
883
+ },
884
+ drain.get,
885
+ );
880
886
  const drainRemoveCommand = cliparse.command(
881
887
  'remove',
882
888
  {
883
889
  description: 'Remove a drain',
884
890
  args: [args.drainId],
885
891
  },
886
- drain.rm,
892
+ drain.remove,
887
893
  );
888
894
  const drainEnableCommand = cliparse.command(
889
895
  'enable',
@@ -905,9 +911,9 @@ async function run() {
905
911
  'drain',
906
912
  {
907
913
  description: 'Manage drains',
908
- options: [opts.alias, opts.appIdOrName, opts.addonId],
914
+ options: [opts.alias, opts.appIdOrName],
909
915
  privateOptions: [opts.humanJsonOutputFormat],
910
- commands: [drainCreateCommand, drainRemoveCommand, drainEnableCommand, drainDisableCommand],
916
+ commands: [drainCreateCommand, drainGetCommand, drainRemoveCommand, drainEnableCommand, drainDisableCommand],
911
917
  },
912
918
  drain.list,
913
919
  );
@@ -1611,6 +1617,14 @@ async function run() {
1611
1617
  },
1612
1618
  otoroshi.get,
1613
1619
  );
1620
+ const otoroshiGetConfigCommand = cliparse.command(
1621
+ 'get-config',
1622
+ {
1623
+ description: 'Get configuration of a deployed Otoroshi in otoroshictl format',
1624
+ args: [args.addonIdOrName],
1625
+ },
1626
+ otoroshi.getConfig,
1627
+ );
1614
1628
  const otoroshiEnableNgCommand = cliparse.command(
1615
1629
  'enable-ng',
1616
1630
  {
@@ -1703,6 +1717,7 @@ async function run() {
1703
1717
  privateOptions: [opts.humanJsonOutputFormat],
1704
1718
  commands: [
1705
1719
  otoroshiGetCommand,
1720
+ otoroshiGetConfigCommand,
1706
1721
  otoroshiEnableNgCommand,
1707
1722
  otoroshiDisableNgCommand,
1708
1723
  otoroshiOpenCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "4.3.0",
3
+ "version": "4.4.1",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "keywords": [
6
6
  "cli",
@@ -90,6 +90,7 @@
90
90
  "node": ">=22"
91
91
  },
92
92
  "volta": {
93
- "node": "22.17.0"
93
+ "node": "22.17.0",
94
+ "npm": "11.6.2"
94
95
  }
95
96
  }
@@ -15,15 +15,6 @@ export function listApiTokens() {
15
15
  });
16
16
  }
17
17
 
18
- export function updateApiToken(apiTokenId, { name, description }) {
19
- return Promise.resolve({
20
- method: 'put',
21
- url: `/api-tokens/${apiTokenId}`,
22
- headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
23
- body: { name, description },
24
- });
25
- }
26
-
27
18
  export function deleteApiToken(apiTokenId) {
28
19
  return Promise.resolve({
29
20
  method: 'delete',
@@ -0,0 +1,110 @@
1
+ // TODO: Move this to the Clever Cloud JS Client
2
+
3
+ /**
4
+ * GET /v4/drains/organisations/{ownerId}/applications/{applicationId}/drains?status={status}&executionStatus={executionStatus}&executionStatusNotIn={executionStatusNotIn}
5
+ * @param {Object} params
6
+ * @param {String} params.ownerId
7
+ * @param {String} params.applicationId
8
+ * @param {String} params.status
9
+ * @param {String} params.executionStatus
10
+ * @param {String} params.executionStatusNotIn
11
+ */
12
+ export function getDrains(params) {
13
+ // no multipath for /self or /organisations/{id}
14
+ return Promise.resolve({
15
+ method: 'get',
16
+ url: `/v4/drains/organisations/${params.ownerId}/applications/${params.applicationId}/drains`,
17
+ headers: { Accept: 'application/json' },
18
+ queryParams: {
19
+ status: params.status,
20
+ executionStatus: params.executionStatus,
21
+ executionStatusNotIn: params.executionStatusNotIn,
22
+ },
23
+ // no body
24
+ });
25
+ }
26
+
27
+ /**
28
+ * POST /v4/drains/organisations/{ownerId}/applications/{applicationId}/drains
29
+ * @param {Object} params
30
+ * @param {String} params.ownerId
31
+ * @param {String} params.applicationId
32
+ * @param {Object} params.body
33
+ */
34
+ export function createDrain(params) {
35
+ // no multipath for /self or /organisations/{id}
36
+ return Promise.resolve({
37
+ method: 'post',
38
+ url: `/v4/drains/organisations/${params.ownerId}/applications/${params.applicationId}/drains`,
39
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
40
+ body: params.body,
41
+ });
42
+ }
43
+
44
+ /**
45
+ * DELETE /v4/drains/organisations/{ownerId}/applications/{applicationId}/drains/{drainId}
46
+ * @param {Object} params
47
+ * @param {String} params.ownerId
48
+ * @param {String} params.applicationId
49
+ * @param {String} params.drainId
50
+ */
51
+ export function deleteDrain(params) {
52
+ // no multipath for /self or /organisations/{id}
53
+ return Promise.resolve({
54
+ method: 'delete',
55
+ url: `/v4/drains/organisations/${params.ownerId}/applications/${params.applicationId}/drains/${params.drainId}`,
56
+ headers: { Accept: 'application/json' },
57
+ // no body
58
+ });
59
+ }
60
+
61
+ /**
62
+ * GET /v4/drains/organisations/{ownerId}/applications/{applicationId}/drains/{drainId}
63
+ * @param {Object} params
64
+ * @param {String} params.ownerId
65
+ * @param {String} params.applicationId
66
+ * @param {String} params.drainId
67
+ */
68
+ export function getDrain(params) {
69
+ // no multipath for /self or /organisations/{id}
70
+ return Promise.resolve({
71
+ method: 'get',
72
+ url: `/v4/drains/organisations/${params.ownerId}/applications/${params.applicationId}/drains/${params.drainId}`,
73
+ headers: { Accept: 'application/json' },
74
+ // no body
75
+ });
76
+ }
77
+
78
+ /**
79
+ * PUT /v4/drains/organisations/{ownerId}/applications/{applicationId}/drains/{drainId}/disable
80
+ * @param {Object} params
81
+ * @param {String} params.ownerId
82
+ * @param {String} params.applicationId
83
+ * @param {String} params.drainId
84
+ */
85
+ export function disableDrain(params) {
86
+ // no multipath for /self or /organisations/{id}
87
+ return Promise.resolve({
88
+ method: 'put',
89
+ url: `/v4/drains/organisations/${params.ownerId}/applications/${params.applicationId}/drains/${params.drainId}/disable`,
90
+ headers: { Accept: 'application/json' },
91
+ // no body
92
+ });
93
+ }
94
+
95
+ /**
96
+ * PUT /v4/drains/organisations/{ownerId}/applications/{applicationId}/drains/{drainId}/enable
97
+ * @param {Object} params
98
+ * @param {String} params.ownerId
99
+ * @param {String} params.applicationId
100
+ * @param {String} params.drainId
101
+ */
102
+ export function enableDrain(params) {
103
+ // no multipath for /self or /organisations/{id}
104
+ return Promise.resolve({
105
+ method: 'put',
106
+ url: `/v4/drains/organisations/${params.ownerId}/applications/${params.applicationId}/drains/${params.drainId}/enable`,
107
+ headers: { Accept: 'application/json' },
108
+ // no body
109
+ });
110
+ }
@@ -119,3 +119,19 @@ export function versionUpdate(params, body) {
119
119
  body,
120
120
  });
121
121
  }
122
+
123
+ /**
124
+ * GET /v4/addon-providers/addon-otoroshi/addons/{realId}/config.yaml
125
+ * @param {Object} params
126
+ * @param {String} params.realId
127
+ */
128
+ export function getOtoroshiConfig(params) {
129
+ // no multipath for /self or /organisations/{id}
130
+ return Promise.resolve({
131
+ method: 'get',
132
+ url: `/v4/addon-providers/addon-otoroshi/addons/${params.realId}/config.yaml`,
133
+ headers: { Accept: 'application/yaml' },
134
+ // no queryParams
135
+ // no body
136
+ });
137
+ }
@@ -1,108 +1,164 @@
1
- import { createDrain, deleteDrain, getDrains, updateDrainState } from '@clevercloud/client/esm/api/v2/log.js';
1
+ import { createDrain, deleteDrain, disableDrain, enableDrain, getDrain, getDrains } from '../clever-client/drains.js';
2
+ import { styleText } from '../lib/style-text.js';
2
3
  import { Logger } from '../logger.js';
3
4
  import * as Application from '../models/application.js';
4
- import { createDrainBody } from '../models/drain.js';
5
+ import { DRAIN_TYPE_CLI_CODES, DRAIN_TYPES, formatDrain } from '../models/drain.js';
5
6
  import { sendToApi } from '../models/send-to-api.js';
6
7
 
7
- // TODO: This could be useful in other commands
8
- async function getAppOrAddonId({ alias, appIdOrName, addonId }) {
9
- return addonId != null ? addonId : await Application.resolveId(appIdOrName, alias).then(({ appId }) => appId);
10
- }
11
-
12
8
  export async function list(params) {
13
- const { alias, app: appIdOrName, addon: addonId, format } = params.options;
9
+ const { alias, app: appIdOrName, format } = params.options;
10
+
11
+ const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
14
12
 
15
- const appIdOrAddonId = await getAppOrAddonId({ alias, appIdOrName, addonId });
16
- const drains = await getDrains({ appId: appIdOrAddonId }).then(sendToApi);
13
+ const drains = await getDrains({ ownerId, applicationId }).then(sendToApi);
17
14
 
18
15
  switch (format) {
19
16
  case 'json': {
20
- const formattedDrains = drains.map((drain) => ({
21
- id: drain.id,
22
- target: drain.target,
23
- state: drain.state,
24
- }));
25
-
26
- Logger.printJson(formattedDrains);
17
+ Logger.printJson(drains);
27
18
  break;
28
19
  }
29
20
  case 'human':
30
21
  default: {
31
22
  if (drains.length === 0) {
32
- Logger.println(`There are no drains for ${appIdOrAddonId}`);
23
+ Logger.println(`There are no drains for ${applicationId}`);
24
+ return;
25
+ }
26
+
27
+ if (drains.length === 1) {
28
+ const formattedDrain = formatDrain(drains[0]);
29
+ console.table(formattedDrain);
30
+ return;
33
31
  }
34
32
 
35
- drains.forEach((drain) => {
36
- const { id, state, target } = drain;
37
- const { url, drainType, indexPrefix, structuredDataParameters } = target;
38
-
39
- let drainView = `${id} -> ${state} for ${url} as ${drainType}`;
40
- if (indexPrefix != null) {
41
- drainView += `, custom index: '${indexPrefix}-YYYY-MM-DD'`;
42
- }
43
- if (structuredDataParameters != null) {
44
- drainView += `, sd-params: '${structuredDataParameters}'`;
45
- }
46
- Logger.println(drainView);
33
+ const formattedDrains = drains.map((drain) => {
34
+ return {
35
+ ID: drain.id,
36
+ Status: drain.status.status,
37
+ 'Execution status': drain.execution.status,
38
+ URL: drain.recipient.url,
39
+ };
47
40
  });
41
+
42
+ console.table(formattedDrains);
48
43
  }
49
44
  }
50
45
  }
51
46
 
52
47
  export async function create(params) {
53
- const [drainTargetType, drainTargetURL] = params.args;
48
+ const { alias, app: appIdOrName } = params.options;
54
49
  const {
55
- alias,
56
- app: appIdOrName,
57
- addon: addonId,
58
50
  username,
59
51
  password,
60
52
  'api-key': apiKey,
61
53
  'index-prefix': indexPrefix,
62
- 'sd-params': structuredDataParameters,
54
+ 'sd-params': rfc5424StructuredDataParameters,
63
55
  } = params.options;
64
- const drainTargetCredentials = { username, password };
65
- const drainTargetConfig = { apiKey, indexPrefix, structuredDataParameters };
66
-
67
- const appIdOrAddonId = await getAppOrAddonId({ alias, appIdOrName, addonId });
68
- const body = createDrainBody(
69
- appIdOrAddonId,
70
- drainTargetURL,
71
- drainTargetType,
72
- drainTargetCredentials,
73
- drainTargetConfig,
74
- );
75
- await createDrain({ appId: appIdOrAddonId }, body).then(sendToApi);
76
-
77
- Logger.println('Your drain has been successfully saved');
56
+ const [drainTypeCliCode, url] = params.args;
57
+
58
+ const drainType = Object.values(DRAIN_TYPES).find((drainType) => drainType.cliCode === drainTypeCliCode);
59
+ if (!drainType) {
60
+ throw new Error(`Invalid drain type. Supported types are: ${DRAIN_TYPE_CLI_CODES.join(', ')}`);
61
+ }
62
+
63
+ const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
64
+
65
+ const body = {
66
+ kind: 'LOG',
67
+ recipient: {
68
+ type: drainType.apiCode,
69
+ url,
70
+ },
71
+ };
72
+
73
+ if (drainTypeCliCode === DRAIN_TYPES.ELASTICSEARCH.cliCode) {
74
+ if (!indexPrefix) {
75
+ throw new Error(`${DRAIN_TYPES.ELASTICSEARCH.cliCode} drains require an index prefix (--index-prefix) to be set`);
76
+ }
77
+ if (!url.endsWith('/_bulk')) {
78
+ throw new Error(`${DRAIN_TYPES.ELASTICSEARCH.cliCode} drain URL must end with '/_bulk'`);
79
+ }
80
+ body.recipient.index = indexPrefix;
81
+ }
82
+
83
+ if (drainTypeCliCode === DRAIN_TYPES.ELASTICSEARCH.cliCode || drainTypeCliCode === DRAIN_TYPES.RAW_HTTP.cliCode) {
84
+ if (username) {
85
+ body.recipient.username = username;
86
+ }
87
+ if (password) {
88
+ body.recipient.password = password;
89
+ }
90
+ }
91
+
92
+ if (drainTypeCliCode === DRAIN_TYPES.NEWRELIC.cliCode) {
93
+ if (!apiKey) {
94
+ throw new Error(`${DRAIN_TYPES.NEWRELIC.cliCode} drains require an API key (--api-key) to be set`);
95
+ }
96
+ body.recipient.apiKey = apiKey;
97
+ }
98
+
99
+ if (
100
+ drainTypeCliCode === DRAIN_TYPES.OVH_TCP.cliCode ||
101
+ drainTypeCliCode === DRAIN_TYPES.SYSLOG_TCP.cliCode ||
102
+ drainTypeCliCode === DRAIN_TYPES.SYSLOG_UDP.cliCode
103
+ ) {
104
+ if (rfc5424StructuredDataParameters) {
105
+ body.recipient.rfc5424StructuredDataParameters = rfc5424StructuredDataParameters;
106
+ }
107
+ }
108
+
109
+ const drain = await createDrain({ ownerId, applicationId, body }).then(sendToApi);
110
+ Logger.printSuccess(`Drain ${styleText(['bold', 'green'], drain.id)} has been successfully created and enabled!`);
78
111
  }
79
112
 
80
- export async function rm(params) {
113
+ export async function get(params) {
81
114
  const [drainId] = params.args;
82
- const { alias, app: appIdOrName, addon: addonId } = params.options;
115
+ const { alias, app: appIdOrName, format } = params.options;
116
+
117
+ const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
83
118
 
84
- const appIdOrAddonId = await getAppOrAddonId({ alias, appIdOrName, addonId });
85
- await deleteDrain({ appId: appIdOrAddonId, drainId }).then(sendToApi);
119
+ const drain = await getDrain({ ownerId, applicationId, drainId }).then(sendToApi);
86
120
 
87
- Logger.println('Your drain has been successfully removed');
121
+ switch (format) {
122
+ case 'json': {
123
+ Logger.printJson(drain);
124
+ break;
125
+ }
126
+ case 'human':
127
+ default: {
128
+ const formattedDrain = formatDrain(drain);
129
+ console.table(formattedDrain);
130
+ }
131
+ }
132
+ }
133
+
134
+ export async function remove(params) {
135
+ const [drainId] = params.args;
136
+ const { alias, app: appIdOrName } = params.options;
137
+
138
+ const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
139
+
140
+ await deleteDrain({ ownerId, applicationId, drainId }).then(sendToApi);
141
+ Logger.printSuccess(`Drain ${styleText(['bold', 'green'], drainId)} has been successfully removed!`);
88
142
  }
89
143
 
90
144
  export async function enable(params) {
91
145
  const [drainId] = params.args;
92
- const { alias, app: appIdOrName, addon: addonId } = params.options;
146
+ const { alias, app: appIdOrName } = params.options;
93
147
 
94
- const appIdOrAddonId = await getAppOrAddonId({ alias, appIdOrName, addonId });
95
- await updateDrainState({ appId: appIdOrAddonId, drainId }, { state: 'ENABLED' }).then(sendToApi);
148
+ const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
96
149
 
97
- Logger.println('Your drain has been enabled');
150
+ await enableDrain({ ownerId, applicationId, drainId }).then(sendToApi);
151
+
152
+ Logger.printSuccess(`Drain ${styleText(['bold', 'green'], drainId)} has been successfully enabled!`);
98
153
  }
99
154
 
100
155
  export async function disable(params) {
101
156
  const [drainId] = params.args;
102
- const { alias, app: appIdOrName, addon: addonId } = params.options;
157
+ const { alias, app: appIdOrName } = params.options;
158
+
159
+ const { ownerId, appId: applicationId } = await Application.resolveId(appIdOrName, alias);
103
160
 
104
- const appIdOrAddonId = await getAppOrAddonId({ alias, appIdOrName, addonId });
105
- await updateDrainState({ appId: appIdOrAddonId, drainId }, { state: 'DISABLED' }).then(sendToApi);
161
+ await disableDrain({ ownerId, applicationId, drainId }).then(sendToApi);
106
162
 
107
- Logger.println('Your drain has been disabled');
163
+ Logger.printSuccess(`Drain ${styleText(['bold', 'green'], drainId)} has been successfully disabled!`);
108
164
  }
@@ -73,7 +73,7 @@ export async function del(params) {
73
73
  const [clusterIdOrName] = params.args;
74
74
  const { org: orgIdOrName, yes: confirmDeletion } = params.options;
75
75
 
76
- let proceedDeletion = false;
76
+ let proceedDeletion;
77
77
  if (confirmDeletion) {
78
78
  proceedDeletion = true;
79
79
  } else {
@@ -50,6 +50,17 @@ export async function get(params) {
50
50
  await operatorPrint('otoroshi', addonIdOrName, format);
51
51
  }
52
52
 
53
+ /**
54
+ * Print the configuration of an Otoroshi operator in otoroshictl format
55
+ * @param {object} params The command's parameters
56
+ * @param {string} params.args[0] The operator's name or ID
57
+ * @returns {Promise<void>}
58
+ */
59
+ export async function getConfig(params) {
60
+ const [addonIdOrName] = params.args;
61
+ await operatorPrint('otoroshi', addonIdOrName, 'otoroshictl');
62
+ }
63
+
53
64
  /**
54
65
  * List all Otoroshi operators
55
66
  * @returns {Promise<void>}
package/src/lib/k8s.js CHANGED
@@ -42,7 +42,7 @@ export async function k8sCreate(name, ownerId) {
42
42
  * @param {string} format The output format
43
43
  * @returns {Promise<void>}
44
44
  */
45
- export async function k8sList(orgIdOrName, format) {
45
+ export async function k8sList(orgIdOrName) {
46
46
  const ownerId = await getOwnerIdFromOrgIdOrName(orgIdOrName);
47
47
  const deployed = await listK8sClusters({ ownerId }).then(sendToApi);
48
48
 
@@ -1,6 +1,7 @@
1
1
  import dedent from 'dedent';
2
2
  import _ from 'lodash';
3
3
  import {
4
+ getOtoroshiConfig,
4
5
  ngDisableOperator,
5
6
  ngEnableOperator,
6
7
  rebootOperator,
@@ -249,6 +250,13 @@ export async function operatorRebuild(provider, addonIdOrName) {
249
250
  export async function operatorPrint(provider, addonIdOrName, format = 'human') {
250
251
  const operator = await Operator.getDetails(provider, addonIdOrName);
251
252
 
253
+ if (provider === 'otoroshi' && format === 'otoroshictl') {
254
+ const config = await getOtoroshiConfig({ realId: operator.resourceId }).then(sendToApi);
255
+
256
+ Logger.println(config);
257
+ return;
258
+ }
259
+
252
260
  const dataToPrint = {
253
261
  Name: operator.name,
254
262
  ID: operator.resourceId,
@@ -228,22 +228,6 @@ export function completePlan() {
228
228
  return cliparse.autocomplete.words(['dev', 's', 'm', 'l', 'xl', 'xxl']);
229
229
  }
230
230
 
231
- export async function findById(addonId) {
232
- const { user, organisations } = await getSummary({}).then(sendToApi);
233
- for (const orga of [user, ...organisations]) {
234
- for (const simpleAddon of orga.addons) {
235
- if (simpleAddon.id === addonId) {
236
- const addon = await getAddon({ id: orga.id, addonId }).then(sendToApi);
237
- return {
238
- ...addon,
239
- orgaId: orga.id,
240
- };
241
- }
242
- }
243
- }
244
- throw new Error(`Could not find add-on with ID: ${addonId}`);
245
- }
246
-
247
231
  export async function findByName(addonName) {
248
232
  const { user, organisations } = await getSummary({}).then(sendToApi);
249
233
  for (const orga of [user, ...organisations]) {
@@ -131,27 +131,6 @@ function findDefaultApp(config) {
131
131
  );
132
132
  }
133
133
 
134
- async function getAppDetailsForId(appId) {
135
- const config = await loadApplicationConf();
136
-
137
- if (_.isEmpty(config.apps)) {
138
- throw new Error('There is no linked or targeted application. Use `--app` option or `clever link` command.');
139
- }
140
-
141
- const [appById, secondAppById] = _.filter(config.apps, { app_id: appId });
142
- if (appById == null) {
143
- throw new Error(`There are no applications matching id '${appId}'`);
144
- }
145
- if (secondAppById != null) {
146
- throw new Error(
147
- `There are several applications matching id '${appId}'.` +
148
- 'This should not happen, your `.clever.json` should be fixed.',
149
- );
150
- }
151
-
152
- return appById;
153
- }
154
-
155
134
  export async function getAppDetails({ alias }) {
156
135
  const config = await loadApplicationConf();
157
136
  const app = findApp(config, alias);
@@ -165,15 +144,6 @@ export async function getAppDetails({ alias }) {
165
144
  };
166
145
  }
167
146
 
168
- export async function getMostNaturalName(appId) {
169
- try {
170
- const details = await getAppDetailsForId(appId);
171
- return details.alias || details.name || appId;
172
- } catch {
173
- return appId;
174
- }
175
- }
176
-
177
147
  function persistConfig(modifiedConfig) {
178
148
  const jsonContents = JSON.stringify(modifiedConfig, null, 2);
179
149
  return fs.writeFile(conf.APP_CONFIGURATION_FILE, jsonContents);
@@ -1,133 +1,29 @@
1
- import cliparse from 'cliparse';
2
-
3
- const DRAIN_TYPES = [
4
- { id: 'TCPSyslog', structuredDataParameters: 'OPTIONAL' },
5
- { id: 'UDPSyslog', structuredDataParameters: 'OPTIONAL' },
6
- { id: 'HTTP', credentials: 'OPTIONAL' },
7
- { id: 'ElasticSearch', credentials: 'MANDATORY', indexPrefix: 'OPTIONAL' },
8
- { id: 'DatadogHTTP' },
9
- { id: 'NewRelicHTTP', apiKey: 'MANDATORY' },
10
- ];
11
-
12
- export function createDrainBody(appId, drainTargetURL, drainTargetType, drainTargetCredentials, drainTargetConfig) {
13
- if (!authorizeDrainCreation(drainTargetType, drainTargetCredentials, drainTargetConfig)) {
14
- throw new Error(
15
- "Credentials are: optional for HTTP, mandatory for ElasticSearch, NewRelicHTTP and TCPSyslog/UDPSyslog don't need them.",
16
- );
17
- }
18
-
19
- const body = {
20
- url: drainTargetURL,
21
- drainType: drainTargetType,
22
- };
23
- if (credentialsExist(drainTargetCredentials)) {
24
- body.credentials = {
25
- username: drainTargetCredentials.username || '',
26
- password: drainTargetCredentials.password || '',
27
- };
28
- }
29
- if (keyExist(drainTargetConfig)) {
30
- body.APIKey = drainTargetConfig.apiKey;
31
- }
32
- if (indexPrefixExist(drainTargetConfig)) {
33
- body.indexPrefix = drainTargetConfig.indexPrefix;
34
- }
35
- if (structuredDataParametersExist(drainTargetConfig)) {
36
- body.structuredDataParameters = drainTargetConfig.structuredDataParameters;
37
- }
38
- return body;
39
- }
40
-
41
- export function authorizeDrainCreation(drainTargetType, drainTargetCredentials, drainTargetConfig) {
42
- if (drainTypeExists(drainTargetType)) {
43
- // retrieve field for drain type ('mandatory', 'optional', undefined)
44
- const credStatus = fieldStatus(drainTargetType).credentials;
45
- const keyStatus = fieldStatus(drainTargetType).apiKey;
46
- const indexPrefixStatus = fieldStatus(drainTargetType).indexPrefix;
47
- const structuredDataParametersStatus = fieldStatus(drainTargetType).structuredDataParameters;
48
-
49
- if (credStatus === 'MANDATORY') {
50
- return credentialsExist(drainTargetCredentials);
51
- }
52
- if (credStatus === 'OPTIONAL') {
53
- return true;
54
- }
55
- if (!credStatus && !keyStatus) {
56
- return credentialsEmpty(drainTargetCredentials);
57
- }
58
-
59
- if (keyStatus === 'MANDATORY') {
60
- return keyExist(drainTargetConfig);
61
- }
62
- if (keyStatus === 'OPTIONAL') {
63
- return true;
64
- }
65
- if (!keyStatus) {
66
- return keyEmpty(drainTargetConfig);
67
- }
68
-
69
- if (indexPrefixStatus === 'MANDATORY') {
70
- return indexPrefixExist(drainTargetConfig);
71
- }
72
- if (indexPrefixStatus === 'OPTIONAL') {
73
- return true;
74
- }
75
- if (!indexPrefixStatus) {
76
- return indexPrefixEmpty(drainTargetConfig);
77
- }
78
-
79
- if (structuredDataParametersStatus === 'MANDATORY') {
80
- return structuredDataParametersExist(drainTargetConfig);
81
- }
82
- if (structuredDataParametersStatus === 'OPTIONAL') {
83
- return true;
84
- }
85
- if (!structuredDataParametersStatus) {
86
- return structuredDataParametersEmpty(drainTargetConfig);
87
- }
88
- }
89
- }
90
-
91
- function fieldStatus(drainTargetType) {
92
- return DRAIN_TYPES.find(({ id }) => id === drainTargetType);
93
- }
94
-
95
- function drainTypeExists(drainTargetType) {
96
- return DRAIN_TYPES.some(({ id }) => id === drainTargetType);
97
- }
98
-
99
- function credentialsExist({ username, password }) {
100
- return username != null && password != null;
101
- }
102
-
103
- function credentialsEmpty({ username, password }) {
104
- return username == null && password == null;
105
- }
106
-
107
- function indexPrefixExist({ indexPrefix }) {
108
- return indexPrefix != null;
109
- }
110
-
111
- function indexPrefixEmpty({ indexPrefix }) {
112
- return indexPrefix == null;
113
- }
114
-
115
- function structuredDataParametersExist({ structuredDataParameters }) {
116
- return structuredDataParameters != null;
117
- }
118
-
119
- function structuredDataParametersEmpty({ structuredDataParameters }) {
120
- return structuredDataParameters == null;
121
- }
122
-
123
- function keyExist({ apiKey }) {
124
- return apiKey != null;
125
- }
126
-
127
- function keyEmpty({ apiKey }) {
128
- return apiKey == null;
129
- }
130
-
131
- export function listDrainTypes() {
132
- return cliparse.autocomplete.words(DRAIN_TYPES.map((type) => type.id));
1
+ export const DRAIN_TYPES = {
2
+ DATADOG: { apiCode: 'DATADOG', cliCode: 'datadog', label: 'Datadog' },
3
+ ELASTICSEARCH: { apiCode: 'ELASTICSEARCH', cliCode: 'elasticsearch', label: 'Elasticsearch' },
4
+ NEWRELIC: { apiCode: 'NEWRELIC', cliCode: 'newrelic', label: 'New Relic' },
5
+ OVH_TCP: { apiCode: 'OVH_TCP', cliCode: 'ovh-tcp', label: 'OVH TCP' },
6
+ RAW_HTTP: { apiCode: 'RAW_HTTP', cliCode: 'raw-http', label: 'Raw HTTP' },
7
+ SYSLOG_TCP: { apiCode: 'SYSLOG_TCP', cliCode: 'syslog-tcp', label: 'Syslog TCP' },
8
+ SYSLOG_UDP: { apiCode: 'SYSLOG_UDP', cliCode: 'syslog-udp', label: 'Syslog UDP' },
9
+ };
10
+
11
+ export const DRAIN_TYPE_CLI_CODES = Object.values(DRAIN_TYPES).map(({ cliCode }) => cliCode);
12
+
13
+ export function formatDrain(rawDrain) {
14
+ const drainType = DRAIN_TYPES[rawDrain.recipient.type];
15
+ const drainDetails = [
16
+ ['ID', rawDrain.id],
17
+ ['Status', rawDrain.status.status],
18
+ ['Execution status', rawDrain.execution.status],
19
+ ['URL', rawDrain.recipient.url],
20
+ ['Type', drainType.label],
21
+ ['Custom index', rawDrain.recipient.index],
22
+ ['SD parameters', rawDrain.recipient.rfc5424StructuredDataParameters],
23
+ ['Message output rate', Math.floor(rawDrain.backlog.msgRateOut * 60) + ' messages/minute'],
24
+ ['Message throughput', Math.floor(rawDrain.backlog.msgRateOut) + ' bytes/second'],
25
+ ['Backlog', rawDrain.backlog.msgBacklog + ' pending messages'],
26
+ ['Last error', rawDrain.execution.lastError],
27
+ ];
28
+ return Object.fromEntries(drainDetails.filter(([_name, value]) => value != null));
133
29
  }
@@ -134,11 +134,17 @@ export async function watchDeploymentAndDisplayLogs(options) {
134
134
  }
135
135
 
136
136
  if (deploymentEnded.state === 'OK') {
137
- const favouriteDomain = await getBest(appId, ownerId);
138
137
  Logger.println('');
139
- Logger.println(
140
- `${styleText(['bold', 'green'], '✓ Access your application:')} ${styleText(['underline', 'bold'], `https://${favouriteDomain.fqdn}`)}`,
141
- );
138
+
139
+ // There can be applications without any domain, so we don't fail if we can't get one
140
+ const favouriteDomain = await getBest(appId, ownerId).catch(() => null);
141
+
142
+ if (favouriteDomain) {
143
+ Logger.println(
144
+ `${styleText(['bold', 'green'], '✓ Access your application:')} ${styleText(['underline', 'bold'], `https://${favouriteDomain.fqdn}`)}`,
145
+ );
146
+ }
147
+
142
148
  Logger.println(
143
149
  `${styleText(['bold', 'blue'], '→ Manage your application:')} ${styleText(['underline', 'bold'], `${conf.GOTO_URL}/${appId}`)}`,
144
150
  );
package/src/parsers.js CHANGED
@@ -154,25 +154,6 @@ export function tags(string) {
154
154
  return cliparse.parsers.success(tags);
155
155
  }
156
156
 
157
- export const ipAddressRegex =
158
- /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])$/;
159
-
160
- export function ipAddress(string) {
161
- if (string.match(ipAddressRegex)) {
162
- return cliparse.parsers.success(string);
163
- }
164
- return cliparse.parsers.error(`Invalid IP address '${string}'. Should match ${ipAddressRegex}`);
165
- }
166
-
167
- export const portNumberRegex = /^\d{1,5}$/;
168
-
169
- export function portNumber(number) {
170
- if (String(number).match(portNumberRegex)) {
171
- return cliparse.parsers.success(number);
172
- }
173
- return cliparse.parsers.error(`Invalid port number '${number}'. Should match ${portNumberRegex}`);
174
- }
175
-
176
157
  /**
177
158
  * Parse a duration into seconds
178
159
  * A Zero seconds duration is allowed