clever-tools 4.1.0 → 4.3.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 CHANGED
@@ -28,6 +28,7 @@ import * as drain from '../src/commands/drain.js';
28
28
  import * as emails from '../src/commands/emails.js';
29
29
  import * as env from '../src/commands/env.js';
30
30
  import * as features from '../src/commands/features.js';
31
+ import * as k8s from '../src/commands/k8s.js';
31
32
  import * as keycloak from '../src/commands/keycloak.js';
32
33
  import * as kv from '../src/commands/kv.js';
33
34
  import * as link from '../src/commands/link.js';
@@ -79,6 +80,10 @@ process.stdout.on('error', (error) => {
79
80
  const cliparseCommand = cliparse.command;
80
81
 
81
82
  cliparse.command = function (name, options, commandFunction) {
83
+ if (commandFunction == null) {
84
+ return cliparseCommand(name, options);
85
+ }
86
+
82
87
  return cliparseCommand(name, options, (...args) => {
83
88
  const promise = commandFunction(...args);
84
89
  handleCommandPromise(promise);
@@ -95,6 +100,11 @@ function colorizeExperimentalCommand(command, id) {
95
100
  async function run() {
96
101
  // ARGUMENTS
97
102
  const args = {
103
+ k8sClusterName: cliparse.argument('cluster-name', { description: 'Kubernetes cluster name' }),
104
+ k8sIdOrName: cliparse.argument('id-or-name', {
105
+ description: 'Kubernetes cluster ID or name',
106
+ parser: Parsers.addonIdOrName,
107
+ }),
98
108
  kvRawCommand: cliparse.argument('command', {
99
109
  description: 'The raw command to send to the Materia KV or Redis® add-on',
100
110
  }),
@@ -120,12 +130,16 @@ async function run() {
120
130
  parser: Parsers.ngResourceType,
121
131
  }),
122
132
  ngAnyIdOrLabel: cliparse.argument('id-or-label', {
123
- description: 'ID or Label of a Network group, a member or an (external) peer',
133
+ description: 'ID or Label of a Network Group, a member or an (external) peer',
134
+ parser: Parsers.ngResourceType,
135
+ }),
136
+ ngResourceId: cliparse.argument('id', {
137
+ description: 'ID of a resource to (un)link to a Network Group',
124
138
  parser: Parsers.ngResourceType,
125
139
  }),
126
140
  wgPublicKey: cliparse.argument('public-key', {
127
141
  metavar: 'public_key',
128
- description: 'Wireguard public key of the external peer to link to a Network Group',
142
+ description: 'WireGuard public key of the external peer to link to a Network Group',
129
143
  }),
130
144
  email: cliparse.argument('email', {
131
145
  description: 'Email address',
@@ -188,6 +202,13 @@ async function run() {
188
202
 
189
203
  // OPTIONS
190
204
  const opts = {
205
+ k8sDeployWatch: cliparse.flag('watch', {
206
+ aliases: ['w'],
207
+ description: 'Watch the deployment until the cluster is deployed',
208
+ }),
209
+ kubeConfigForceDownload: cliparse.flag('force', {
210
+ description: 'Force the download of the kubeconfig file, even if it already exists',
211
+ }),
191
212
  targetVersion: cliparse.option('target', {
192
213
  metavar: 'version',
193
214
  description: 'Target version to upgrade to (e.g.: 24, 2.4, 2.4.1)',
@@ -206,7 +227,7 @@ async function run() {
206
227
  ngMembersIdsToLink: cliparse.option('link', {
207
228
  metavar: 'members_ids',
208
229
  description:
209
- "Comma separated list of members IDs to link to a Network Group ('app_xxx', 'addon_xxx', 'external_xxx')",
230
+ 'Comma separated list of members IDs to link to a Network Group (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.)',
210
231
  parser: Parsers.commaSeparated,
211
232
  }),
212
233
  ngResourceType: cliparse.option('type', {
@@ -1035,6 +1056,73 @@ async function run() {
1035
1056
  features.list,
1036
1057
  );
1037
1058
 
1059
+ // K8S COMMAND
1060
+ const k8sListCommand = cliparse.command(
1061
+ 'list',
1062
+ {
1063
+ description: 'List Kubernetes clusters',
1064
+ options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
1065
+ },
1066
+ k8s.list,
1067
+ );
1068
+ const k8sCreateCommand = cliparse.command(
1069
+ 'create',
1070
+ {
1071
+ description: 'Create a Kubernetes cluster',
1072
+ args: [args.k8sClusterName],
1073
+ options: [opts.orgaIdOrName, opts.k8sDeployWatch],
1074
+ },
1075
+ k8s.create,
1076
+ );
1077
+ const k8sDeleteCommand = cliparse.command(
1078
+ 'delete',
1079
+ {
1080
+ description: 'Delete a Kubernetes cluster',
1081
+ options: [opts.orgaIdOrName, opts.confirmAddonDeletion],
1082
+ args: [args.k8sIdOrName],
1083
+ },
1084
+ k8s.del,
1085
+ );
1086
+ const k8sGetCommand = cliparse.command(
1087
+ 'get',
1088
+ {
1089
+ description: 'Get information about a Kubernetes cluster',
1090
+ args: [args.k8sIdOrName],
1091
+ options: [opts.orgaIdOrName, opts.humanJsonOutputFormat],
1092
+ },
1093
+ k8s.get,
1094
+ );
1095
+ const k8sGetConfigCommand = cliparse.command(
1096
+ 'get-kubeconfig',
1097
+ {
1098
+ description: 'Get configuration of a Kubernetes cluster',
1099
+ args: [args.k8sIdOrName],
1100
+ options: [opts.orgaIdOrName],
1101
+ },
1102
+ k8s.getConfig,
1103
+ );
1104
+ const k8sAddPersistentStorageCommand = cliparse.command(
1105
+ 'add-persistent-storage',
1106
+ {
1107
+ description: 'Activate persistent storage to a deployed Kubernetes cluster',
1108
+ args: [args.k8sIdOrName],
1109
+ options: [opts.orgaIdOrName],
1110
+ },
1111
+ k8s.addPersistentStorage,
1112
+ );
1113
+
1114
+ const k8sCommand = cliparse.command('k8s', {
1115
+ description: 'Manage Kubernetes clusters',
1116
+ commands: [
1117
+ k8sListCommand,
1118
+ k8sCreateCommand,
1119
+ k8sAddPersistentStorageCommand,
1120
+ k8sDeleteCommand,
1121
+ k8sGetCommand,
1122
+ k8sGetConfigCommand,
1123
+ ],
1124
+ });
1125
+
1038
1126
  // KEYCLOAK COMMAND
1039
1127
  const keycloakGetCommand = cliparse.command(
1040
1128
  'get',
@@ -1413,16 +1501,18 @@ async function run() {
1413
1501
  const ngLinkCommand = cliparse.command(
1414
1502
  'link',
1415
1503
  {
1416
- description: 'Link an application or a database add-on by its ID to a Network Group',
1417
- args: [args.ngAnyIdOrLabel, args.ngIdOrLabel],
1504
+ description:
1505
+ 'Link a resource by its ID (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.) to a Network Group',
1506
+ args: [args.ngResourceId, args.ngIdOrLabel],
1418
1507
  },
1419
1508
  ng.linkToNg,
1420
1509
  );
1421
1510
  const ngUnlinkCommand = cliparse.command(
1422
1511
  'unlink',
1423
1512
  {
1424
- description: 'Unlink an application or a database add-on by its ID from a Network Group',
1425
- args: [args.ngAnyIdOrLabel, args.ngIdOrLabel],
1513
+ description:
1514
+ 'Unlink a resource by its ID (app_xxx, external_xxx, mysql_xxx, postgresql_xxx, redis_xxx, etc.) from a Network Group',
1515
+ args: [args.ngResourceId, args.ngIdOrLabel],
1426
1516
  },
1427
1517
  ng.unlinkFromNg,
1428
1518
  );
@@ -1438,7 +1528,7 @@ async function run() {
1438
1528
  const ngGetConfigCommand = cliparse.command(
1439
1529
  'get-config',
1440
1530
  {
1441
- description: 'Get the Wireguard configuration of a peer in a Network Group',
1531
+ description: 'Get the WireGuard configuration of a peer in a Network Group',
1442
1532
  args: [args.ngExternalIdOrLabel, args.ngIdOrLabel],
1443
1533
  options: [opts.humanJsonOutputFormat],
1444
1534
  },
@@ -1977,18 +2067,10 @@ async function run() {
1977
2067
  },
1978
2068
  database.listBackups,
1979
2069
  );
1980
- const databaseCommand = cliparse.command(
1981
- 'database',
1982
- {
1983
- description: 'List available databases',
1984
- commands: [backupsCommand],
1985
- },
1986
- async () => {
1987
- console.info('This command is not available, you can try the following commands:');
1988
- console.info('clever database backups');
1989
- console.info('clever database backups download');
1990
- },
1991
- );
2070
+ const databaseCommand = cliparse.command('database', {
2071
+ description: 'Manage databases and backups',
2072
+ commands: [backupsCommand],
2073
+ });
1992
2074
 
1993
2075
  // Patch help command description
1994
2076
  cliparseCommands.helpCommand.description = 'Display help about the Clever Cloud CLI';
@@ -2046,6 +2128,10 @@ async function run() {
2046
2128
  commands.push(colorizeExperimentalCommand(otoroshiCommand, 'operators'));
2047
2129
  }
2048
2130
 
2131
+ if (featuresFromConf.k8s) {
2132
+ commands.push(colorizeExperimentalCommand(k8sCommand, 'k8s'));
2133
+ }
2134
+
2049
2135
  if (featuresFromConf.kv) {
2050
2136
  commands.push(colorizeExperimentalCommand(kvRawCommand, 'kv'));
2051
2137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clever-tools",
3
- "version": "4.1.0",
3
+ "version": "4.3.0",
4
4
  "description": "Command Line Interface for Clever Cloud.",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,96 @@
1
+ // TODO: Move this to the Clever Cloud JS Client
2
+
3
+ /**
4
+ * POST /v4/kubernetes/organisations/{ownerId}/clusters
5
+ * @param {Object} params
6
+ * @param {String} params.ownerId
7
+ * @param {Object} body
8
+ */
9
+ export function createK8sCluster(params, body) {
10
+ return Promise.resolve({
11
+ method: 'post',
12
+ url: `/v4/kubernetes/organisations/${params.ownerId}/clusters`,
13
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
14
+ // no queryParams
15
+ body,
16
+ });
17
+ }
18
+
19
+ /**
20
+ * DELETE /v4/kubernetes/organisations/{ownerId}/clusters/{clusterId}
21
+ * @param {Object} params
22
+ * @param {String} params.ownerId
23
+ * @param {String} params.clusterId
24
+ */
25
+ export function deleteK8sCluster(params) {
26
+ return Promise.resolve({
27
+ method: 'delete',
28
+ url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}`,
29
+ headers: { Accept: 'application/json' },
30
+ // no queryParams
31
+ // no body
32
+ });
33
+ }
34
+
35
+ /**
36
+ * GET /v4/kubernetes/organisations/{ownerId}/clusters
37
+ * @param {Object} params
38
+ * @param {String} params.ownerId
39
+ */
40
+ export function listK8sClusters(params) {
41
+ return Promise.resolve({
42
+ method: 'get',
43
+ url: `/v4/kubernetes/organisations/${params.ownerId}/clusters`,
44
+ headers: { Accept: 'application/json' },
45
+ // no queryParams
46
+ // no body
47
+ });
48
+ }
49
+
50
+ /**
51
+ * GET /v4/kubernetes/organisations/{ownerId}/clusters/{clusterId}
52
+ * @param {Object} params
53
+ * @param {String} params.ownerId
54
+ * @param {String} params.clusterId
55
+ */
56
+ export function getK8sAddon(params) {
57
+ return Promise.resolve({
58
+ method: 'get',
59
+ url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}`,
60
+ headers: { Accept: 'application/json' },
61
+ // no queryParams
62
+ // no body
63
+ });
64
+ }
65
+
66
+ /**
67
+ * GET /v4/kubernetes/organisations/{ownerId}/clusters/{clusterId}/kubeconfig.yaml
68
+ * @param {Object} params
69
+ * @param {String} params.ownerId
70
+ * @param {String} params.clusterId
71
+ */
72
+ export function getK8sConfig(params) {
73
+ return Promise.resolve({
74
+ method: 'get',
75
+ url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}/kubeconfig.yaml`,
76
+ // headers: { Accept: 'application/x-yaml' },
77
+ // no queryParams
78
+ // no body
79
+ });
80
+ }
81
+
82
+ /**
83
+ * POST /v4/kubernetes/organisations/ownerId}/clusters/{clusterId}/csi/ceph
84
+ * @param {Object} params
85
+ * @param {String} params.ownerId
86
+ * @param {String} params.clusterId
87
+ */
88
+ export function addK8sPersistentStorage(params) {
89
+ return Promise.resolve({
90
+ method: 'post',
91
+ url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}/csi/ceph`,
92
+ headers: { Accept: 'application/json' },
93
+ // no queryParams
94
+ // no body
95
+ });
96
+ }
@@ -3,6 +3,7 @@ import { styleText } from '../lib/style-text.js';
3
3
  import { getAllEnvVars } from '@clevercloud/client/esm/api/v2/addon.js';
4
4
  import { toNameEqualsValueString } from '@clevercloud/client/esm/utils/env-vars.js';
5
5
  import dedent from 'dedent';
6
+ import { getOperator } from '../clever-client/operators.js';
6
7
  import { formatTable } from '../format-table.js';
7
8
  import { Logger } from '../logger.js';
8
9
  import * as Addon from '../models/addon.js';
@@ -53,6 +54,45 @@ export async function list(params) {
53
54
  }
54
55
  }
55
56
 
57
+ const ADDON_PROVIDERS = {
58
+ keycloak: {
59
+ name: 'Keycloak',
60
+ isOperator: true,
61
+ postCreateInstructions: `Learn more about Keycloak on Clever Cloud: ${conf.DOC_URL}/addons/keycloak/`,
62
+ },
63
+ kv: {
64
+ name: 'Materia KV',
65
+ isOperator: false,
66
+ status: 'beta',
67
+ postCreateInstructions: (addonId) => dedent`
68
+ ${styleText('yellow', "You can easily use Materia KV with 'redis-cli', with such commands:")}
69
+ ${styleText('blue', `source <(clever addon env ${addonId} -F shell)`)}
70
+ ${styleText('blue', 'redis-cli -h $KV_HOST -p $KV_PORT --tls')}
71
+ Learn more about Materia KV on Clever Cloud: ${conf.DOC_URL}/addons/materia-kv/
72
+ `,
73
+ },
74
+ 'addon-matomo': {
75
+ name: 'Matomo',
76
+ isOperator: true,
77
+ postCreateInstructions: `Learn more about Matomo on Clever Cloud: ${conf.DOC_URL}/addons/matomo/`,
78
+ },
79
+ metabase: {
80
+ name: 'Metabase',
81
+ isOperator: true,
82
+ postCreateInstructions: `Learn more about Metabase on Clever Cloud: ${conf.DOC_URL}/addons/metabase/`,
83
+ },
84
+ otoroshi: {
85
+ name: 'Otoroshi with LLM',
86
+ isOperator: true,
87
+ postCreateInstructions: `Learn more about Otoroshi with LLM on Clever Cloud: ${conf.DOC_URL}/addons/otoroshi/`,
88
+ },
89
+ 'addon-pulsar': {
90
+ name: 'Pulsar',
91
+ isOperator: false,
92
+ postCreateInstructions: `Learn more about Pulsar on Clever Cloud: ${conf.DOC_URL}/addons/pulsar/`,
93
+ },
94
+ };
95
+
56
96
  export async function create(params) {
57
97
  const [providerName, name] = params.args;
58
98
  const {
@@ -62,23 +102,21 @@ export async function create(params) {
62
102
  yes: skipConfirmation,
63
103
  org: orgaIdOrName,
64
104
  format,
105
+ 'addon-version': version,
106
+ option: addonOptions,
65
107
  } = params.options;
66
- const version = params.options['addon-version'];
67
- const addonOptions = parseAddonOptions(params.options.option);
68
-
69
- const ownerId = orgaIdOrName != null ? await Organisation.getId(orgaIdOrName) : await User.getCurrentId();
70
108
 
71
109
  const addonToCreate = {
72
- ownerId,
73
110
  name,
74
111
  providerName,
75
112
  planName,
76
113
  region,
77
114
  skipConfirmation,
78
115
  version,
79
- addonOptions,
116
+ addonOptions: parseAddonOptions(addonOptions),
80
117
  };
81
118
 
119
+ const ownerId = orgaIdOrName != null ? await Organisation.getId(orgaIdOrName) : await User.getCurrentId();
82
120
  if (linkedAppAlias != null) {
83
121
  const linkedAppData = await AppConfig.getAppDetails({ alias: linkedAppAlias });
84
122
  if (orgaIdOrName != null && linkedAppData.ownerId !== ownerId && format === 'human') {
@@ -86,163 +124,98 @@ export async function create(params) {
86
124
  'The specified application does not belong to the specified organisation. Ignoring the `--org` option',
87
125
  );
88
126
  }
89
- const newAddon = await Addon.create({
90
- ...addonToCreate,
91
- ownerId: linkedAppData.ownerId,
92
- });
93
- await Addon.link(linkedAppData.ownerId, linkedAppData.appId, { addon_id: newAddon.id });
94
- displayAddon(
95
- format,
96
- newAddon,
97
- providerName,
98
- `Add-on created and linked to application ${linkedAppAlias} successfully!`,
99
- );
127
+ addonToCreate.ownerId = linkedAppData.ownerId;
100
128
  } else {
101
- const newAddon = await Addon.create(addonToCreate);
102
- displayAddon(format, newAddon, providerName, 'Add-on created successfully!');
129
+ addonToCreate.ownerId = ownerId;
103
130
  }
104
- }
105
131
 
106
- function displayAddon(format, addon, providerName, message) {
107
- const PROVIDERS_WITH_URL = {
108
- keycloak: {
109
- name: 'Keycloak',
110
- urlEnv: 'CC_KEYCLOAK_URL',
111
- },
112
- 'addon-matomo': {
113
- name: 'Matomo',
114
- urlEnv: 'MATOMO_URL',
115
- },
116
- metabase: {
117
- name: 'Metabase',
118
- urlEnv: 'METABASE_URL',
119
- },
120
- otoroshi: {
121
- name: 'Otoroshi with LLM',
122
- urlEnv: 'CC_OTOROSHI_URL',
123
- },
124
- };
132
+ const newAddon = await Addon.create(addonToCreate);
125
133
 
126
- const WIP_PROVIDERS = {
127
- keycloak: {
128
- status: '',
129
- postCreateInstructions: dedent`
130
- Learn more about Keycloak on Clever Cloud: ${conf.DOC_URL}/addons/keycloak/,
131
- `,
132
- },
133
- kv: {
134
- status: 'alpha',
135
- postCreateInstructions: dedent`
136
- ${styleText('yellow', "You can easily use Materia KV with 'redis-cli', with such commands:")}
137
- ${styleText('blue', `source <(clever addon env ${addon.id} -F shell)`)}
138
- ${styleText('blue', 'redis-cli -h $KV_HOST -p $KV_PORT --tls')}
139
- Learn more about Materia KV on Clever Cloud: ${conf.DOC_URL}/addons/materia-kv/
140
- `,
141
- },
142
- 'addon-matomo': {
143
- status: '',
144
- postCreateInstructions: dedent`
145
- Learn more about Matomo on Clever Cloud: ${conf.DOC_URL}/addons/matomo/
146
- `,
147
- },
148
- metabase: {
149
- status: '',
150
- postCreateInstructions: dedent`
151
- Learn more about Metabase on Clever Cloud: ${conf.DOC_URL}/addons/metabase/
152
- `,
153
- },
154
- otoroshi: {
155
- status: '',
156
- postCreateInstructions: dedent`
157
- Learn more about Otoroshi with LLM on Clever Cloud: ${conf.DOC_URL}/addons/otoroshi/
158
- `,
159
- },
160
- 'addon-pulsar': {
161
- status: 'beta',
162
- postCreateInstructions: dedent`
163
- Learn more about Pulsar on Clever Cloud: ${conf.DOC_URL}/addons/pulsar/
164
- `,
165
- },
166
- };
134
+ if (linkedAppAlias != null) {
135
+ const linkedAppData = await AppConfig.getAppDetails({ alias: linkedAppAlias });
136
+ await Addon.link(linkedAppData.ownerId, linkedAppData.appId, { addon_id: newAddon.id });
137
+ }
167
138
 
168
- let providerNameToShow = '';
139
+ const provider = ADDON_PROVIDERS[providerName];
169
140
  let statusMessage = '';
170
- if (providerName in WIP_PROVIDERS && WIP_PROVIDERS[providerName].status !== '') {
171
- providerNameToShow = providerName === 'kv' ? 'Materia KV' : providerName;
141
+ if (provider?.status) {
142
+ statusMessage = `The ${provider.name} provider is in ${provider.status} testing phase`;
143
+ if (provider.status === 'alpha') {
144
+ statusMessage += ". Don't store sensitive or production grade data.";
145
+ }
146
+ }
147
+
148
+ // JSON format
149
+ if (format === 'json') {
150
+ const jsonAddon = {
151
+ id: newAddon.id,
152
+ realId: newAddon.realId,
153
+ name: newAddon.name,
154
+ env: newAddon.env,
155
+ };
156
+
157
+ if (provider?.status) {
158
+ jsonAddon.availability = provider.status;
159
+ jsonAddon.message = statusMessage;
160
+ }
172
161
 
173
- statusMessage = `The ${providerNameToShow} provider is in ${WIP_PROVIDERS[providerName].status} testing phase`;
174
- statusMessage +=
175
- WIP_PROVIDERS[providerName].status === 'alpha' ? ". Don't store sensitive or production grade data." : '';
162
+ Logger.printJson(jsonAddon);
163
+ return;
176
164
  }
177
165
 
178
- switch (format) {
179
- case 'json': {
180
- const jsonAddon = {
181
- id: addon.id,
182
- realId: addon.realId,
183
- name: addon.name,
184
- env: addon.env,
185
- };
166
+ // Human format
167
+ if (linkedAppAlias != null) {
168
+ Logger.println(`Add-on created and linked to application ${linkedAppAlias} successfully!`);
169
+ } else {
170
+ Logger.println('Add-on created successfully!');
171
+ }
186
172
 
187
- Logger.printJson(
188
- WIP_PROVIDERS[providerName] != null
189
- ? { ...jsonAddon, availability: WIP_PROVIDERS[providerName].status, message: statusMessage }
190
- : jsonAddon,
191
- );
192
- break;
193
- }
173
+ Logger.println(`ID: ${newAddon.id}`);
174
+ Logger.println(`Real ID: ${newAddon.realId}`);
175
+ Logger.println(`Name: ${newAddon.name}`);
194
176
 
195
- case 'human':
196
- default:
197
- Logger.println([message, `ID: ${addon.id}`, `Real ID: ${addon.realId}`, `Name: ${addon.name}`].join('\n'));
198
-
199
- if (providerName in PROVIDERS_WITH_URL) {
200
- const provider = PROVIDERS_WITH_URL[providerName];
201
- const urlEnv = addon.env.find((entry) => entry.name === provider.urlEnv);
202
- const urlToShow = urlEnv.value;
203
- const urlToShowWithHttps = urlToShow.startsWith('http') ? urlToShow : `https://${urlToShow}`;
204
-
205
- if (urlEnv) {
206
- Logger.println();
207
- Logger.println(`Your ${provider.name} is starting:`);
208
- Logger.println(` - Access it: ${urlToShowWithHttps}`);
209
- Logger.println(` - Manage it: ${conf.GOTO_URL}/${addon.id}`);
210
- }
177
+ const operator = ADDON_PROVIDERS[providerName]?.isOperator
178
+ ? await getOperator({ provider: providerName, realId: newAddon.realId }).then(sendToApi)
179
+ : null;
211
180
 
212
- if (providerName === 'keycloak') {
213
- Logger.println();
214
- Logger.println(
215
- "An initial account has been created, you'll be invited to change the password at first login:",
216
- );
217
- Logger.println(` - Admin user name: ${addon.env.find((e) => e.name === 'CC_KEYCLOAK_ADMIN').value}`);
218
- Logger.println(
219
- ` - Temporary password: ${addon.env.find((e) => e.name === 'CC_KEYCLOAK_ADMIN_DEFAULT_PASSWORD').value}`,
220
- );
221
- }
181
+ if (operator) {
182
+ if (operator.accessUrl) {
183
+ Logger.println();
184
+ Logger.println(`Your ${ADDON_PROVIDERS[providerName].name} is starting:`);
185
+ Logger.println(` - Access it: ${operator.accessUrl}`);
186
+ Logger.println(` - Manage it: ${conf.GOTO_URL}/${newAddon.id}`);
187
+ }
222
188
 
223
- if (providerName === 'otoroshi') {
224
- Logger.println();
225
- Logger.println(
226
- 'An initial account has been created, change the password at first login (Security -> Administrators -> Edit user):',
227
- );
228
- Logger.println(
229
- ` - Admin user name: ${addon.env.find((e) => e.name === 'CC_OTOROSHI_INITIAL_ADMIN_LOGIN').value}`,
230
- );
231
- Logger.println(
232
- ` - Initial password: ${addon.env.find((e) => e.name === 'CC_OTOROSHI_INITIAL_ADMIN_PASSWORD').value}`,
233
- );
234
- }
189
+ if (operator.initialCredentials) {
190
+ if (providerName === 'keycloak') {
191
+ Logger.println();
192
+ Logger.println("An initial account has been created, you'll be invited to change the password at first login:");
193
+ Logger.println(` - Admin user name: ${operator.initialCredentials.user}`);
194
+ Logger.println(` - Temporary password: ${operator.initialCredentials.password}`);
235
195
  }
236
-
237
- if (providerName in WIP_PROVIDERS) {
196
+ if (providerName === 'otoroshi') {
238
197
  Logger.println();
198
+ Logger.println(
199
+ 'An initial account has been created, change the password at first login (Security -> Administrators -> Edit user):',
200
+ );
201
+ Logger.println(` - Admin user name: ${operator.initialCredentials.user}`);
202
+ Logger.println(` - Password: ${operator.initialCredentials.password}`);
203
+ }
204
+ }
205
+ }
239
206
 
240
- if (statusMessage !== '') {
241
- Logger.println(styleText('yellow', `/!\\ ${statusMessage}`));
242
- }
207
+ if (provider?.postCreateInstructions) {
208
+ Logger.println();
243
209
 
244
- Logger.println(WIP_PROVIDERS[providerName].postCreateInstructions);
245
- }
210
+ if (statusMessage !== '') {
211
+ Logger.println(styleText('yellow', `/!\\ ${statusMessage}`));
212
+ }
213
+
214
+ if (typeof provider.postCreateInstructions === 'function') {
215
+ Logger.println(provider.postCreateInstructions(newAddon.id));
216
+ } else {
217
+ Logger.println(provider.postCreateInstructions);
218
+ }
246
219
  }
247
220
  }
248
221