@rancher/shell 2.0.1 → 2.0.2
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/assets/translations/en-us.yaml +73 -34
- package/assets/translations/zh-hans.yaml +1 -0
- package/components/AssignTo.vue +2 -0
- package/components/PromptRemove.vue +8 -3
- package/components/Questions/index.vue +2 -2
- package/components/ResourceDetail/Masthead.vue +1 -0
- package/components/auth/RoleDetailEdit.vue +5 -4
- package/components/fleet/FleetClusters.vue +0 -3
- package/components/form/Members/ClusterPermissionsEditor.vue +1 -1
- package/components/form/ProjectMemberEditor.vue +1 -1
- package/components/form/ResourceLabeledSelect.vue +11 -3
- package/components/form/labeled-select-utils/labeled-select.utils.ts +1 -1
- package/components/formatter/CloudCredExpired.vue +69 -0
- package/components/formatter/Date.vue +1 -1
- package/components/nav/Header.vue +9 -5
- package/components/nav/TopLevelMenu.vue +115 -51
- package/components/nav/__tests__/TopLevelMenu.test.ts +53 -27
- package/config/labels-annotations.js +2 -0
- package/config/pagination-table-headers.js +5 -4
- package/config/roles.ts +34 -19
- package/config/router/navigation-guards/attempt-first-login.js +1 -1
- package/config/router/navigation-guards/authentication.js +1 -1
- package/config/router/navigation-guards/i18n.js +1 -1
- package/config/router/navigation-guards/index.js +2 -1
- package/config/router/navigation-guards/load-initial-settings.js +1 -1
- package/config/router/navigation-guards/runtime-extension-route.js +31 -0
- package/config/router/routes.js +10 -1
- package/config/uiplugins.js +130 -61
- package/core/plugin.ts +5 -0
- package/core/plugins.js +7 -1
- package/detail/catalog.cattle.io.app.vue +17 -4
- package/detail/fleet.cattle.io.cluster.vue +11 -9
- package/detail/fleet.cattle.io.gitrepo.vue +1 -1
- package/edit/provisioning.cattle.io.cluster/__tests__/Basics.test.ts +86 -13
- package/edit/provisioning.cattle.io.cluster/__tests__/DirectoryConfig.test.ts +3 -134
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +209 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +8 -4
- package/edit/provisioning.cattle.io.cluster/rke2.vue +128 -17
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnAdditionalManifest.vue +50 -0
- package/edit/provisioning.cattle.io.cluster/tabs/AddOnConfig.vue +29 -64
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +42 -3
- package/edit/provisioning.cattle.io.cluster/tabs/DirectoryConfig.vue +22 -86
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +8 -2
- package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +61 -0
- package/initialize/entry-helpers.js +4 -21
- package/list/provisioning.cattle.io.cluster.vue +56 -5
- package/mixins/__tests__/chart.test.ts +4 -1
- package/mixins/chart.js +36 -16
- package/models/__tests__/apps.deployment.test.ts +93 -0
- package/models/apps.deployment.js +18 -4
- package/models/catalog.cattle.io.app.js +108 -21
- package/models/cloudcredential.js +159 -2
- package/models/fleet.cattle.io.gitrepo.js +4 -13
- package/models/management.cattle.io.cluster.js +15 -4
- package/models/management.cattle.io.user.js +3 -3
- package/models/nodedriver.js +5 -0
- package/models/provisioning.cattle.io.cluster.js +41 -3
- package/package.json +1 -1
- package/pages/404.vue +15 -0
- package/pages/auth/login.vue +4 -1
- package/pages/auth/setup.vue +4 -1
- package/pages/c/_cluster/apps/charts/install.vue +2 -1
- package/pages/c/_cluster/explorer/__tests__/index.test.ts +1 -1
- package/pages/c/_cluster/explorer/index.vue +6 -2
- package/pages/c/_cluster/fleet/index.vue +11 -5
- package/pages/c/_cluster/manager/cloudCredential/index.vue +68 -4
- package/pages/c/_cluster/manager/jwt.authentication/index.vue +10 -4
- package/pages/c/_cluster/settings/performance.vue +2 -2
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +7 -10
- package/pages/c/_cluster/uiplugins/index.vue +28 -18
- package/pages/home.vue +2 -13
- package/plugins/dashboard-store/actions.js +1 -1
- package/plugins/dashboard-store/getters.js +1 -1
- package/plugins/steve/__tests__/getters.test.ts +5 -5
- package/plugins/steve/getters.js +6 -4
- package/plugins/steve/hybrid-class.js +1 -5
- package/scripts/extension/bundle +1 -1
- package/scripts/extension/helm/charts/ui-plugin-server/Chart.yaml +1 -1
- package/scripts/publish-shell.sh +56 -59
- package/scripts/test-plugins-build.sh +45 -39
- package/scripts/typegen.sh +26 -23
- package/store/type-map.js +4 -2
- package/types/shell/index.d.ts +10 -0
- package/types/store/pagination.types.ts +1 -1
- package/utils/cluster.js +9 -0
- package/utils/settings.ts +3 -1
- package/utils/string.js +9 -0
- package/utils/v-sphere.ts +251 -0
- package/creators/app/app.package.json +0 -14
- package/creators/app/files/.eslintignore +0 -16
- package/creators/app/files/.eslintrc.js +0 -173
- package/creators/app/files/.gitignore +0 -70
- package/creators/app/files/.gitlab-ci.yml +0 -14
- package/creators/app/files/.vscode/settings.json +0 -21
- package/creators/app/files/babel.config.js +0 -1
- package/creators/app/files/tsconfig.json +0 -42
- package/creators/app/files/vue.config.js +0 -6
- package/creators/app/init +0 -120
- package/creators/app/package.json +0 -25
- package/creators/pkg/files/.github/workflows/build-extension-catalog.yml +0 -24
- package/creators/pkg/files/.github/workflows/build-extension-charts.yml +0 -22
- package/creators/pkg/files/babel.config.js +0 -1
- package/creators/pkg/files/index.ts +0 -14
- package/creators/pkg/files/tsconfig.json +0 -53
- package/creators/pkg/files/vue.config.js +0 -1
- package/creators/pkg/init +0 -286
- package/creators/pkg/package.json +0 -19
- package/creators/pkg/pkg.package.json +0 -21
- package/creators/pkg/vue-shim.ts +0 -4
- package/creators/update/init +0 -56
- package/creators/update/package.json +0 -20
- package/creators/update/upgrade +0 -56
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
import { CATALOG as CATALOG_ANNOTATIONS, FLEET } from '@shell/config/labels-annotations';
|
|
5
5
|
import { compare, isPrerelease, sortable } from '@shell/utils/version';
|
|
6
6
|
import { filterBy } from '@shell/utils/array';
|
|
7
|
-
import { CATALOG, MANAGEMENT, NORMAN } from '@shell/config/types';
|
|
7
|
+
import { CATALOG, MANAGEMENT, NORMAN, SECRET } from '@shell/config/types';
|
|
8
8
|
import { SHOW_PRE_RELEASE } from '@shell/store/prefs';
|
|
9
9
|
import { set } from '@shell/utils/object';
|
|
10
10
|
|
|
@@ -279,28 +279,115 @@ export default class CatalogApp extends SteveModel {
|
|
|
279
279
|
};
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
}
|
|
282
|
+
async deployedAsLegacy() {
|
|
283
|
+
await this.fetchValues();
|
|
284
|
+
|
|
285
|
+
if (this.values?.global) {
|
|
286
|
+
const { clusterName, projectName } = this.values.global;
|
|
287
|
+
|
|
288
|
+
if (clusterName && projectName) {
|
|
289
|
+
try {
|
|
290
|
+
const legacyApp = await this.$dispatch('rancher/find', {
|
|
291
|
+
type: NORMAN.APP,
|
|
292
|
+
id: `${ projectName }:${ this.metadata?.name }`,
|
|
293
|
+
opt: { url: `/v3/project/${ clusterName }:${ projectName }/apps/${ projectName }:${ this.metadata?.name }` }
|
|
294
|
+
}, { root: true });
|
|
295
|
+
|
|
296
|
+
if (legacyApp) {
|
|
297
|
+
return legacyApp;
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {}
|
|
300
300
|
}
|
|
301
|
+
}
|
|
301
302
|
|
|
302
|
-
|
|
303
|
-
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* User and Chart values live in a helm secret, so fetch it (with special param)
|
|
308
|
+
*/
|
|
309
|
+
async fetchValues(force = false) {
|
|
310
|
+
if (!this.secretId) {
|
|
311
|
+
// If there's no secret id this isn't ever going to work, no need to carry on
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const haveValues = !!this._values && !!this._chartValues;
|
|
316
|
+
|
|
317
|
+
if (haveValues && !force) {
|
|
318
|
+
// If we already have the required values and we're not forced to re-fetch, no need to carry on
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
await this.$dispatch('find', {
|
|
324
|
+
type: SECRET,
|
|
325
|
+
id: this.secretId,
|
|
326
|
+
opt: {
|
|
327
|
+
force: force || (!!this._secret && !haveValues), // force if explicitly requested or there's ean existing secret without the required values we have a secret without the values in (Secret has been fetched another way)
|
|
328
|
+
watch: false, // Cannot watch with custom params (they are dropped on calls made when resyncing over socket)
|
|
329
|
+
params: { includeHelmData: true }
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
} catch (e) {
|
|
333
|
+
console.error(`Cannot find values for ${ this.id } (unable to fetch)`, e); // eslint-disable-line no-console
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
get secretId() {
|
|
338
|
+
const metadata = this.metadata;
|
|
339
|
+
const secretReference = metadata.ownerReferences?.find((ow) => ow.kind.toLowerCase() === SECRET);
|
|
340
|
+
|
|
341
|
+
const secretId = secretReference?.name;
|
|
342
|
+
const secretNamespace = metadata.namespace;
|
|
343
|
+
|
|
344
|
+
if (!secretNamespace || !secretId) {
|
|
345
|
+
console.warn(`Cannot find values for ${ this.id } (cannot find related secret namespace or id)`); // eslint-disable-line no-console
|
|
346
|
+
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return `${ secretNamespace }/${ secretId }`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
get _secret() {
|
|
354
|
+
return this.secretId ? this.$getters['byId'](SECRET, this.secretId) : null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_validateSecret(noun) {
|
|
358
|
+
if (this._secret === undefined) {
|
|
359
|
+
throw new Error(`Cannot find ${ noun } for ${ this.id } (chart secret has not been fetched via app \`fetchValues\`)`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (this._secret === null) {
|
|
363
|
+
throw new Error(`Cannot find ${ noun } for ${ this.id } (chart secret cannot or has failed to fetch) `);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The user's helm values
|
|
369
|
+
*/
|
|
370
|
+
get values() {
|
|
371
|
+
this._validateSecret('values');
|
|
372
|
+
|
|
373
|
+
return this._values;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
get _values() {
|
|
377
|
+
return this._secret?.data?.release?.config;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The Charts default helm values
|
|
382
|
+
*/
|
|
383
|
+
get chartValues() {
|
|
384
|
+
this._validateSecret('chartValues');
|
|
385
|
+
|
|
386
|
+
return this._chartValues;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
get _chartValues() {
|
|
390
|
+
return this._secret?.data?.release?.chart?.values;
|
|
304
391
|
}
|
|
305
392
|
}
|
|
306
393
|
|
|
@@ -1,11 +1,65 @@
|
|
|
1
|
-
import { CAPI } from '@shell/config/labels-annotations';
|
|
1
|
+
import { CAPI, CLOUD_CREDENTIALS } from '@shell/config/labels-annotations';
|
|
2
2
|
import { fullFields, prefixFields, simplify, suffixFields } from '@shell/store/plugins';
|
|
3
3
|
import { isEmpty, set } from '@shell/utils/object';
|
|
4
|
-
import { SECRET } from '@shell/config/types';
|
|
4
|
+
import { MANAGEMENT, SECRET } from '@shell/config/types';
|
|
5
5
|
import { escapeHtml } from '@shell/utils/string';
|
|
6
6
|
import NormanModel from '@shell/plugins/steve/norman-class';
|
|
7
|
+
import { DATE_FORMAT, TIME_FORMAT } from '@shell/store/prefs';
|
|
8
|
+
import day from 'dayjs';
|
|
9
|
+
|
|
10
|
+
const harvesterProvider = 'harvester';
|
|
11
|
+
|
|
12
|
+
const renew = {
|
|
13
|
+
[harvesterProvider]: {
|
|
14
|
+
renew: ({ cloudCredential, $ctx }) => {
|
|
15
|
+
return renew[harvesterProvider].renewBulk(
|
|
16
|
+
{ cloudCredentials: [cloudCredential], $ctx }
|
|
17
|
+
);
|
|
18
|
+
},
|
|
19
|
+
renewBulk: async({ cloudCredentials, $ctx }) => {
|
|
20
|
+
// A harvester cloud credential (at the moment) is a kubeconfig complete with expiring token
|
|
21
|
+
// So to renew we just need to generate a new kubeconfig and save it to the cc (similar to shell/cloud-credential/harvester.vue)
|
|
22
|
+
await Promise.all(cloudCredentials.map(async(cc) => {
|
|
23
|
+
try {
|
|
24
|
+
if (!cc.harvestercredentialConfig?.clusterId) {
|
|
25
|
+
throw new Error(`credential has no matching harvester cluster`);
|
|
26
|
+
}
|
|
27
|
+
const mgmtCluster = $ctx.rootGetters['management/byId'](MANAGEMENT.CLUSTER, cc.harvestercredentialConfig.clusterId);
|
|
28
|
+
|
|
29
|
+
if (!mgmtCluster) {
|
|
30
|
+
throw new Error(`cannot find harvester cluster`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const kubeconfigContent = await mgmtCluster.generateKubeConfig();
|
|
34
|
+
|
|
35
|
+
cc.setData('kubeconfigContent', kubeconfigContent);
|
|
36
|
+
|
|
37
|
+
await cc.save();
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`Unable to refresh harvester cloud credential '${ cc.id }'`, error); // eslint-disable-line no-console
|
|
40
|
+
}
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
7
45
|
|
|
8
46
|
export default class CloudCredential extends NormanModel {
|
|
47
|
+
get _availableActions() {
|
|
48
|
+
const out = super._availableActions;
|
|
49
|
+
|
|
50
|
+
out.splice(0, 0, { divider: true });
|
|
51
|
+
out.splice(0, 0, {
|
|
52
|
+
action: 'renew',
|
|
53
|
+
enabled: this.canRenew,
|
|
54
|
+
bulkable: this.canBulkRenew,
|
|
55
|
+
bulkAction: 'renewBulk',
|
|
56
|
+
icon: 'icon icon-fw icon-refresh',
|
|
57
|
+
label: this.t('manager.cloudCredentials.renew'),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
9
63
|
get hasSensitiveData() {
|
|
10
64
|
return true;
|
|
11
65
|
}
|
|
@@ -177,4 +231,107 @@ export default class CloudCredential extends NormanModel {
|
|
|
177
231
|
get doneRoute() {
|
|
178
232
|
return 'c-cluster-manager-secret';
|
|
179
233
|
}
|
|
234
|
+
|
|
235
|
+
get canRenew() {
|
|
236
|
+
return !!renew[this.provider]?.renew && this.expires !== undefined && this.canUpdate;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
get canBulkRenew() {
|
|
240
|
+
return !!renew[this.provider]?.renewBulk;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get expiresForSort() {
|
|
244
|
+
// Why not just `expires`?
|
|
245
|
+
// Ensures the correct sort order:
|
|
246
|
+
// 'expired' --> 'expiring soon' --> 'never expires'
|
|
247
|
+
return this.expires !== undefined ? this.expires : Number.MAX_SAFE_INTEGER;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get expires() {
|
|
251
|
+
const expires = this.annotations[CLOUD_CREDENTIALS.EXPIRATION];
|
|
252
|
+
|
|
253
|
+
if (typeof expires === 'string') {
|
|
254
|
+
return parseInt(expires);
|
|
255
|
+
} else if (typeof expires === 'number') {
|
|
256
|
+
return expires;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return undefined; // Weird things happen if this isn't a number
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
get expireData() {
|
|
263
|
+
if (typeof this.expiresIn !== 'number') {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const sevenDays = 1000 * 60 * 60 * 24 * 7;
|
|
268
|
+
|
|
269
|
+
if (this.expiresIn === 0) {
|
|
270
|
+
return {
|
|
271
|
+
expired: true,
|
|
272
|
+
expiring: false,
|
|
273
|
+
};
|
|
274
|
+
} else if (this.expiresIn < sevenDays) {
|
|
275
|
+
return {
|
|
276
|
+
expired: false,
|
|
277
|
+
expiring: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
get expiresString() {
|
|
285
|
+
if (this.expires === undefined) {
|
|
286
|
+
return '';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.expireData.expired) {
|
|
290
|
+
return this.t('manager.cloudCredentials.expired');
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const dateFormat = escapeHtml( this.$rootGetters['prefs/get'](DATE_FORMAT));
|
|
294
|
+
const timeFormat = escapeHtml( this.$rootGetters['prefs/get'](TIME_FORMAT));
|
|
295
|
+
|
|
296
|
+
return day(this.expires).format(`${ dateFormat } ${ timeFormat }`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
get expiresIn() {
|
|
300
|
+
if (this.expires === undefined) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const timeThen = this.expires;
|
|
305
|
+
const timeNow = Date.now();
|
|
306
|
+
|
|
307
|
+
const expiresIn = timeThen - timeNow;
|
|
308
|
+
|
|
309
|
+
return expiresIn < 0 ? 0 : expiresIn;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
renew() {
|
|
313
|
+
const renewFn = renew[this.provider]?.renew;
|
|
314
|
+
|
|
315
|
+
if (!renewFn) {
|
|
316
|
+
console.error('No fn renew function for ', this.provider); // eslint-disable-line no-console
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return renewFn({
|
|
320
|
+
cloudCredential: this,
|
|
321
|
+
$ctx: this.$ctx
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async renewBulk(cloudCredentials = []) {
|
|
326
|
+
const renewBulkFn = renew[this.provider]?.renewBulk;
|
|
327
|
+
|
|
328
|
+
if (!renewBulkFn) {
|
|
329
|
+
console.error('No fn renew bulk function for ', this.provider); // eslint-disable-line no-console
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return renewBulkFn({
|
|
333
|
+
cloudCredentials,
|
|
334
|
+
$ctx: this.$ctx
|
|
335
|
+
});
|
|
336
|
+
}
|
|
180
337
|
}
|
|
@@ -308,20 +308,11 @@ export default class GitRepo extends SteveModel {
|
|
|
308
308
|
bundle.namespacedName.startsWith(`${ this.namespace }:${ this.name }`));
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Bundles with state of active
|
|
313
|
+
*/
|
|
311
314
|
get bundlesReady() {
|
|
312
|
-
|
|
313
|
-
return this.bundles.filter((bundle) => bundle.state === 'active');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return 0;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
get targetClustersReady() {
|
|
320
|
-
if (this.targetClusters && this.targetClusters.length) {
|
|
321
|
-
return this.targetClusters.filter((cluster) => cluster.state === 'active');
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return 0;
|
|
315
|
+
return this.bundles?.filter((bundle) => bundle.state === 'active');
|
|
325
316
|
}
|
|
326
317
|
|
|
327
318
|
get bundleDeployments() {
|
|
@@ -10,12 +10,14 @@ import { addParams } from '@shell/utils/url';
|
|
|
10
10
|
import { isEmpty } from '@shell/utils/object';
|
|
11
11
|
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
|
12
12
|
import { isHarvesterCluster } from '@shell/utils/cluster';
|
|
13
|
-
import
|
|
13
|
+
import SteveModel from '@shell/plugins/steve/steve-class';
|
|
14
14
|
import { LINUX, WINDOWS } from '@shell/store/catalog';
|
|
15
15
|
import { KONTAINER_TO_DRIVER } from './management.cattle.io.kontainerdriver';
|
|
16
16
|
import { PINNED_CLUSTERS } from '@shell/store/prefs';
|
|
17
17
|
import { copyTextToClipboard } from '@shell/utils/clipboard';
|
|
18
18
|
|
|
19
|
+
const DEFAULT_BADGE_COLOR = '#707070';
|
|
20
|
+
|
|
19
21
|
// See translation file cluster.providers for list of providers
|
|
20
22
|
// If the logo is not named with the provider name, add an override here
|
|
21
23
|
const PROVIDER_LOGO_OVERRIDE = {};
|
|
@@ -27,7 +29,7 @@ function findRelationship(verb, type, relationships = []) {
|
|
|
27
29
|
return relationships.find((r) => r[from] === type)?.[id];
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
export default class MgmtCluster extends
|
|
32
|
+
export default class MgmtCluster extends SteveModel {
|
|
31
33
|
get details() {
|
|
32
34
|
const out = [
|
|
33
35
|
{
|
|
@@ -306,13 +308,22 @@ export default class MgmtCluster extends HybridModel {
|
|
|
306
308
|
return undefined;
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
|
|
311
|
+
let color = this.metadata?.annotations[CLUSTER_BADGE.COLOR] || DEFAULT_BADGE_COLOR;
|
|
310
312
|
const iconText = this.metadata?.annotations[CLUSTER_BADGE.ICON_TEXT] || '';
|
|
313
|
+
let foregroundColor;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
foregroundColor = textColor(parseColor(color.trim())); // Remove any whitespace
|
|
317
|
+
} catch (_e) {
|
|
318
|
+
// If we could not parse the badge color, use the defaults
|
|
319
|
+
color = DEFAULT_BADGE_COLOR;
|
|
320
|
+
foregroundColor = textColor(parseColor(color));
|
|
321
|
+
}
|
|
311
322
|
|
|
312
323
|
return {
|
|
313
324
|
text: comment || undefined,
|
|
314
325
|
color,
|
|
315
|
-
textColor:
|
|
326
|
+
textColor: foregroundColor,
|
|
316
327
|
iconText: iconText.substr(0, 3)
|
|
317
328
|
};
|
|
318
329
|
}
|
|
@@ -105,7 +105,7 @@ export default class User extends HybridModel {
|
|
|
105
105
|
* @returns {number}
|
|
106
106
|
*/
|
|
107
107
|
get userLastLogin() {
|
|
108
|
-
return this.metadata?.labels?.['cattle.io/last-login'] * 1000;
|
|
108
|
+
return this.metadata?.labels?.['cattle.io/last-login'] * 1000 || 0;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -113,7 +113,7 @@ export default class User extends HybridModel {
|
|
|
113
113
|
* @returns {number}
|
|
114
114
|
*/
|
|
115
115
|
get userDisabledIn() {
|
|
116
|
-
return this.metadata?.labels?.['cattle.io/disable-after'] * 1000;
|
|
116
|
+
return this.metadata?.labels?.['cattle.io/disable-after'] * 1000 || 0;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
@@ -129,7 +129,7 @@ export default class User extends HybridModel {
|
|
|
129
129
|
* @returns {number}
|
|
130
130
|
*/
|
|
131
131
|
get userDeletedIn() {
|
|
132
|
-
return this.metadata?.labels?.['cattle.io/delete-after'] * 1000;
|
|
132
|
+
return this.metadata?.labels?.['cattle.io/delete-after'] * 1000 || 0;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
get state() {
|
package/models/nodedriver.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import Driver from '@shell/models/driver';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Overrides for spec.addCloudCredential
|
|
5
|
+
*/
|
|
6
|
+
export const CLOUD_CREDENTIAL_OVERRIDE = { nutanix: true };
|
|
7
|
+
|
|
3
8
|
export default class NodeDriver extends Driver {
|
|
4
9
|
get doneRoute() {
|
|
5
10
|
return 'c-cluster-manager-driver-nodedriver';
|
|
@@ -10,7 +10,6 @@ import { compare } from '@shell/utils/version';
|
|
|
10
10
|
import { AS, MODE, _VIEW, _YAML } from '@shell/config/query-params';
|
|
11
11
|
import { HARVESTER_NAME as HARVESTER } from '@shell/config/features';
|
|
12
12
|
import { CAPI as CAPI_ANNOTATIONS, NODE_ARCHITECTURE } from '@shell/config/labels-annotations';
|
|
13
|
-
import capitalize from 'lodash/capitalize';
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Class representing Cluster resource.
|
|
@@ -166,6 +165,19 @@ export default class ProvCluster extends SteveModel {
|
|
|
166
165
|
enabled: canSaveRKETemplate,
|
|
167
166
|
}, { divider: true }];
|
|
168
167
|
|
|
168
|
+
// Harvester Cluster 1:1 Harvester Cloud Cred
|
|
169
|
+
if (this.cloudCredential?.canRenew || this.cloudCredential?.canBulkRenew) {
|
|
170
|
+
out.splice(0, 0, { divider: true });
|
|
171
|
+
out.splice(0, 0, {
|
|
172
|
+
action: 'renew',
|
|
173
|
+
enabled: this.cloudCredential?.canRenew,
|
|
174
|
+
bulkable: this.cloudCredential?.canBulkRenew,
|
|
175
|
+
bulkAction: 'renewBulk',
|
|
176
|
+
icon: 'icon icon-fw icon-refresh',
|
|
177
|
+
label: this.$rootGetters['i18n/t']('cluster.cloudCredentials.renew'),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
169
181
|
return actions.concat(out);
|
|
170
182
|
}
|
|
171
183
|
|
|
@@ -411,7 +423,7 @@ export default class ProvCluster extends SteveModel {
|
|
|
411
423
|
if (!node.metadata?.state?.transitioning) {
|
|
412
424
|
const architecture = node.status?.nodeLabels?.[NODE_ARCHITECTURE];
|
|
413
425
|
|
|
414
|
-
const key = architecture
|
|
426
|
+
const key = architecture || this.t('cluster.architecture.label.unknown');
|
|
415
427
|
|
|
416
428
|
obj[key] = (obj[key] || 0) + 1;
|
|
417
429
|
}
|
|
@@ -885,7 +897,8 @@ export default class ProvCluster extends SteveModel {
|
|
|
885
897
|
get agentConfig() {
|
|
886
898
|
// The one we want is the first one with no selector.
|
|
887
899
|
// If there are multiple with no selector, that will fall under the unsupported message below.
|
|
888
|
-
return this.spec.rkeConfig
|
|
900
|
+
return this.spec.rkeConfig?.machineSelectorConfig
|
|
901
|
+
?.find((x) => !x.machineLabelSelector)?.config || { };
|
|
889
902
|
}
|
|
890
903
|
|
|
891
904
|
get cloudProvider() {
|
|
@@ -986,4 +999,29 @@ export default class ProvCluster extends SteveModel {
|
|
|
986
999
|
'spec.rkeConfig.machinePools.dynamicSchemaSpec',
|
|
987
1000
|
];
|
|
988
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
get description() {
|
|
1004
|
+
return super.description || this.mgmt?.description;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
renew() {
|
|
1008
|
+
return this.cloudCredential?.renew();
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
renewBulk(clusters = []) {
|
|
1012
|
+
// In theory we don't need to filter by cloudCred, but do so for safety
|
|
1013
|
+
const cloudCredentials = clusters.filter((c) => c.cloudCredential).map((c) => c.cloudCredential);
|
|
1014
|
+
|
|
1015
|
+
return this.cloudCredential?.renewBulk(cloudCredentials);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
get cloudCredential() {
|
|
1019
|
+
return this.$rootGetters['rancher/all'](NORMAN.CLOUD_CREDENTIAL).find((cc) => cc.id === this.spec.cloudCredentialSecretName);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
get cloudCredentialWarning() {
|
|
1023
|
+
const expireData = this.cloudCredential?.expireData;
|
|
1024
|
+
|
|
1025
|
+
return expireData?.expired || expireData?.expiring;
|
|
1026
|
+
}
|
|
989
1027
|
}
|
package/package.json
CHANGED
package/pages/404.vue
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import Brand from '@shell/mixins/brand';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
mixins: [Brand],
|
|
6
|
+
beforeMount() {
|
|
7
|
+
this.$store.commit('setError', { error: new Error('404: This page could not be found') });
|
|
8
|
+
this.$router.replace('/fail-whale');
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<template>
|
|
14
|
+
<div class="dashboard-root" />
|
|
15
|
+
</template>
|
package/pages/auth/login.vue
CHANGED
package/pages/auth/setup.vue
CHANGED
|
@@ -302,8 +302,9 @@ export default {
|
|
|
302
302
|
*/
|
|
303
303
|
userValues = diff(this.loadedVersionValues, this.chartValues);
|
|
304
304
|
} else if ( this.existing ) {
|
|
305
|
+
await this.existing.fetchValues(); // In theory this has already been called, but do again to be safe
|
|
305
306
|
/* For an already installed app, use the values from the previous install. */
|
|
306
|
-
userValues = clone(this.existing.
|
|
307
|
+
userValues = clone(this.existing.values || {});
|
|
307
308
|
} else {
|
|
308
309
|
/* For an new app, start empty. */
|
|
309
310
|
userValues = {};
|
|
@@ -163,7 +163,7 @@ describe('page: cluster dashboard', () => {
|
|
|
163
163
|
['created', 'glance.created', []],
|
|
164
164
|
['architecture', 'mixed', [{ labels: { [NODE_ARCHITECTURE]: 'amd64' } }, { labels: { [NODE_ARCHITECTURE]: 'intel' } }]],
|
|
165
165
|
['architecture', 'mixed', [{ labels: { [NODE_ARCHITECTURE]: 'amd64' } }, { labels: { } }]],
|
|
166
|
-
['architecture', '
|
|
166
|
+
['architecture', 'amd64', [{ labels: { [NODE_ARCHITECTURE]: 'amd64' } }]],
|
|
167
167
|
['architecture', 'unknown', [{ labels: { } }]],
|
|
168
168
|
['architecture', '—', [{ metadata: { state: { transitioning: true } } }]],
|
|
169
169
|
])('should show %p label %p', (label, text, nodes) => {
|
|
@@ -47,7 +47,7 @@ import Certificates from '@shell/components/Certificates';
|
|
|
47
47
|
import { NAME as EXPLORER } from '@shell/config/product/explorer';
|
|
48
48
|
import TabTitle from '@shell/components/TabTitle';
|
|
49
49
|
import { STATES_ENUM } from '@shell/plugins/dashboard-store/resource-class';
|
|
50
|
-
import
|
|
50
|
+
import paginationUtils from '@shell/utils/pagination-utils';
|
|
51
51
|
|
|
52
52
|
export const RESOURCES = [NAMESPACE, INGRESS, PV, WORKLOAD_TYPES.DEPLOYMENT, WORKLOAD_TYPES.STATEFUL_SET, WORKLOAD_TYPES.JOB, WORKLOAD_TYPES.DAEMON_SET, SERVICE];
|
|
53
53
|
|
|
@@ -122,6 +122,8 @@ export default {
|
|
|
122
122
|
this.loadAgents();
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
this.showCertificates = !paginationUtils.isSteveCacheEnabled({ rootGetters: this.$store.getters });
|
|
125
127
|
},
|
|
126
128
|
|
|
127
129
|
data() {
|
|
@@ -156,6 +158,7 @@ export default {
|
|
|
156
158
|
clusterCounts,
|
|
157
159
|
selectedTab: 'cluster-events',
|
|
158
160
|
extensionCards: getApplicableExtensionEnhancements(this, ExtensionPoint.CARD, CardLocation.CLUSTER_DASHBOARD_CARD, this.$route),
|
|
161
|
+
showCertificates: false,
|
|
159
162
|
};
|
|
160
163
|
},
|
|
161
164
|
|
|
@@ -212,7 +215,7 @@ export default {
|
|
|
212
215
|
if (!node.metadata?.state?.transitioning) {
|
|
213
216
|
const architecture = node.labels?.[NODE_ARCHITECTURE];
|
|
214
217
|
|
|
215
|
-
const key = architecture
|
|
218
|
+
const key = architecture || this.t('cluster.architecture.label.unknown');
|
|
216
219
|
|
|
217
220
|
obj[key] = (obj[key] || 0) + 1;
|
|
218
221
|
}
|
|
@@ -739,6 +742,7 @@ export default {
|
|
|
739
742
|
<AlertTable v-if="selectedTab === 'cluster-alerts'" />
|
|
740
743
|
</Tab>
|
|
741
744
|
<Tab
|
|
745
|
+
v-if="showCertificates"
|
|
742
746
|
name="cluster-certs"
|
|
743
747
|
:label="t('clusterIndexPage.sections.certs.label')"
|
|
744
748
|
:weight="1"
|
|
@@ -12,6 +12,7 @@ import { WORKSPACE_ANNOTATION } from '@shell/config/labels-annotations';
|
|
|
12
12
|
import { filterBy } from '@shell/utils/array';
|
|
13
13
|
import FleetNoWorkspaces from '@shell/components/fleet/FleetNoWorkspaces.vue';
|
|
14
14
|
import { NAME } from '@shell/config/product/fleet';
|
|
15
|
+
import { xOfy } from '@shell/utils/string';
|
|
15
16
|
|
|
16
17
|
export default {
|
|
17
18
|
name: 'FleetDashboard',
|
|
@@ -175,7 +176,12 @@ export default {
|
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
if (area === 'clusters') {
|
|
178
|
-
|
|
179
|
+
if (row.clusterInfo?.ready === row.clusterInfo?.total && row.clusterInfo?.ready) {
|
|
180
|
+
return {
|
|
181
|
+
badgeClass: STATES[STATES_ENUM.ACTIVE].color,
|
|
182
|
+
icon: STATES[STATES_ENUM.ACTIVE].compoundIcon
|
|
183
|
+
};
|
|
184
|
+
}
|
|
179
185
|
} else if (area === 'bundles') {
|
|
180
186
|
group = row.bundles;
|
|
181
187
|
} else if (area === 'resources') {
|
|
@@ -223,7 +229,7 @@ export default {
|
|
|
223
229
|
}
|
|
224
230
|
|
|
225
231
|
if (area === 'clusters') {
|
|
226
|
-
group =
|
|
232
|
+
group = '';
|
|
227
233
|
} else if (area === 'bundles') {
|
|
228
234
|
group = row.bundles;
|
|
229
235
|
} else if (area === 'resources') {
|
|
@@ -262,11 +268,11 @@ export default {
|
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
if (area === 'clusters') {
|
|
265
|
-
|
|
271
|
+
return `${ row.clusterInfo.ready }/${ row.clusterInfo.total }`;
|
|
266
272
|
} else if (area === 'bundles') {
|
|
267
|
-
value =
|
|
273
|
+
value = xOfy(row.bundlesReady?.length, row.bundles?.length);
|
|
268
274
|
} else if (area === 'resources') {
|
|
269
|
-
value =
|
|
275
|
+
value = xOfy(row.status?.resourceCounts?.ready, row.status?.resourceCounts?.desiredReady);
|
|
270
276
|
}
|
|
271
277
|
|
|
272
278
|
return value;
|