@rancher/shell 3.0.9-rc.2 → 3.0.9-rc.4
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 +24 -2
- package/assets/translations/zh-hans.yaml +13 -0
- package/components/ActionMenu.vue +7 -8
- package/components/ActionMenuShell.vue +19 -20
- package/components/Resource/Detail/Card/Scaler.vue +10 -2
- package/components/Resource/Detail/Card/StatusCard/index.vue +4 -1
- package/components/ResourceTable.vue +1 -1
- package/components/Tabbed/Tab.vue +4 -0
- package/components/Tabbed/index.vue +11 -3
- package/components/__tests__/ProjectRow.test.ts +102 -15
- package/components/form/ResourceQuota/Project.vue +59 -8
- package/components/form/ResourceQuota/ProjectRow.vue +116 -21
- package/components/form/ResourceQuota/shared.js +42 -18
- package/components/formatter/KubeconfigClusters.vue +74 -0
- package/components/formatter/LinkName.vue +3 -2
- package/components/formatter/__tests__/KubeconfigClusters.test.ts +125 -0
- package/config/product/explorer.js +1 -1
- package/config/product/manager.js +29 -2
- package/config/router/routes.js +4 -1
- package/config/table-headers.js +9 -7
- package/config/types.js +4 -1
- package/detail/management.cattle.io.oidcclient.vue +15 -4
- package/edit/__tests__/management.cattle.io.project.test.js +137 -0
- package/edit/management.cattle.io.project.vue +36 -6
- package/edit/monitoring.coreos.com.alertmanagerconfig/index.vue +16 -3
- package/edit/provisioning.cattle.io.cluster/defaults.ts +1 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +2 -1
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/S3Config.vue +57 -2
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/__tests__/S3Config.test.ts +109 -0
- package/edit/provisioning.cattle.io.cluster/tabs/etcd/index.vue +1 -0
- package/initialize/install-plugins.js +0 -2
- package/list/ext.cattle.io.kubeconfig.vue +118 -0
- package/mixins/__tests__/chart.test.ts +147 -0
- package/mixins/chart.js +10 -8
- package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +364 -0
- package/models/__tests__/secret.test.ts +55 -0
- package/models/ext.cattle.io.kubeconfig.ts +97 -0
- package/models/management.cattle.io.cluster.js +22 -30
- package/models/provisioning.cattle.io.cluster.js +2 -2
- package/models/secret.js +1 -1
- package/package.json +2 -2
- package/pages/__tests__/diagnostic.test.ts +71 -0
- package/pages/about.vue +3 -2
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +38 -14
- package/pages/c/_cluster/apps/charts/index.vue +1 -0
- package/pages/c/_cluster/explorer/tools/index.vue +23 -5
- package/pages/c/_cluster/monitoring/alertmanagerconfig/_alertmanagerconfigid/receiver.vue +18 -5
- package/pages/c/_cluster/uiplugins/index.vue +40 -8
- package/pages/diagnostic.vue +17 -3
- package/rancher-components/Pill/RcCounterBadge/RcCounterBadge.vue +1 -0
- package/rancher-components/RcItemCard/RcItemCard.test.ts +20 -8
- package/rancher-components/RcItemCard/RcItemCard.vue +38 -31
- package/store/__tests__/auth.test.ts +21 -5
- package/store/auth.js +6 -3
- package/types/shell/index.d.ts +177 -157
- package/utils/__tests__/chart.test.ts +96 -0
- package/utils/__tests__/version.test.ts +1 -19
- package/utils/chart.js +64 -0
- package/utils/version.js +5 -17
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import { useStore } from 'vuex';
|
|
4
|
+
import PaginatedResourceTable from '@shell/components/PaginatedResourceTable.vue';
|
|
5
|
+
import { CAPI, MANAGEMENT } from '@shell/config/types';
|
|
6
|
+
import { ActionFindPageArgs } from '@shell/types/store/dashboard-store.types';
|
|
7
|
+
import { FilterArgs, PaginationFilterField, PaginationParamFilter } from '@shell/types/store/pagination.types';
|
|
8
|
+
import { PagTableFetchPageSecondaryResourcesOpts, PagTableFetchSecondaryResourcesOpts, PagTableFetchSecondaryResourcesReturns } from '@shell/types/components/paginatedResourceTable';
|
|
9
|
+
|
|
10
|
+
defineProps({
|
|
11
|
+
schema: {
|
|
12
|
+
type: Object,
|
|
13
|
+
required: true
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
useQueryParamsForSimpleFiltering: {
|
|
17
|
+
type: Boolean,
|
|
18
|
+
default: false
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const store = useStore();
|
|
23
|
+
|
|
24
|
+
const canViewProvClusters = computed<boolean>(() => {
|
|
25
|
+
return !!store.getters['management/canList'](CAPI.RANCHER_CLUSTER);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const canViewMgmtClusters = computed<boolean>(() => {
|
|
29
|
+
return !!store.getters['management/canList'](MANAGEMENT.CLUSTER);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fetch all clusters when not using pagination
|
|
34
|
+
*/
|
|
35
|
+
async function fetchSecondaryResources({ canPaginate }: PagTableFetchSecondaryResourcesOpts): PagTableFetchSecondaryResourcesReturns {
|
|
36
|
+
if (canPaginate) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const promises = [];
|
|
41
|
+
|
|
42
|
+
if (canViewProvClusters.value) {
|
|
43
|
+
promises.push(store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (canViewMgmtClusters.value) {
|
|
47
|
+
promises.push(store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await Promise.all(promises);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetch only the clusters referenced by kubeconfigs on the current page
|
|
55
|
+
*
|
|
56
|
+
* NOTE: For the time being this isn't validated because ext.cattle.io.kubeconfig is not one of the indexed resources. I'm putting this in for future support since secondary resources are needed.
|
|
57
|
+
*/
|
|
58
|
+
async function fetchPageSecondaryResources({ force, page }: PagTableFetchPageSecondaryResourcesOpts) {
|
|
59
|
+
if (!page?.length) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const uniqueClusterIds = new Set<string>();
|
|
64
|
+
|
|
65
|
+
page.forEach((kubeconfig: any) => {
|
|
66
|
+
const ids = kubeconfig.spec?.clusters || [];
|
|
67
|
+
|
|
68
|
+
ids.forEach((id: string) => uniqueClusterIds.add(id));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (uniqueClusterIds.size === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const clusterIdArray = Array.from(uniqueClusterIds);
|
|
76
|
+
|
|
77
|
+
if (canViewProvClusters.value) {
|
|
78
|
+
const opt: ActionFindPageArgs = {
|
|
79
|
+
force,
|
|
80
|
+
pagination: new FilterArgs({
|
|
81
|
+
filters: PaginationParamFilter.createMultipleFields(
|
|
82
|
+
clusterIdArray.map((id) => new PaginationFilterField({
|
|
83
|
+
field: 'status.clusterName', // Verified it's one of the attribute fields listed in the schema, according to steve-pagination-utils that means it should be filterable
|
|
84
|
+
value: id
|
|
85
|
+
}))
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
store.dispatch('management/findPage', { type: CAPI.RANCHER_CLUSTER, opt });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (canViewMgmtClusters.value) {
|
|
94
|
+
const opt: ActionFindPageArgs = {
|
|
95
|
+
force,
|
|
96
|
+
pagination: new FilterArgs({
|
|
97
|
+
filters: PaginationParamFilter.createMultipleFields(
|
|
98
|
+
clusterIdArray.map((id) => new PaginationFilterField({
|
|
99
|
+
field: 'metadata.name', // Verified it's one of the attribute fields listed in the schema, according to steve-pagination-utils that means it should be filterable
|
|
100
|
+
value: id
|
|
101
|
+
}))
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
store.dispatch('management/findPage', { type: MANAGEMENT.CLUSTER, opt });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<template>
|
|
112
|
+
<PaginatedResourceTable
|
|
113
|
+
:schema="schema"
|
|
114
|
+
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
|
115
|
+
:fetch-secondary-resources="fetchSecondaryResources"
|
|
116
|
+
:fetch-page-secondary-resources="fetchPageSecondaryResources"
|
|
117
|
+
/>
|
|
118
|
+
</template>
|
|
@@ -322,5 +322,152 @@ describe('chartMixin', () => {
|
|
|
322
322
|
icon: 'icon-upgrade-alt',
|
|
323
323
|
});
|
|
324
324
|
});
|
|
325
|
+
|
|
326
|
+
it('should return "upgrade" action when upgrading from a pre-release to a stable version with "up" build metadata', () => {
|
|
327
|
+
const wrapper = mount(DummyComponent, {
|
|
328
|
+
data: () => ({
|
|
329
|
+
existing: { spec: { chart: { metadata: { version: '108.0.0+up0.25.0-rc.4' } } } },
|
|
330
|
+
version: { version: '108.0.0+up0.25.0' }
|
|
331
|
+
}),
|
|
332
|
+
global: {
|
|
333
|
+
mocks: {
|
|
334
|
+
$store: mockStore,
|
|
335
|
+
$route: { query: {} }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(wrapper.vm.action).toStrictEqual({
|
|
341
|
+
name: 'upgrade',
|
|
342
|
+
tKey: 'upgrade',
|
|
343
|
+
icon: 'icon-upgrade-alt',
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should return "upgrade" action when upgrading with build metadata change', () => {
|
|
348
|
+
const wrapper = mount(DummyComponent, {
|
|
349
|
+
data: () => ({
|
|
350
|
+
existing: { spec: { chart: { metadata: { version: '1.0.0+1' } } } },
|
|
351
|
+
version: { version: '1.0.0+2' }
|
|
352
|
+
}),
|
|
353
|
+
global: {
|
|
354
|
+
mocks: {
|
|
355
|
+
$store: mockStore,
|
|
356
|
+
$route: { query: {} }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(wrapper.vm.action).toStrictEqual({
|
|
362
|
+
name: 'upgrade',
|
|
363
|
+
tKey: 'upgrade',
|
|
364
|
+
icon: 'icon-upgrade-alt',
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('mappedVersions', () => {
|
|
370
|
+
it('should return versions sorted by semver (descending)', () => {
|
|
371
|
+
const versions = [
|
|
372
|
+
{ version: '0.1.0', created: '2026-01-01' },
|
|
373
|
+
{ version: '0.2.0-rc1', created: '2026-01-01' },
|
|
374
|
+
{ version: '0.2.0', created: '2026-01-01' },
|
|
375
|
+
{ version: '1.2.3', created: '2026-01-01' },
|
|
376
|
+
{ version: '1.2.3-dev', created: '2026-01-01' },
|
|
377
|
+
{ version: '10.0.0', created: '2026-01-01' },
|
|
378
|
+
{ version: '2.0.0', created: '2026-01-01' },
|
|
379
|
+
{ version: '2.0.0-rc2', created: '2026-01-01' },
|
|
380
|
+
{ version: '2.0.0-rc1', created: '2026-01-01' },
|
|
381
|
+
{ version: '2.0.0-beta.1', created: '2026-01-01' },
|
|
382
|
+
{ version: '2.0.0-alpha', created: '2026-01-01' },
|
|
383
|
+
{ version: '3.0.0-rc.3', created: '2026-01-01' },
|
|
384
|
+
{ version: '3.0.0-rc.2', created: '2026-01-01' },
|
|
385
|
+
{ version: '3.0.0-rc.10', created: '2026-01-01' },
|
|
386
|
+
{ version: '108.0.0+up0.25.0-rc.4', created: '2026-01-01' },
|
|
387
|
+
{ version: '108.0.0+up0.25.0', created: '2026-01-01' },
|
|
388
|
+
{ version: '1.0.0-alpha.beta', created: '2026-01-01' },
|
|
389
|
+
{ version: '1.0.0-alpha.1', created: '2026-01-01' },
|
|
390
|
+
{ version: '1.0.0-alpha.2', created: '2026-01-01' },
|
|
391
|
+
{ version: '1.0.0-alpha', created: '2026-01-01' },
|
|
392
|
+
{ version: '1.0.0-beta.11', created: '2026-01-01' },
|
|
393
|
+
{ version: '1.0.0-beta.2', created: '2026-01-01' },
|
|
394
|
+
{ version: '1.0.0-beta', created: '2026-01-01' },
|
|
395
|
+
{ version: '1.0.0+build.1', created: '2026-01-01' },
|
|
396
|
+
{ version: '1.0.0+build.2', created: '2026-01-01' },
|
|
397
|
+
{ version: '1.0.0+up1.0.0', created: '2026-01-01' },
|
|
398
|
+
{ version: '1.0.0+upFoo', created: '2026-01-01' },
|
|
399
|
+
{ version: '108.0.0+up0.25.0-rc.5', created: '2026-01-01' },
|
|
400
|
+
{ version: '108.0.0+up0.25.1', created: '2026-01-01' },
|
|
401
|
+
{ version: '0.0.1', created: '2026-01-01' }
|
|
402
|
+
];
|
|
403
|
+
|
|
404
|
+
const mockStore = {
|
|
405
|
+
dispatch: jest.fn(() => Promise.resolve()),
|
|
406
|
+
getters: {
|
|
407
|
+
currentCluster: () => {},
|
|
408
|
+
isRancher: () => true,
|
|
409
|
+
'catalog/repo': () => () => 'repo',
|
|
410
|
+
'catalog/chart': () => ({ versions }),
|
|
411
|
+
'prefs/get': () => (key: string) => true,
|
|
412
|
+
'i18n/t': () => jest.fn()
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const DummyComponent = {
|
|
417
|
+
mixins: [ChartMixin],
|
|
418
|
+
template: '<div></div>',
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const wrapper = mount(
|
|
422
|
+
DummyComponent,
|
|
423
|
+
{
|
|
424
|
+
data() {
|
|
425
|
+
return { chart: { versions } };
|
|
426
|
+
},
|
|
427
|
+
global: {
|
|
428
|
+
mocks: {
|
|
429
|
+
$store: mockStore,
|
|
430
|
+
$route: { query: { version: '10.0.0' } }
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// mappedVersions is a computed property, so we access it directly
|
|
436
|
+
const result = wrapper.vm.mappedVersions;
|
|
437
|
+
const resultVersions = result.map((v: any) => v.version);
|
|
438
|
+
|
|
439
|
+
expect(resultVersions).toStrictEqual([
|
|
440
|
+
'108.0.0+up0.25.1',
|
|
441
|
+
'108.0.0+up0.25.0',
|
|
442
|
+
'108.0.0+up0.25.0-rc.5',
|
|
443
|
+
'108.0.0+up0.25.0-rc.4',
|
|
444
|
+
'10.0.0',
|
|
445
|
+
'3.0.0-rc.10',
|
|
446
|
+
'3.0.0-rc.3',
|
|
447
|
+
'3.0.0-rc.2',
|
|
448
|
+
'2.0.0',
|
|
449
|
+
'2.0.0-rc2',
|
|
450
|
+
'2.0.0-rc1',
|
|
451
|
+
'2.0.0-beta.1',
|
|
452
|
+
'2.0.0-alpha',
|
|
453
|
+
'1.2.3',
|
|
454
|
+
'1.2.3-dev',
|
|
455
|
+
'1.0.0+up1.0.0',
|
|
456
|
+
'1.0.0+upFoo',
|
|
457
|
+
'1.0.0+build.2',
|
|
458
|
+
'1.0.0+build.1',
|
|
459
|
+
'1.0.0-beta.11',
|
|
460
|
+
'1.0.0-beta.2',
|
|
461
|
+
'1.0.0-beta',
|
|
462
|
+
'1.0.0-alpha.beta',
|
|
463
|
+
'1.0.0-alpha.2',
|
|
464
|
+
'1.0.0-alpha.1',
|
|
465
|
+
'1.0.0-alpha',
|
|
466
|
+
'0.2.0',
|
|
467
|
+
'0.2.0-rc1',
|
|
468
|
+
'0.1.0',
|
|
469
|
+
'0.0.1'
|
|
470
|
+
]);
|
|
471
|
+
});
|
|
325
472
|
});
|
|
326
473
|
});
|
package/mixins/chart.js
CHANGED
|
@@ -10,7 +10,8 @@ import { NAME as MANAGER } from '@shell/config/product/manager';
|
|
|
10
10
|
import { OPA_GATE_KEEPER_ID } from '@shell/pages/c/_cluster/gatekeeper/index.vue';
|
|
11
11
|
import { formatSi, parseSi } from '@shell/utils/units';
|
|
12
12
|
import { CAPI, CATALOG } from '@shell/config/types';
|
|
13
|
-
import { isPrerelease
|
|
13
|
+
import { isPrerelease } from '@shell/utils/version';
|
|
14
|
+
import { compareChartVersions } from '@shell/utils/chart';
|
|
14
15
|
import difference from 'lodash/difference';
|
|
15
16
|
import { LINUX, APP_UPGRADE_STATUS } from '@shell/store/catalog';
|
|
16
17
|
import { clone } from '@shell/utils/object';
|
|
@@ -51,7 +52,12 @@ export default {
|
|
|
51
52
|
},
|
|
52
53
|
|
|
53
54
|
mappedVersions() {
|
|
54
|
-
const versions = this.chart?.versions || [];
|
|
55
|
+
const versions = (this.chart?.versions || []).slice();
|
|
56
|
+
|
|
57
|
+
versions.sort((a, b) => {
|
|
58
|
+
return compareChartVersions(b.version, a.version);
|
|
59
|
+
});
|
|
60
|
+
|
|
55
61
|
const selectedVersion = this.targetVersion;
|
|
56
62
|
const OSs = this.currentCluster?.workerOSs;
|
|
57
63
|
const out = [];
|
|
@@ -240,13 +246,9 @@ export default {
|
|
|
240
246
|
};
|
|
241
247
|
}
|
|
242
248
|
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
name: 'upgrade', tKey: 'upgrade', icon: 'icon-upgrade-alt'
|
|
246
|
-
};
|
|
247
|
-
}
|
|
249
|
+
const diff = compareChartVersions(this.currentVersion, this.targetVersion);
|
|
248
250
|
|
|
249
|
-
if (
|
|
251
|
+
if (diff < 0) {
|
|
250
252
|
return {
|
|
251
253
|
name: 'upgrade', tKey: 'upgrade', icon: 'icon-upgrade-alt'
|
|
252
254
|
};
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import Kubeconfig from '@shell/models/ext.cattle.io.kubeconfig';
|
|
2
|
+
import { CAPI, MANAGEMENT } from '@shell/config/types';
|
|
3
|
+
|
|
4
|
+
// SteveModel is JS, so we need to type the constructor
|
|
5
|
+
const KubeconfigModel = Kubeconfig as unknown as new (data: object) => Kubeconfig;
|
|
6
|
+
|
|
7
|
+
describe('class Kubeconfig', () => {
|
|
8
|
+
const mockT = jest.fn((key: string, opts?: { name: string }) => {
|
|
9
|
+
if (key === '"ext.cattle.io.kubeconfig".deleted') {
|
|
10
|
+
return `${ opts?.name } (deleted)`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return key;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const createKubeconfig = (data: object, rootGetters: object = {}) => {
|
|
17
|
+
const kubeconfig = new KubeconfigModel(data);
|
|
18
|
+
|
|
19
|
+
// Mock $rootGetters before any getters are accessed
|
|
20
|
+
// Cast to any since $rootGetters is inherited from JS SteveModel
|
|
21
|
+
jest.spyOn(kubeconfig as any, '$rootGetters', 'get').mockReturnValue({
|
|
22
|
+
'i18n/t': mockT,
|
|
23
|
+
'management/all': () => [],
|
|
24
|
+
...rootGetters
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return kubeconfig;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('expiresAt', () => {
|
|
35
|
+
it('should return null when ttl is not set', () => {
|
|
36
|
+
const kubeconfig = createKubeconfig({
|
|
37
|
+
metadata: { creationTimestamp: '2024-01-01T00:00:00Z' },
|
|
38
|
+
spec: {}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
expect(kubeconfig.expiresAt).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should return null when creationTimestamp is not set', () => {
|
|
45
|
+
const kubeconfig = createKubeconfig({
|
|
46
|
+
metadata: {},
|
|
47
|
+
spec: { ttl: 3600 }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(kubeconfig.expiresAt).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should calculate expiry correctly', () => {
|
|
54
|
+
const kubeconfig = createKubeconfig({
|
|
55
|
+
metadata: { creationTimestamp: '2024-01-01T00:00:00Z' },
|
|
56
|
+
spec: { ttl: 3600 } // 1 hour in seconds
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(kubeconfig.expiresAt).toBe('2024-01-01T01:00:00.000Z');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle large ttl values', () => {
|
|
63
|
+
const kubeconfig = createKubeconfig({
|
|
64
|
+
metadata: { creationTimestamp: '2024-01-01T00:00:00Z' },
|
|
65
|
+
spec: { ttl: 86400 } // 24 hours in seconds
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(kubeconfig.expiresAt).toBe('2024-01-02T00:00:00.000Z');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('referencedClusters', () => {
|
|
73
|
+
const mockProvCluster = {
|
|
74
|
+
mgmt: { id: 'c-m-abc123' },
|
|
75
|
+
status: { clusterName: 'c-m-abc123' },
|
|
76
|
+
nameDisplay: 'my-cluster',
|
|
77
|
+
detailLocation: { name: 'c-cluster-product-resource-id', params: { cluster: 'my-cluster' } }
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const mockMgmtCluster = {
|
|
81
|
+
id: 'c-m-def456',
|
|
82
|
+
nameDisplay: 'mgmt-cluster',
|
|
83
|
+
detailLocation: { name: 'c-cluster-product-resource-id', params: { cluster: 'mgmt-cluster' } }
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
it('should return empty array when no clusters are specified', () => {
|
|
87
|
+
const kubeconfig = createKubeconfig({
|
|
88
|
+
metadata: {},
|
|
89
|
+
spec: {}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(kubeconfig.referencedClusters).toStrictEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should map provisioning cluster by mgmt id', () => {
|
|
96
|
+
const kubeconfig = createKubeconfig(
|
|
97
|
+
{
|
|
98
|
+
metadata: {},
|
|
99
|
+
spec: { clusters: ['c-m-abc123'] }
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
'management/all': (type: string) => {
|
|
103
|
+
if (type === CAPI.RANCHER_CLUSTER) {
|
|
104
|
+
return [mockProvCluster];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(kubeconfig.referencedClusters).toStrictEqual([
|
|
113
|
+
{
|
|
114
|
+
label: 'my-cluster',
|
|
115
|
+
location: mockProvCluster.detailLocation
|
|
116
|
+
}
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should map management cluster when no provisioning cluster found', () => {
|
|
121
|
+
const kubeconfig = createKubeconfig(
|
|
122
|
+
{
|
|
123
|
+
metadata: {},
|
|
124
|
+
spec: { clusters: ['c-m-def456'] }
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
'management/all': (type: string) => {
|
|
128
|
+
if (type === MANAGEMENT.CLUSTER) {
|
|
129
|
+
return [mockMgmtCluster];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(kubeconfig.referencedClusters).toStrictEqual([
|
|
138
|
+
{
|
|
139
|
+
label: 'mgmt-cluster',
|
|
140
|
+
location: mockMgmtCluster.detailLocation
|
|
141
|
+
}
|
|
142
|
+
]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return deleted label when cluster not found', () => {
|
|
146
|
+
const kubeconfig = createKubeconfig({
|
|
147
|
+
metadata: {},
|
|
148
|
+
spec: { clusters: ['c-m-deleted'] }
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(kubeconfig.referencedClusters).toStrictEqual([
|
|
152
|
+
{
|
|
153
|
+
label: 'c-m-deleted (deleted)',
|
|
154
|
+
location: null
|
|
155
|
+
}
|
|
156
|
+
]);
|
|
157
|
+
expect(mockT).toHaveBeenCalledWith('"ext.cattle.io.kubeconfig".deleted', { name: 'c-m-deleted' });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should prefer provisioning cluster over management cluster', () => {
|
|
161
|
+
const mgmtClusterSameId = {
|
|
162
|
+
id: 'c-m-abc123',
|
|
163
|
+
nameDisplay: 'mgmt-version',
|
|
164
|
+
detailLocation: { name: 'mgmt-location' }
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const kubeconfig = createKubeconfig(
|
|
168
|
+
{
|
|
169
|
+
metadata: {},
|
|
170
|
+
spec: { clusters: ['c-m-abc123'] }
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
'management/all': (type: string) => {
|
|
174
|
+
if (type === CAPI.RANCHER_CLUSTER) {
|
|
175
|
+
return [mockProvCluster];
|
|
176
|
+
}
|
|
177
|
+
if (type === MANAGEMENT.CLUSTER) {
|
|
178
|
+
return [mgmtClusterSameId];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(kubeconfig.referencedClusters).toStrictEqual([
|
|
187
|
+
{
|
|
188
|
+
label: 'my-cluster',
|
|
189
|
+
location: mockProvCluster.detailLocation
|
|
190
|
+
}
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('sortedReferencedClusters', () => {
|
|
196
|
+
it('should sort existing clusters before deleted clusters', () => {
|
|
197
|
+
const existingCluster = {
|
|
198
|
+
mgmt: { id: 'c-m-exists' },
|
|
199
|
+
nameDisplay: 'existing-cluster',
|
|
200
|
+
detailLocation: { name: 'location' }
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const kubeconfig = createKubeconfig(
|
|
204
|
+
{
|
|
205
|
+
metadata: {},
|
|
206
|
+
spec: { clusters: ['deleted-1', 'c-m-exists', 'deleted-2'] }
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
'management/all': (type: string) => {
|
|
210
|
+
if (type === CAPI.RANCHER_CLUSTER) {
|
|
211
|
+
return [existingCluster];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const sorted = kubeconfig.sortedReferencedClusters;
|
|
220
|
+
|
|
221
|
+
expect(sorted[0].label).toBe('existing-cluster');
|
|
222
|
+
expect(sorted[0].location).not.toBeNull();
|
|
223
|
+
expect(sorted[1].location).toBeNull();
|
|
224
|
+
expect(sorted[2].location).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should sort existing clusters alphabetically', () => {
|
|
228
|
+
const clusters = [
|
|
229
|
+
{
|
|
230
|
+
mgmt: { id: 'c-m-zebra' }, nameDisplay: 'zebra', detailLocation: { name: 'z' }
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
mgmt: { id: 'c-m-alpha' }, nameDisplay: 'alpha', detailLocation: { name: 'a' }
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
mgmt: { id: 'c-m-beta' }, nameDisplay: 'beta', detailLocation: { name: 'b' }
|
|
237
|
+
}
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
const kubeconfig = createKubeconfig(
|
|
241
|
+
{
|
|
242
|
+
metadata: {},
|
|
243
|
+
spec: { clusters: ['c-m-zebra', 'c-m-alpha', 'c-m-beta'] }
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
'management/all': (type: string) => {
|
|
247
|
+
if (type === CAPI.RANCHER_CLUSTER) {
|
|
248
|
+
return clusters;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const sorted = kubeconfig.sortedReferencedClusters;
|
|
257
|
+
|
|
258
|
+
expect(sorted.map((c) => c.label)).toStrictEqual(['alpha', 'beta', 'zebra']);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should sort numerically when names contain numbers', () => {
|
|
262
|
+
const clusters = [
|
|
263
|
+
{
|
|
264
|
+
mgmt: { id: 'c-m-2' }, nameDisplay: 'cluster2', detailLocation: { name: 'c2' }
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
mgmt: { id: 'c-m-10' }, nameDisplay: 'cluster10', detailLocation: { name: 'c10' }
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
mgmt: { id: 'c-m-1' }, nameDisplay: 'cluster1', detailLocation: { name: 'c1' }
|
|
271
|
+
}
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
const kubeconfig = createKubeconfig(
|
|
275
|
+
{
|
|
276
|
+
metadata: {},
|
|
277
|
+
spec: { clusters: ['c-m-2', 'c-m-10', 'c-m-1'] }
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
'management/all': (type: string) => {
|
|
281
|
+
if (type === CAPI.RANCHER_CLUSTER) {
|
|
282
|
+
return clusters;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const sorted = kubeconfig.sortedReferencedClusters;
|
|
291
|
+
|
|
292
|
+
expect(sorted.map((c) => c.label)).toStrictEqual(['cluster1', 'cluster2', 'cluster10']);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('referencedClustersSortable', () => {
|
|
297
|
+
it('should return comma-separated lowercase labels', () => {
|
|
298
|
+
const clusters = [
|
|
299
|
+
{
|
|
300
|
+
mgmt: { id: 'c-m-1' }, nameDisplay: 'Alpha', detailLocation: { name: 'a' }
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
mgmt: { id: 'c-m-2' }, nameDisplay: 'Beta', detailLocation: { name: 'b' }
|
|
304
|
+
}
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
const kubeconfig = createKubeconfig(
|
|
308
|
+
{
|
|
309
|
+
metadata: {},
|
|
310
|
+
spec: { clusters: ['c-m-1', 'c-m-2'] }
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
'management/all': (type: string) => {
|
|
314
|
+
if (type === CAPI.RANCHER_CLUSTER) {
|
|
315
|
+
return clusters;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(kubeconfig.referencedClustersSortable).toBe('alpha,beta');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should return empty string when no clusters', () => {
|
|
327
|
+
const kubeconfig = createKubeconfig({
|
|
328
|
+
metadata: {},
|
|
329
|
+
spec: {}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(kubeconfig.referencedClustersSortable).toBe('');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('_availableActions', () => {
|
|
337
|
+
it('should filter out goToEdit, goToEditYaml, cloneYaml, and download actions', () => {
|
|
338
|
+
const kubeconfig = createKubeconfig({
|
|
339
|
+
metadata: {},
|
|
340
|
+
spec: {}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const mockActions = [
|
|
344
|
+
{ action: 'goToClone' },
|
|
345
|
+
{ action: 'divider' },
|
|
346
|
+
{ action: 'goToEdit' },
|
|
347
|
+
{ action: 'goToEditYaml' },
|
|
348
|
+
{ action: 'cloneYaml' },
|
|
349
|
+
{ action: 'download' },
|
|
350
|
+
{ action: 'promptRemove' }
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
jest.spyOn(Object.getPrototypeOf(Object.getPrototypeOf(kubeconfig)), '_availableActions', 'get')
|
|
354
|
+
.mockReturnValue(mockActions);
|
|
355
|
+
|
|
356
|
+
const actions = kubeconfig._availableActions;
|
|
357
|
+
|
|
358
|
+
expect(actions).toStrictEqual([
|
|
359
|
+
{ action: 'goToClone' },
|
|
360
|
+
{ action: 'promptRemove' }
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|