@rancher/shell 3.0.11 → 3.0.12-rc.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/assets/styles/base/_mixins.scss +31 -0
- package/assets/styles/base/_variables.scss +2 -0
- package/assets/styles/themes/_modern.scss +6 -5
- package/assets/translations/en-us.yaml +5 -4
- package/assets/translations/zh-hans.yaml +0 -3
- package/components/EmptyProductPage.vue +76 -0
- package/components/Resource/Detail/CopyToClipboard.vue +1 -2
- package/components/Resource/Detail/Metadata/KeyValueRow.vue +9 -3
- package/components/Resource/Detail/TitleBar/__tests__/__snapshots__/index.test.ts.snap +31 -0
- package/components/Resource/Detail/TitleBar/__tests__/index.test.ts +45 -1
- package/components/Resource/Detail/TitleBar/index.vue +1 -1
- package/components/Resource/Detail/ViewOptions/__tests__/__snapshots__/index.test.ts.snap +9 -0
- package/components/Resource/Detail/ViewOptions/__tests__/index.test.ts +62 -0
- package/components/Resource/Detail/ViewOptions/index.vue +2 -1
- package/components/ResourceList/Masthead.vue +25 -2
- package/components/SideNav.vue +13 -0
- package/components/__tests__/PromptModal.test.ts +2 -0
- package/components/fleet/FleetClusters.vue +1 -0
- package/components/fleet/__tests__/FleetClusters.test.ts +71 -0
- package/components/form/NodeScheduling.vue +17 -3
- package/components/form/PrivateRegistry.vue +69 -0
- package/components/form/__tests__/PrivateRegistry.test.ts +133 -0
- package/components/formatter/WorkloadHealthScale.vue +3 -1
- package/components/nav/Group.vue +26 -3
- package/components/nav/Header.vue +32 -7
- package/components/nav/TopLevelMenu.vue +15 -1
- package/config/pagination-table-headers.js +8 -1
- package/config/product/apps.js +2 -1
- package/config/product/auth.js +1 -0
- package/config/product/backup.js +1 -0
- package/config/product/compliance.js +1 -1
- package/config/product/explorer.js +25 -6
- package/config/product/fleet.js +1 -0
- package/config/product/gatekeeper.js +1 -0
- package/config/product/istio.js +1 -0
- package/config/product/logging.js +1 -0
- package/config/product/longhorn.js +2 -1
- package/config/product/manager.js +1 -0
- package/config/product/monitoring.js +1 -0
- package/config/product/navlinks.js +1 -0
- package/config/product/neuvector.js +2 -1
- package/config/product/settings.js +1 -0
- package/config/product/uiplugins.js +1 -0
- package/core/__tests__/plugin-products-helpers.test.ts +454 -0
- package/core/__tests__/plugin-products.test.ts +3219 -0
- package/core/extension-manager-impl.js +30 -1
- package/core/plugin-products-base.ts +375 -0
- package/core/plugin-products-extending.ts +44 -0
- package/core/plugin-products-helpers.ts +262 -0
- package/core/plugin-products-top-level.ts +66 -0
- package/core/plugin-products-type-guards.ts +33 -0
- package/core/plugin-products.ts +50 -0
- package/core/plugin-types.ts +222 -0
- package/core/plugin.ts +45 -10
- package/core/productDebugger.js +48 -0
- package/core/types.ts +95 -11
- package/detail/__tests__/__snapshots__/fleet.cattle.io.bundle.test.ts.snap +52 -0
- package/detail/__tests__/fleet.cattle.io.bundle.test.ts +171 -0
- package/detail/fleet.cattle.io.bundle.vue +21 -34
- package/dialog/ExtensionCatalogInstallDialog.vue +1 -1
- package/dialog/InstallExtensionDialog.vue +6 -27
- package/dialog/UninstallExistingExtensionDialog.vue +141 -0
- package/dialog/UninstallExtensionDialog.vue +4 -26
- package/dialog/__tests__/UninstallExistingExtensionDialog.test.ts +114 -0
- package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +1 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/Ingress.test.ts +176 -0
- package/edit/provisioning.cattle.io.cluster/rke2.vue +4 -1
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +6 -0
- package/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue +7 -2
- package/list/provisioning.cattle.io.cluster.vue +0 -1
- package/list/workload.vue +11 -4
- package/mixins/resource-fetch.js +12 -3
- package/models/pod.js +18 -0
- package/models/workload.js +20 -2
- package/package.json +1 -2
- package/pages/c/_cluster/apps/charts/AppChartCardFooter.vue +0 -1
- package/pages/c/_cluster/settings/brand.vue +4 -4
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +231 -13
- package/pages/c/_cluster/uiplugins/index.vue +143 -37
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +1 -0
- package/plugins/dashboard-store/actions.js +3 -2
- package/plugins/dashboard-store/resource-class.js +62 -6
- package/plugins/plugin.js +16 -0
- package/plugins/steve/steve-pagination-utils.ts +7 -0
- package/scripts/typegen.sh +13 -1
- package/store/__tests__/type-map.test.ts +84 -24
- package/store/type-map.js +42 -3
- package/tsconfig.paths.json +1 -0
- package/types/resources/pod.ts +18 -0
- package/types/shell/index.d.ts +8506 -2909
- package/types/store/dashboard-store.types.ts +5 -0
- package/types/store/pagination.types.ts +6 -0
- package/utils/axios.js +1 -4
- package/utils/dynamic-importer.js +3 -2
- package/utils/pagination-utils.ts +1 -1
- package/utils/uiplugins.ts +12 -16
- package/utils/validators/__tests__/private-registry.test.ts +76 -0
- package/utils/validators/private-registry.ts +28 -0
|
@@ -302,7 +302,7 @@ export default {
|
|
|
302
302
|
});
|
|
303
303
|
|
|
304
304
|
if (this.extensionSvc) {
|
|
305
|
-
this.extensionUrl = `http://${ this.extensionSvc.
|
|
305
|
+
this.extensionUrl = `http://${ this.extensionSvc.metadata.name }.${ this.extensionSvc.metadata.namespace }.svc:${ this.extensionSvc.spec.ports[0].port }`;
|
|
306
306
|
} else {
|
|
307
307
|
throw new Error('Error fetching extension service');
|
|
308
308
|
}
|
|
@@ -223,7 +223,7 @@ export default {
|
|
|
223
223
|
|
|
224
224
|
const plugin = this.plugin;
|
|
225
225
|
|
|
226
|
-
this.updateStatus(plugin.
|
|
226
|
+
this.updateStatus(plugin.id, this.action);
|
|
227
227
|
|
|
228
228
|
// Find the version that the user wants to install
|
|
229
229
|
const version = plugin.versions?.find((v) => v.version === this.version);
|
|
@@ -370,31 +370,21 @@ export default {
|
|
|
370
370
|
</template>
|
|
371
371
|
|
|
372
372
|
<style lang="scss" scoped>
|
|
373
|
-
.
|
|
374
|
-
padding: 10px;
|
|
373
|
+
@import '@shell/assets/styles/base/_mixins.scss';
|
|
375
374
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
375
|
+
.plugin-install-dialog {
|
|
376
|
+
@include extension-dialog;
|
|
379
377
|
|
|
380
378
|
.dialog-panel {
|
|
381
|
-
display: flex;
|
|
382
|
-
flex-direction: column;
|
|
383
|
-
min-height: 100px;
|
|
384
|
-
|
|
385
379
|
p {
|
|
386
|
-
margin-bottom:
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
.dialog-info {
|
|
390
|
-
flex: 1;
|
|
380
|
+
margin-bottom: 4px;
|
|
391
381
|
}
|
|
392
382
|
|
|
393
383
|
.toggle-advanced {
|
|
394
384
|
display: flex;
|
|
395
385
|
align-items: center;
|
|
396
386
|
cursor: pointer;
|
|
397
|
-
margin:
|
|
387
|
+
margin: 8px 0;
|
|
398
388
|
|
|
399
389
|
&:hover {
|
|
400
390
|
text-decoration: none;
|
|
@@ -403,19 +393,8 @@ export default {
|
|
|
403
393
|
}
|
|
404
394
|
|
|
405
395
|
.version-selector {
|
|
406
|
-
margin: 0 10px 10px 10px;
|
|
407
396
|
width: auto;
|
|
408
397
|
}
|
|
409
398
|
}
|
|
410
|
-
|
|
411
|
-
.dialog-buttons {
|
|
412
|
-
display: flex;
|
|
413
|
-
justify-content: flex-end;
|
|
414
|
-
margin-top: 10px;
|
|
415
|
-
|
|
416
|
-
> *:not(:last-child) {
|
|
417
|
-
margin-right: 10px;
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
399
|
}
|
|
421
400
|
</style>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import AsyncButton from '@shell/components/AsyncButton';
|
|
3
|
+
import { CATALOG } from '@shell/config/types';
|
|
4
|
+
import { UI_PLUGIN_NAMESPACE } from '@shell/config/uiplugins';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dialog shown when user tries to install an extension that is already installed from a different source.
|
|
8
|
+
* Prompts the user to uninstall the existing version first before installing from the new source.
|
|
9
|
+
*/
|
|
10
|
+
export default {
|
|
11
|
+
emits: ['close'],
|
|
12
|
+
|
|
13
|
+
components: { AsyncButton },
|
|
14
|
+
|
|
15
|
+
props: {
|
|
16
|
+
/**
|
|
17
|
+
* The installed plugin that needs to be uninstalled
|
|
18
|
+
*/
|
|
19
|
+
installedPlugin: {
|
|
20
|
+
type: Object,
|
|
21
|
+
default: () => {},
|
|
22
|
+
required: true
|
|
23
|
+
},
|
|
24
|
+
/**
|
|
25
|
+
* Callback to update install status on extensions main screen
|
|
26
|
+
*/
|
|
27
|
+
updateStatus: {
|
|
28
|
+
type: Function,
|
|
29
|
+
default: () => {},
|
|
30
|
+
required: true
|
|
31
|
+
},
|
|
32
|
+
/**
|
|
33
|
+
* Callback when modal is closed
|
|
34
|
+
*/
|
|
35
|
+
closed: {
|
|
36
|
+
type: Function,
|
|
37
|
+
default: () => {},
|
|
38
|
+
required: true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
data() {
|
|
43
|
+
return { busy: false };
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
methods: {
|
|
47
|
+
closeDialog(result) {
|
|
48
|
+
this.closed(result);
|
|
49
|
+
this.$emit('close');
|
|
50
|
+
},
|
|
51
|
+
async uninstall() {
|
|
52
|
+
this.busy = true;
|
|
53
|
+
|
|
54
|
+
const plugin = this.installedPlugin;
|
|
55
|
+
|
|
56
|
+
this.updateStatus(plugin.id, 'uninstall');
|
|
57
|
+
|
|
58
|
+
// Delete the CR if this is a developer plugin (there is no Helm App, so need to remove the CRD ourselves)
|
|
59
|
+
if (plugin.uiplugin?.isDeveloper) {
|
|
60
|
+
// Delete the custom resource
|
|
61
|
+
await plugin.uiplugin.remove();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Find the app for this plugin using direct lookup (more efficient than findAll)
|
|
65
|
+
let pluginApp = null;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const appId = `${ UI_PLUGIN_NAMESPACE }/${ plugin.name }`;
|
|
69
|
+
|
|
70
|
+
pluginApp = await this.$store.dispatch('management/find', {
|
|
71
|
+
type: CATALOG.APP,
|
|
72
|
+
id: appId
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
// If the app cannot be found (e.g. already removed), proceed without error
|
|
76
|
+
pluginApp = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (pluginApp) {
|
|
80
|
+
try {
|
|
81
|
+
await pluginApp.remove();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
this.$store.dispatch('growl/error', {
|
|
84
|
+
title: this.t('plugins.error.generic'),
|
|
85
|
+
message: e.message ? e.message : e,
|
|
86
|
+
timeout: 10000
|
|
87
|
+
}, { root: true });
|
|
88
|
+
|
|
89
|
+
this.busy = false;
|
|
90
|
+
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await this.$store.dispatch('management/findAll', { type: CATALOG.OPERATION });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Close the dialog
|
|
98
|
+
this.closeDialog({ uninstalled: true, plugin });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<div class="plugin-install-dialog">
|
|
106
|
+
<h4 class="mt-10">
|
|
107
|
+
{{ t('plugins.install.alreadyInstalledTitle') }}
|
|
108
|
+
</h4>
|
|
109
|
+
<div class="mt-10 dialog-panel">
|
|
110
|
+
<div class="dialog-info">
|
|
111
|
+
<p>
|
|
112
|
+
{{ t('plugins.install.alreadyInstalledPrompt') }}
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="dialog-buttons">
|
|
116
|
+
<button
|
|
117
|
+
:disabled="busy"
|
|
118
|
+
class="btn role-secondary"
|
|
119
|
+
data-testid="uninstall-existing-ext-modal-cancel-btn"
|
|
120
|
+
@click="closeDialog(false)"
|
|
121
|
+
>
|
|
122
|
+
{{ t('generic.cancel') }}
|
|
123
|
+
</button>
|
|
124
|
+
<AsyncButton
|
|
125
|
+
mode="uninstall"
|
|
126
|
+
:action-label="t('plugins.install.uninstallExisting')"
|
|
127
|
+
data-testid="uninstall-existing-ext-modal-uninstall-btn"
|
|
128
|
+
@click="uninstall()"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
|
|
135
|
+
<style lang="scss" scoped>
|
|
136
|
+
@import '@shell/assets/styles/base/_mixins.scss';
|
|
137
|
+
|
|
138
|
+
.plugin-install-dialog {
|
|
139
|
+
@include extension-dialog;
|
|
140
|
+
}
|
|
141
|
+
</style>
|
|
@@ -65,7 +65,7 @@ export default {
|
|
|
65
65
|
|
|
66
66
|
const plugin = this.plugin;
|
|
67
67
|
|
|
68
|
-
this.updateStatus(plugin.
|
|
68
|
+
this.updateStatus(plugin.id, 'uninstall');
|
|
69
69
|
|
|
70
70
|
// Delete the CR if this is a developer plugin (there is no Helm App, so need to remove the CRD ourselves)
|
|
71
71
|
if (plugin.uiplugin?.isDeveloper) {
|
|
@@ -132,31 +132,9 @@ export default {
|
|
|
132
132
|
</template>
|
|
133
133
|
|
|
134
134
|
<style lang="scss" scoped>
|
|
135
|
-
.
|
|
136
|
-
padding: 10px;
|
|
137
|
-
|
|
138
|
-
h4 {
|
|
139
|
-
font-weight: bold;
|
|
140
|
-
}
|
|
135
|
+
@import '@shell/assets/styles/base/_mixins.scss';
|
|
141
136
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
flex-direction: column;
|
|
145
|
-
min-height: 100px;
|
|
146
|
-
|
|
147
|
-
.dialog-info {
|
|
148
|
-
flex: 1;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.dialog-buttons {
|
|
153
|
-
display: flex;
|
|
154
|
-
justify-content: flex-end;
|
|
155
|
-
margin-top: 10px;
|
|
156
|
-
|
|
157
|
-
> *:not(:last-child) {
|
|
158
|
-
margin-right: 10px;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
137
|
+
.plugin-install-dialog {
|
|
138
|
+
@include extension-dialog;
|
|
161
139
|
}
|
|
162
140
|
</style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { shallowMount, VueWrapper } from '@vue/test-utils';
|
|
2
|
+
import UninstallExistingExtensionDialog from '@shell/dialog/UninstallExistingExtensionDialog.vue';
|
|
3
|
+
|
|
4
|
+
const t = (key: string): string => key;
|
|
5
|
+
|
|
6
|
+
describe('component: UninstallExistingExtensionDialog', () => {
|
|
7
|
+
let wrapper: VueWrapper<any>;
|
|
8
|
+
|
|
9
|
+
const mountComponent = (propsData = {}) => {
|
|
10
|
+
const store = { dispatch: jest.fn().mockResolvedValue([]) };
|
|
11
|
+
|
|
12
|
+
const defaultProps = {
|
|
13
|
+
installedPlugin: {
|
|
14
|
+
id: 'test-plugin', name: 'test-plugin', label: 'Test Plugin'
|
|
15
|
+
},
|
|
16
|
+
updateStatus: jest.fn(),
|
|
17
|
+
closed: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return shallowMount(UninstallExistingExtensionDialog, {
|
|
21
|
+
propsData: {
|
|
22
|
+
...defaultProps,
|
|
23
|
+
...propsData,
|
|
24
|
+
},
|
|
25
|
+
global: {
|
|
26
|
+
mocks: {
|
|
27
|
+
$store: store,
|
|
28
|
+
$router: { go: jest.fn() },
|
|
29
|
+
t,
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('rendering', () => {
|
|
36
|
+
it('should render the dialog title', () => {
|
|
37
|
+
wrapper = mountComponent();
|
|
38
|
+
|
|
39
|
+
const title = wrapper.find('h4');
|
|
40
|
+
|
|
41
|
+
expect(title.text()).toBe('plugins.install.alreadyInstalledTitle');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should render the dialog prompt', () => {
|
|
45
|
+
wrapper = mountComponent();
|
|
46
|
+
|
|
47
|
+
const prompt = wrapper.find('.dialog-info p');
|
|
48
|
+
|
|
49
|
+
expect(prompt.text()).toBe('plugins.install.alreadyInstalledPrompt');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should render cancel button', () => {
|
|
53
|
+
wrapper = mountComponent();
|
|
54
|
+
|
|
55
|
+
const cancelBtn = wrapper.find('[data-testid="uninstall-existing-ext-modal-cancel-btn"]');
|
|
56
|
+
|
|
57
|
+
expect(cancelBtn.exists()).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should render uninstall button', () => {
|
|
61
|
+
wrapper = mountComponent();
|
|
62
|
+
|
|
63
|
+
const uninstallBtn = wrapper.find('[data-testid="uninstall-existing-ext-modal-uninstall-btn"]');
|
|
64
|
+
|
|
65
|
+
expect(uninstallBtn.exists()).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('closeDialog', () => {
|
|
70
|
+
it('should call closed callback and emit close event when cancel is clicked', async() => {
|
|
71
|
+
const closedFn = jest.fn();
|
|
72
|
+
|
|
73
|
+
wrapper = mountComponent({ closed: closedFn });
|
|
74
|
+
|
|
75
|
+
const cancelBtn = wrapper.find('[data-testid="uninstall-existing-ext-modal-cancel-btn"]');
|
|
76
|
+
|
|
77
|
+
await cancelBtn.trigger('click');
|
|
78
|
+
|
|
79
|
+
expect(closedFn).toHaveBeenCalledWith(false);
|
|
80
|
+
expect(wrapper.emitted('close')).toBeTruthy();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('uninstall', () => {
|
|
85
|
+
it('should call updateStatus with uninstall action', async() => {
|
|
86
|
+
const updateStatusFn = jest.fn();
|
|
87
|
+
const installedPlugin = {
|
|
88
|
+
id: 'test-plugin', name: 'test-plugin', label: 'Test Plugin'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
wrapper = mountComponent({ installedPlugin, updateStatus: updateStatusFn });
|
|
92
|
+
|
|
93
|
+
await wrapper.vm.uninstall();
|
|
94
|
+
|
|
95
|
+
expect(updateStatusFn).toHaveBeenCalledWith('test-plugin', 'uninstall');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should remove developer plugin CR if isDeveloper is true', async() => {
|
|
99
|
+
const removeFn = jest.fn().mockResolvedValue(undefined);
|
|
100
|
+
const installedPlugin = {
|
|
101
|
+
id: 'test-plugin',
|
|
102
|
+
name: 'test-plugin',
|
|
103
|
+
label: 'Test Plugin',
|
|
104
|
+
uiplugin: { isDeveloper: true, remove: removeFn }
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
wrapper = mountComponent({ installedPlugin });
|
|
108
|
+
|
|
109
|
+
await wrapper.vm.uninstall();
|
|
110
|
+
|
|
111
|
+
expect(removeFn).toHaveBeenCalledWith();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils';
|
|
2
|
+
import Ingress from '@shell/edit/provisioning.cattle.io.cluster/tabs/Ingress.vue';
|
|
3
|
+
import { _EDIT } from '@shell/config/query-params';
|
|
4
|
+
import { INGRESS_DUAL, TRAEFIK, INGRESS_NGINX, INGRESS_NONE } from '@shell/edit/provisioning.cattle.io.cluster/shared';
|
|
5
|
+
|
|
6
|
+
jest.mock('vuex', () => ({
|
|
7
|
+
useStore: () => ({ getters: { 'i18n/t': (key: string) => key } }),
|
|
8
|
+
mapGetters: () => ({ t: (key: string) => key })
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
jest.mock('@shell/edit/provisioning.cattle.io.cluster/shared', () => ({
|
|
12
|
+
|
|
13
|
+
INGRESS_NGINX: 'ingress-nginx',
|
|
14
|
+
TRAEFIK: 'traefik',
|
|
15
|
+
INGRESS_DUAL: 'dual',
|
|
16
|
+
INGRESS_NONE: 'none',
|
|
17
|
+
INGRESS_OPTIONS: [{
|
|
18
|
+
id: 'traefik',
|
|
19
|
+
image: { src: '', alt: 'Traefik' },
|
|
20
|
+
header: { title: { key: 'cluster.ingress.traefik.header' } },
|
|
21
|
+
subHeader: { label: { key: 'cluster.ingress.recommended' } },
|
|
22
|
+
content: { key: 'cluster.ingress.traefik.content' },
|
|
23
|
+
doc: { url: 'https://docs.rke2.io/networking/networking_services?_highlight=ingress#ingress-controller' }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'ingress-nginx',
|
|
27
|
+
image: { src: '', alt: 'NGINX' },
|
|
28
|
+
header: { title: { key: 'cluster.ingress.nginx.header' } },
|
|
29
|
+
subHeader: { label: { key: 'cluster.ingress.legacy' } },
|
|
30
|
+
content: { key: 'cluster.ingress.nginx.content' },
|
|
31
|
+
doc: { url: 'https://www.kubernetes.dev/blog/2025/11/12/ingress-nginx-retirement/' }
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'dual',
|
|
35
|
+
header: { title: { key: 'cluster.ingress.dual.header' } },
|
|
36
|
+
subHeader: { label: { key: 'cluster.ingress.migration' } },
|
|
37
|
+
content: { key: 'cluster.ingress.dual.content' }
|
|
38
|
+
}],
|
|
39
|
+
INGRESS_MIGRATION_KB_LINK: 'mock-link',
|
|
40
|
+
INGRESS_CONTROLLER_CLASS_MIGRATION: 'rke2.cattle.io/ingress-nginx-migration',
|
|
41
|
+
INGRESS_CLASS_DEFAULT: 'rke2.cattle.io/ingress-nginx-default',
|
|
42
|
+
INGRESS_CONTROLLER_CLASS_DEFAULT: 'rke2.cattle.io/ingress-nginx-controller-default',
|
|
43
|
+
INGRESS_CLASS_MIGRATION: 'rke2.cattle.io/ingress-nginx-migration'
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
describe('ingress.vue', () => {
|
|
47
|
+
const defaultProps = {
|
|
48
|
+
mode: _EDIT,
|
|
49
|
+
value: INGRESS_NONE,
|
|
50
|
+
nginxSupported: true,
|
|
51
|
+
traefikSupported: true,
|
|
52
|
+
nginxChart: 'rancher-ingress-nginx',
|
|
53
|
+
traefikChart: 'traefik',
|
|
54
|
+
userChartValues: {},
|
|
55
|
+
versionInfo: {
|
|
56
|
+
'rancher-ingress-nginx': { values: {} },
|
|
57
|
+
traefik: { values: {} }
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const createWrapper = (props = {}) => mount(Ingress, {
|
|
62
|
+
props: { ...defaultProps, ...props },
|
|
63
|
+
global: {
|
|
64
|
+
stubs: {
|
|
65
|
+
Checkbox: true,
|
|
66
|
+
Banner: true,
|
|
67
|
+
IngressCards: true,
|
|
68
|
+
IngressConfiguration: true,
|
|
69
|
+
YamlEditor: true,
|
|
70
|
+
RichTranslation: true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders checkbox to enable/disable ingress', () => {
|
|
76
|
+
const wrapper = createWrapper();
|
|
77
|
+
const checkbox = wrapper.findComponent({ name: 'Checkbox' });
|
|
78
|
+
|
|
79
|
+
expect(checkbox.exists()).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('emits update:value with INGRESS_NONE when ingress is disabled', async() => {
|
|
83
|
+
const wrapper = createWrapper({ value: TRAEFIK });
|
|
84
|
+
const checkbox = wrapper.findComponent({ name: 'Checkbox' });
|
|
85
|
+
|
|
86
|
+
await checkbox.vm.$emit('update:value', false);
|
|
87
|
+
|
|
88
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
89
|
+
expect(wrapper.emitted('update:value')?.[0]).toStrictEqual([INGRESS_NONE]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('emits update:value with TRAEFIK when ingress is enabled and traefik is supported', async() => {
|
|
93
|
+
const wrapper = createWrapper({ value: INGRESS_NONE });
|
|
94
|
+
const checkbox = wrapper.findComponent({ name: 'Checkbox' });
|
|
95
|
+
|
|
96
|
+
await checkbox.vm.$emit('update:value', true);
|
|
97
|
+
|
|
98
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
99
|
+
expect(wrapper.emitted('update:value')?.[0]).toStrictEqual([TRAEFIK]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('emits update:value with INGRESS_NGINX when ingress is enabled, traefik is NOT supported, and nginx IS supported', async() => {
|
|
103
|
+
const wrapper = createWrapper({ value: INGRESS_NONE, traefikSupported: false });
|
|
104
|
+
const checkbox = wrapper.findComponent({ name: 'Checkbox' });
|
|
105
|
+
|
|
106
|
+
await checkbox.vm.$emit('update:value', true);
|
|
107
|
+
|
|
108
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
109
|
+
expect(wrapper.emitted('update:value')?.[0]).toStrictEqual([INGRESS_NGINX]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('selectIngress emits [INGRESS_NGINX, TRAEFIK] string value when INGRESS_DUAL is selected and previous value was ingress-nginx', () => {
|
|
113
|
+
const wrapper = createWrapper({ value: INGRESS_NGINX, originalIngressController: INGRESS_NGINX });
|
|
114
|
+
const ingressCards = wrapper.findComponent({ name: 'IngressCards' });
|
|
115
|
+
|
|
116
|
+
ingressCards.vm.$emit('select', INGRESS_DUAL);
|
|
117
|
+
|
|
118
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
119
|
+
expect(wrapper.emitted('update:value')?.[0]).toStrictEqual([[INGRESS_NGINX, TRAEFIK]]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('selectIngress emits [TRAEFIK, INGRESS_NGINX] string value when INGRESS_DUAL is selected and previous value was traefik', () => {
|
|
123
|
+
const wrapper = createWrapper({ value: TRAEFIK, originalIngressController: TRAEFIK });
|
|
124
|
+
const ingressCards = wrapper.findComponent({ name: 'IngressCards' });
|
|
125
|
+
|
|
126
|
+
ingressCards.vm.$emit('select', INGRESS_DUAL);
|
|
127
|
+
|
|
128
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
129
|
+
expect(wrapper.emitted('update:value')?.[0]).toStrictEqual([[TRAEFIK, INGRESS_NGINX]]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('selectIngress emits [TRAEFIK, INGRESS_NGINX] string value when INGRESS_DUAL is selected and value went traefik -> nginx -> dual', () => {
|
|
133
|
+
const wrapper = createWrapper({ value: TRAEFIK, originalIngressController: TRAEFIK });
|
|
134
|
+
const ingressCards = wrapper.findComponent({ name: 'IngressCards' });
|
|
135
|
+
|
|
136
|
+
ingressCards.vm.$emit('select', INGRESS_NGINX);
|
|
137
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
138
|
+
expect(wrapper.emitted('update:value')?.[0]).toStrictEqual([INGRESS_NGINX]);
|
|
139
|
+
|
|
140
|
+
ingressCards.vm.$emit('select', INGRESS_DUAL);
|
|
141
|
+
|
|
142
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
143
|
+
expect(wrapper.emitted('update:value')?.[1]).toStrictEqual([[TRAEFIK, INGRESS_NGINX]]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('selectIngress emits string value when a single ingress is selected', () => {
|
|
147
|
+
const wrapper = createWrapper({ value: TRAEFIK });
|
|
148
|
+
const ingressCards = wrapper.findComponent({ name: 'IngressCards' });
|
|
149
|
+
|
|
150
|
+
ingressCards.vm.$emit('select', INGRESS_NGINX);
|
|
151
|
+
|
|
152
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
153
|
+
expect(wrapper.emitted('update:value')?.[0]).toStrictEqual([INGRESS_NGINX]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('renders IngressConfiguration when versionInfo contains chart values', () => {
|
|
157
|
+
const wrapper = createWrapper({ value: TRAEFIK });
|
|
158
|
+
const config = wrapper.findComponent({ name: 'IngressConfiguration' });
|
|
159
|
+
|
|
160
|
+
expect(config.exists()).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('toggles advanced configuration visibility and renders YamlEditor', async() => {
|
|
164
|
+
const wrapper = createWrapper({ value: TRAEFIK });
|
|
165
|
+
|
|
166
|
+
expect(wrapper.findComponent({ name: 'YamlEditor' }).exists()).toBe(false);
|
|
167
|
+
|
|
168
|
+
const advancedButton = wrapper.find('.advanced-toggle');
|
|
169
|
+
|
|
170
|
+
await advancedButton.trigger('click');
|
|
171
|
+
|
|
172
|
+
const yamlEditor = wrapper.find('[data-testid="traefik-yaml-editor"]');
|
|
173
|
+
|
|
174
|
+
expect(yamlEditor.exists()).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -297,7 +297,8 @@ export default {
|
|
|
297
297
|
originalKubeVersion: null,
|
|
298
298
|
isEmpty,
|
|
299
299
|
AGENT_CONFIGURATION_TYPES,
|
|
300
|
-
basicsValid: true
|
|
300
|
+
basicsValid: true,
|
|
301
|
+
originalIngressController: this.value.spec.rkeConfig.machineGlobalConfig?.[INGRESS_CONTROLLER] || INGRESS_NONE,
|
|
301
302
|
};
|
|
302
303
|
},
|
|
303
304
|
|
|
@@ -2522,6 +2523,7 @@ export default {
|
|
|
2522
2523
|
<Tab
|
|
2523
2524
|
v-if="!obj.remove"
|
|
2524
2525
|
:key="obj.id"
|
|
2526
|
+
:weight="-1 * idx"
|
|
2525
2527
|
:name="obj.id"
|
|
2526
2528
|
:label="obj.pool.name || '(Not Named)'"
|
|
2527
2529
|
:show-header="false"
|
|
@@ -2592,6 +2594,7 @@ export default {
|
|
|
2592
2594
|
:is-azure-provider-unsupported="isAzureProviderUnsupported"
|
|
2593
2595
|
:can-azure-migrate-on-edit="canAzureMigrateOnEdit"
|
|
2594
2596
|
:has-some-ipv6-pools="hasOnlyIpv6Pools"
|
|
2597
|
+
:original-ingress-controller="originalIngressController"
|
|
2595
2598
|
@update:value="$emit('input', $event)"
|
|
2596
2599
|
@cilium-values-changed="handleCiliumValuesChanged"
|
|
2597
2600
|
@enabled-system-services-changed="handleEnabledSystemServicesChanged"
|
|
@@ -120,6 +120,11 @@ export default {
|
|
|
120
120
|
canAzureMigrateOnEdit: {
|
|
121
121
|
type: Boolean,
|
|
122
122
|
required: true
|
|
123
|
+
},
|
|
124
|
+
originalIngressController: {
|
|
125
|
+
type: [String, Array],
|
|
126
|
+
required: false,
|
|
127
|
+
default: INGRESS_NONE
|
|
123
128
|
}
|
|
124
129
|
},
|
|
125
130
|
|
|
@@ -699,6 +704,7 @@ export default {
|
|
|
699
704
|
:traefik-chart="traefikChart"
|
|
700
705
|
:user-chart-values="userChartValues"
|
|
701
706
|
:version-info="versionInfo"
|
|
707
|
+
:original-ingress-controller="originalIngressController"
|
|
702
708
|
@update-values="(name, val) => $emit('update-values', name, val)"
|
|
703
709
|
@error="$emit('error', $event)"
|
|
704
710
|
@yaml-validation-changed="e => $emit('yaml-validation-changed', e)"
|
|
@@ -24,6 +24,7 @@ interface Props {
|
|
|
24
24
|
traefikChart: string;
|
|
25
25
|
userChartValues: any;
|
|
26
26
|
versionInfo: any;
|
|
27
|
+
originalIngressController?: string | string[];
|
|
27
28
|
}
|
|
28
29
|
const {
|
|
29
30
|
mode = _CREATE,
|
|
@@ -33,7 +34,8 @@ const {
|
|
|
33
34
|
nginxSupported,
|
|
34
35
|
traefikSupported,
|
|
35
36
|
userChartValues,
|
|
36
|
-
versionInfo
|
|
37
|
+
versionInfo,
|
|
38
|
+
originalIngressController = INGRESS_NONE
|
|
37
39
|
} = defineProps<Props>();
|
|
38
40
|
|
|
39
41
|
const emit = defineEmits(['update:value', 'error', 'config-validation-changed', 'yaml-validation-changed', 'update-values']);
|
|
@@ -179,7 +181,10 @@ const compatibilityMode = computed({
|
|
|
179
181
|
|
|
180
182
|
function selectIngress(id: string) {
|
|
181
183
|
if ( id === INGRESS_DUAL) {
|
|
182
|
-
|
|
184
|
+
const newValue: string | string[] = !Array.isArray(originalIngressController) ? (originalIngressController === TRAEFIK ? [TRAEFIK, INGRESS_NGINX] : [INGRESS_NGINX, TRAEFIK]) : originalIngressController;
|
|
185
|
+
|
|
186
|
+
emit('update:value', newValue);
|
|
187
|
+
|
|
183
188
|
preconfigureTraefik();
|
|
184
189
|
} else {
|
|
185
190
|
emit('update:value', id);
|
|
@@ -220,7 +220,6 @@ export default {
|
|
|
220
220
|
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
|
221
221
|
:data-testid="'cluster-list'"
|
|
222
222
|
:force-update-live-and-delayed="forceUpdateLiveAndDelayed"
|
|
223
|
-
:sub-rows="true"
|
|
224
223
|
>
|
|
225
224
|
<!-- Why are state column and subrow overwritten here? -->
|
|
226
225
|
<!-- for rke1 clusters, where they try to use the mgmt cluster stateObj instead of prov cluster stateObj, -->
|
package/list/workload.vue
CHANGED
|
@@ -99,13 +99,21 @@ export default {
|
|
|
99
99
|
const schema = type !== workloadSchema.id ? this.$store.getters['cluster/schemaFor'](type) : workloadSchema;
|
|
100
100
|
const paginationEnabled = !allTypes && this.$store.getters[`cluster/paginationEnabled`]?.({ id: type });
|
|
101
101
|
|
|
102
|
+
const workloadIncludeAssociatedData = paginationEnabled && [
|
|
103
|
+
WORKLOAD_TYPES.DEPLOYMENT,
|
|
104
|
+
WORKLOAD_TYPES.DAEMON_SET,
|
|
105
|
+
WORKLOAD_TYPES.STATEFUL_SET,
|
|
106
|
+
WORKLOAD_TYPES.JOB,
|
|
107
|
+
].includes(type);
|
|
108
|
+
|
|
102
109
|
return {
|
|
103
110
|
allTypes,
|
|
104
111
|
schema,
|
|
105
112
|
paginationEnabled,
|
|
106
113
|
resources: [],
|
|
107
114
|
loadResources,
|
|
108
|
-
loadIndeterminate
|
|
115
|
+
loadIndeterminate,
|
|
116
|
+
workloadIncludeAssociatedData
|
|
109
117
|
};
|
|
110
118
|
},
|
|
111
119
|
|
|
@@ -143,10 +151,8 @@ export default {
|
|
|
143
151
|
* Fetch resources required to populate POD_RESTARTS and WORKLOAD_HEALTH_SCALE columns
|
|
144
152
|
*/
|
|
145
153
|
loadHeathResources() {
|
|
146
|
-
// See https://github.com/rancher/dashboard/issues/10417, health comes from selectors applied locally to all pods (bad)
|
|
147
154
|
if (this.paginationEnabled) {
|
|
148
|
-
//
|
|
149
|
-
// See https://github.com/rancher/dashboard/issues/14211
|
|
155
|
+
// When SSP is enabled we efficiently fetch stats for health column imbedded in the original resource type by supplying `includeAssociatedData` param
|
|
150
156
|
return;
|
|
151
157
|
}
|
|
152
158
|
|
|
@@ -184,6 +190,7 @@ export default {
|
|
|
184
190
|
v-if="paginationEnabled"
|
|
185
191
|
:schema="schema"
|
|
186
192
|
:use-query-params-for-simple-filtering="useQueryParamsForSimpleFiltering"
|
|
193
|
+
:includeAssociatedData="workloadIncludeAssociatedData"
|
|
187
194
|
/>
|
|
188
195
|
<ResourceTable
|
|
189
196
|
v-else
|