@rancher/shell 3.0.5-rc.6 → 3.0.5-rc.7
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/_variables.scss +17 -11
- package/assets/styles/themes/_dark.scss +2 -0
- package/assets/styles/themes/_light.scss +8 -2
- package/assets/translations/en-us.yaml +26 -4
- package/components/CodeMirror.vue +1 -1
- package/components/Drawer/Chrome.vue +0 -1
- package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +26 -2
- package/components/Drawer/ResourceDetailDrawer/composables.ts +4 -1
- package/components/Drawer/ResourceDetailDrawer/index.vue +1 -0
- package/components/Loading.vue +1 -1
- package/components/PaginatedResourceTable.vue +46 -1
- package/components/PromptRestore.vue +22 -44
- package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +10 -2
- package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +21 -2
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +8 -1
- package/components/Resource/Detail/Metadata/KeyValue.vue +12 -10
- package/components/Resource/Detail/Metadata/Rectangle.vue +3 -1
- package/components/Resource/Detail/SpacedRow.vue +1 -1
- package/components/Resource/Detail/TitleBar/composables.ts +4 -3
- package/components/Resource/Detail/TitleBar/index.vue +2 -2
- package/components/ResourceDetail/Masthead/legacy.vue +1 -1
- package/components/ResourceDetail/index.vue +5 -3
- package/components/ResourceList/index.vue +1 -0
- package/components/ResourceTable.vue +6 -1
- package/components/ResourceYaml.vue +1 -1
- package/components/RichTranslation.vue +106 -0
- package/components/SlideInPanelManager.vue +3 -7
- package/components/SortableTable/index.vue +1 -1
- package/components/SortableTable/selection.js +0 -1
- package/components/Tabbed/index.vue +6 -1
- package/components/__tests__/PromptRestore.test.ts +1 -65
- package/components/__tests__/RichTranslation.test.ts +115 -0
- package/components/fleet/dashboard/ResourcePanel.vue +2 -1
- package/components/form/FileImageSelector.vue +1 -1
- package/components/form/NameNsDescription.vue +1 -0
- package/components/form/Networking.vue +24 -19
- package/components/form/ResourceLabeledSelect.vue +4 -3
- package/components/form/SelectOrCreateAuthSecret.vue +6 -3
- package/components/form/__tests__/Networking.test.ts +116 -0
- package/components/formatter/PodImages.vue +1 -1
- package/components/formatter/__tests__/LiveDate.test.ts +10 -2
- package/components/google/AccountAccess.vue +44 -46
- package/components/nav/Group.vue +4 -1
- package/composables/resources.ts +2 -2
- package/config/labels-annotations.js +2 -0
- package/config/pagination-table-headers.js +8 -1
- package/config/product/explorer.js +27 -2
- package/config/product/manager.js +0 -1
- package/config/query-params.js +10 -0
- package/config/router/routes.js +21 -1
- package/config/system-namespaces.js +1 -1
- package/config/table-headers.js +30 -1
- package/config/types.js +1 -1
- package/config/version.js +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +3 -47
- package/dialog/RotateEncryptionKeyDialog.vue +10 -30
- package/edit/auth/ldap/__tests__/config.test.ts +14 -0
- package/edit/auth/ldap/config.vue +24 -0
- package/edit/compliance.cattle.io.clusterscan.vue +1 -1
- package/edit/configmap.vue +4 -1
- package/edit/networking.k8s.io.ingress/Certificate.vue +12 -12
- package/edit/networking.k8s.io.ingress/__tests__/Certificate.test.ts +165 -0
- package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +3 -2
- package/edit/provisioning.cattle.io.cluster/rke2.vue +102 -48
- package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +22 -13
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +2 -2
- package/edit/secret/basic.vue +1 -0
- package/edit/secret/index.vue +126 -15
- package/list/projectsecret.vue +345 -0
- package/list/secret.vue +109 -0
- package/mixins/__tests__/brand.spec.ts +2 -2
- package/mixins/create-edit-view/impl.js +10 -1
- package/mixins/resource-fetch-api-pagination.js +9 -9
- package/mixins/resource-fetch.js +3 -1
- package/models/cluster.x-k8s.io.machinedeployment.js +11 -2
- package/models/fleet.cattle.io.cluster.js +2 -2
- package/models/provisioning.cattle.io.cluster.js +24 -28
- package/models/secret.js +157 -2
- package/package.json +2 -2
- package/pages/c/_cluster/apps/charts/index.vue +46 -35
- package/pages/c/_cluster/explorer/projectsecret.vue +34 -0
- package/pages/c/_cluster/fleet/index.vue +0 -1
- package/pages/explorer/resource/detail/projectsecret.vue +9 -0
- package/pages/explorer/resource/detail/secret.vue +18 -5
- package/plugins/dashboard-store/__tests__/normalize.test.ts +223 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +191 -0
- package/plugins/dashboard-store/__tests__/utils/normalize-usecases.ts +1526 -0
- package/plugins/dashboard-store/normalize.js +29 -17
- package/plugins/dashboard-store/resource-class.js +52 -17
- package/plugins/steve/steve-pagination-utils.ts +14 -3
- package/types/kube/kube-api.ts +12 -0
- package/types/shell/index.d.ts +616 -558
- package/types/store/pagination.types.ts +16 -6
- package/utils/__tests__/create-yaml.test.ts +235 -0
- package/utils/create-yaml.js +103 -9
- package/utils/pagination-utils.ts +18 -0
- package/models/etcdbackup.js +0 -45
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { SNAPSHOT
|
|
2
|
+
import { SNAPSHOT } from '@shell/config/types';
|
|
3
3
|
import AsyncButton from '@shell/components/AsyncButton';
|
|
4
4
|
import { Card } from '@components/Card';
|
|
5
5
|
import { Banner } from '@components/Banner';
|
|
@@ -64,23 +64,11 @@ export default {
|
|
|
64
64
|
},
|
|
65
65
|
|
|
66
66
|
async getEtcdBackups() {
|
|
67
|
-
|
|
68
|
-
let etcdBackups = await this.$store.dispatch('rancher/findAll', { type: NORMAN.ETCD_BACKUP });
|
|
67
|
+
let etcdBackups = await this.$store.dispatch('management/findAll', { type: SNAPSHOT });
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
etcdBackups = etcdBackups.filter((backup) => backup.clusterId === this.cluster.id);
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (this.cluster.isRke2) {
|
|
76
|
-
let etcdBackups = await this.$store.dispatch('management/findAll', { type: SNAPSHOT });
|
|
77
|
-
|
|
78
|
-
etcdBackups = etcdBackups.filter((backup) => backup.clusterId === this.cluster.id);
|
|
79
|
-
|
|
80
|
-
return etcdBackups;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return [];
|
|
71
|
+
return etcdBackups;
|
|
84
72
|
},
|
|
85
73
|
|
|
86
74
|
getFormattedCreatedDate(createdDate) {
|
|
@@ -93,23 +81,15 @@ export default {
|
|
|
93
81
|
},
|
|
94
82
|
|
|
95
83
|
async apply(buttonDone) {
|
|
96
|
-
const isRke2 = this.cluster.isRke2;
|
|
97
|
-
|
|
98
84
|
try {
|
|
99
|
-
|
|
100
|
-
const currentGeneration = this.cluster.spec?.rkeConfig?.rotateEncryptionKeys?.generation || 0;
|
|
101
|
-
|
|
102
|
-
// To rotate the encryption keys, increment
|
|
103
|
-
// rkeConfig.rotateEncyrptionKeys.generation in the YAML.
|
|
104
|
-
set(this.cluster, 'spec.rkeConfig.rotateEncryptionKeys.generation', currentGeneration + 1);
|
|
105
|
-
await this.cluster.save();
|
|
85
|
+
const currentGeneration = this.cluster.spec?.rkeConfig?.rotateEncryptionKeys?.generation || 0;
|
|
106
86
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
87
|
+
// To rotate the encryption keys, increment
|
|
88
|
+
// rkeConfig.rotateEncyrptionKeys.generation in the YAML.
|
|
89
|
+
set(this.cluster, 'spec.rkeConfig.rotateEncryptionKeys.generation', currentGeneration + 1);
|
|
90
|
+
await this.cluster.save();
|
|
110
91
|
|
|
111
|
-
|
|
112
|
-
}
|
|
92
|
+
this.close(buttonDone);
|
|
113
93
|
} catch (err) {
|
|
114
94
|
this.errors = exceptionToErrorsArray(err);
|
|
115
95
|
buttonDone(false);
|
|
@@ -41,4 +41,18 @@ describe('lDAP config', () => {
|
|
|
41
41
|
expect(userLoginFilter.element.value).toBe(expectedValue);
|
|
42
42
|
expect(wrapper.vm.model.userLoginFilter).toBeUndefined();
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
it.each([
|
|
46
|
+
'openldap', 'freeipa'
|
|
47
|
+
])('should display searchUsingServiceAccount checkbox if type %p', (type) => {
|
|
48
|
+
const wrapper = mount(LDAPConfig, {
|
|
49
|
+
propsData: {
|
|
50
|
+
value: {},
|
|
51
|
+
type,
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
const checkbox = wrapper.find('[data-testid="searchUsingServiceAccount"]');
|
|
55
|
+
|
|
56
|
+
expect(checkbox).toBeDefined();
|
|
57
|
+
});
|
|
44
58
|
});
|
|
@@ -11,6 +11,8 @@ const DEFAULT_TLS_PORT = 636;
|
|
|
11
11
|
|
|
12
12
|
export const SHIBBOLETH = 'shibboleth';
|
|
13
13
|
export const OKTA = 'okta';
|
|
14
|
+
export const OPEN_LDAP = 'openldap';
|
|
15
|
+
export const FREE_IPA = 'freeipa';
|
|
14
16
|
|
|
15
17
|
export default {
|
|
16
18
|
emits: ['update:value'],
|
|
@@ -64,6 +66,11 @@ export default {
|
|
|
64
66
|
// Does the auth provider support LDAP for search in addition to SAML?
|
|
65
67
|
isSamlProvider() {
|
|
66
68
|
return this.type === SHIBBOLETH || this.type === OKTA;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Allow to enable user search just for these providers
|
|
72
|
+
isSearchAllowed() {
|
|
73
|
+
return this.type === OPEN_LDAP || this.type === FREE_IPA;
|
|
67
74
|
}
|
|
68
75
|
},
|
|
69
76
|
|
|
@@ -226,6 +233,23 @@ export default {
|
|
|
226
233
|
/>
|
|
227
234
|
</div>
|
|
228
235
|
</div>
|
|
236
|
+
|
|
237
|
+
<div
|
|
238
|
+
v-if="isSearchAllowed"
|
|
239
|
+
class="row mb-20"
|
|
240
|
+
>
|
|
241
|
+
<div class="col">
|
|
242
|
+
<Checkbox
|
|
243
|
+
v-model:value="model.searchUsingServiceAccount"
|
|
244
|
+
:mode="mode"
|
|
245
|
+
data-testid="searchUsingServiceAccount"
|
|
246
|
+
class="full-height"
|
|
247
|
+
:label="t('authConfig.ldap.searchUsingServiceAccount.label')"
|
|
248
|
+
:tooltip="t('authConfig.ldap.searchUsingServiceAccount.tip')"
|
|
249
|
+
/>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
229
253
|
<div class="row mb-20">
|
|
230
254
|
<div class="col span-6">
|
|
231
255
|
<LabeledInput
|
|
@@ -79,7 +79,7 @@ export default {
|
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
try {
|
|
82
|
-
this.defaultConfigMap = await this.$store.dispatch('cluster/find', { type: CONFIG_MAP, id: '
|
|
82
|
+
this.defaultConfigMap = await this.$store.dispatch('cluster/find', { type: CONFIG_MAP, id: 'compliance-operator-system/default-clusterscanprofiles' });
|
|
83
83
|
} catch {}
|
|
84
84
|
|
|
85
85
|
this.allProfiles = hash.profiles;
|
package/edit/configmap.vue
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script>
|
|
2
|
+
import jsyaml from 'js-yaml';
|
|
2
3
|
import CreateEditView from '@shell/mixins/create-edit-view';
|
|
3
4
|
import CruResource from '@shell/components/CruResource';
|
|
4
5
|
import NameNsDescription from '@shell/components/form/NameNsDescription';
|
|
@@ -66,7 +67,9 @@ export default {
|
|
|
66
67
|
const yaml = await this.$refs.cru.createResourceYaml(this.yamlModifiers);
|
|
67
68
|
|
|
68
69
|
try {
|
|
69
|
-
|
|
70
|
+
const initialYaml = jsyaml.dump(this.initialValue);
|
|
71
|
+
|
|
72
|
+
await this.value.saveYaml(yaml, initialYaml);
|
|
70
73
|
this.done();
|
|
71
74
|
} catch (err) {
|
|
72
75
|
this.errors.push(err);
|
|
@@ -35,8 +35,16 @@ export default {
|
|
|
35
35
|
defaultCert,
|
|
36
36
|
hosts,
|
|
37
37
|
secretName,
|
|
38
|
+
secretVal: this.value.secretName ?? DEFAULT_CERT_VALUE,
|
|
38
39
|
};
|
|
39
40
|
},
|
|
41
|
+
watch: {
|
|
42
|
+
value(newVal) {
|
|
43
|
+
this.hosts = newVal.hosts;
|
|
44
|
+
this.secretName = newVal.secretName;
|
|
45
|
+
this.secretVal = this.secretName === null ? DEFAULT_CERT_VALUE : this.secretName;
|
|
46
|
+
},
|
|
47
|
+
},
|
|
40
48
|
computed: {
|
|
41
49
|
certsWithDefault() {
|
|
42
50
|
return [this.defaultCert, ...this.certs.map((c) => ({ label: c, value: c }))];
|
|
@@ -51,15 +59,10 @@ export default {
|
|
|
51
59
|
},
|
|
52
60
|
},
|
|
53
61
|
methods: {
|
|
54
|
-
addHost(ev) {
|
|
55
|
-
ev.preventDefault();
|
|
56
|
-
this.hosts.push('');
|
|
57
|
-
this.update();
|
|
58
|
-
},
|
|
59
62
|
update() {
|
|
60
63
|
const out = { hosts: this.hosts };
|
|
61
64
|
|
|
62
|
-
out.secretName = this.
|
|
65
|
+
out.secretName = this.secretVal;
|
|
63
66
|
|
|
64
67
|
if (out.secretName === DEFAULT_CERT_VALUE) {
|
|
65
68
|
out.secretName = null;
|
|
@@ -68,7 +71,7 @@ export default {
|
|
|
68
71
|
this.$emit('update:value', out);
|
|
69
72
|
},
|
|
70
73
|
onSecretInput(e) {
|
|
71
|
-
this.
|
|
74
|
+
this.secretVal = e && typeof e === 'object' ? e.label : e;
|
|
72
75
|
this.update();
|
|
73
76
|
},
|
|
74
77
|
onHostsInput(e) {
|
|
@@ -80,13 +83,10 @@ export default {
|
|
|
80
83
|
</script>
|
|
81
84
|
|
|
82
85
|
<template>
|
|
83
|
-
<div
|
|
84
|
-
class="cert row"
|
|
85
|
-
@update:value="update"
|
|
86
|
-
>
|
|
86
|
+
<div class="cert row">
|
|
87
87
|
<div class="col span-6">
|
|
88
88
|
<LabeledSelect
|
|
89
|
-
v-model:value="
|
|
89
|
+
v-model:value="secretVal"
|
|
90
90
|
class="secret-name"
|
|
91
91
|
:options="certsWithDefault"
|
|
92
92
|
:label="t('ingress.certificates.certificate.label')"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { shallowMount } from '@vue/test-utils';
|
|
2
|
+
import Certificate from '../Certificate.vue';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CERT_VALUE = '__[[DEFAULT_CERT]]__';
|
|
5
|
+
|
|
6
|
+
const createWrapper = (propsData = {}) => {
|
|
7
|
+
return shallowMount(Certificate, { propsData });
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
describe('networking.k8s.io.ingress/Certificate.vue', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('initializes with default values', () => {
|
|
16
|
+
const wrapper = createWrapper();
|
|
17
|
+
|
|
18
|
+
expect(wrapper.vm.hosts).toStrictEqual(['']);
|
|
19
|
+
expect(wrapper.vm.secretVal).toStrictEqual(DEFAULT_CERT_VALUE);
|
|
20
|
+
expect(wrapper.vm.secretName).toStrictEqual(DEFAULT_CERT_VALUE);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('initializes with provided props', () => {
|
|
24
|
+
const value = { hosts: ['host1', 'host2'], secretName: 'some-secret' };
|
|
25
|
+
const certs = ['cert1', 'cert2'];
|
|
26
|
+
const rules = { host: ['rule1'] };
|
|
27
|
+
const wrapper = createWrapper({
|
|
28
|
+
value, certs, rules
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(wrapper.vm.hosts).toStrictEqual(['host1', 'host2']);
|
|
32
|
+
expect(wrapper.vm.secretVal).toStrictEqual('some-secret');
|
|
33
|
+
expect(wrapper.vm.secretName).toStrictEqual('some-secret');
|
|
34
|
+
expect(wrapper.vm.certs).toStrictEqual(certs);
|
|
35
|
+
expect(wrapper.vm.rules).toStrictEqual(rules);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('emits update:value when update is called', async() => {
|
|
39
|
+
const wrapper = createWrapper();
|
|
40
|
+
|
|
41
|
+
wrapper.vm.hosts = ['host1'];
|
|
42
|
+
wrapper.vm.secretVal = 'cert1';
|
|
43
|
+
wrapper.vm.update();
|
|
44
|
+
await wrapper.vm.$nextTick();
|
|
45
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
46
|
+
expect(wrapper.emitted('update:value')[0][0]).toStrictEqual({
|
|
47
|
+
hosts: ['host1'],
|
|
48
|
+
secretName: 'cert1',
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('sets secretName to null when secretVal is DEFAULT_CERT_VALUE', async() => {
|
|
53
|
+
const wrapper = createWrapper();
|
|
54
|
+
|
|
55
|
+
wrapper.vm.hosts = ['host1'];
|
|
56
|
+
wrapper.vm.secretVal = DEFAULT_CERT_VALUE;
|
|
57
|
+
wrapper.vm.update();
|
|
58
|
+
await wrapper.vm.$nextTick();
|
|
59
|
+
expect(wrapper.emitted('update:value')[0][0].secretName).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('updates secretVal when onSecretInput is called with an object', async() => {
|
|
63
|
+
const wrapper = createWrapper();
|
|
64
|
+
const newCert = { label: 'cert1', value: 'cert1' };
|
|
65
|
+
|
|
66
|
+
wrapper.vm.onSecretInput(newCert);
|
|
67
|
+
await wrapper.vm.$nextTick();
|
|
68
|
+
expect(wrapper.vm.secretVal).toStrictEqual('cert1');
|
|
69
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
70
|
+
expect(wrapper.emitted('update:value')[0][0].secretName).toStrictEqual('cert1');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('updates secretVal when onSecretInput is called with a string', async() => {
|
|
74
|
+
const wrapper = createWrapper();
|
|
75
|
+
|
|
76
|
+
wrapper.vm.onSecretInput('cert1');
|
|
77
|
+
await wrapper.vm.$nextTick();
|
|
78
|
+
expect(wrapper.vm.secretVal).toStrictEqual('cert1');
|
|
79
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
80
|
+
expect(wrapper.emitted('update:value')[0][0].secretName).toStrictEqual('cert1');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('updates hosts when onHostsInput is called', async() => {
|
|
84
|
+
const wrapper = createWrapper();
|
|
85
|
+
|
|
86
|
+
wrapper.vm.onHostsInput(['host1', 'host2']);
|
|
87
|
+
await wrapper.vm.$nextTick();
|
|
88
|
+
expect(wrapper.vm.hosts).toStrictEqual(['host1', 'host2']);
|
|
89
|
+
expect(wrapper.emitted('update:value')).toBeTruthy();
|
|
90
|
+
expect(wrapper.emitted('update:value')[0][0].hosts).toStrictEqual(['host1', 'host2']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('computes certsWithDefault correctly', () => {
|
|
94
|
+
const certs = ['cert1', 'cert2'];
|
|
95
|
+
const wrapper = createWrapper({ certs });
|
|
96
|
+
const expectedCerts = [
|
|
97
|
+
{ label: '%ingress.certificates.defaultCertLabel%', value: DEFAULT_CERT_VALUE },
|
|
98
|
+
{ label: 'cert1', value: 'cert1' },
|
|
99
|
+
{ label: 'cert2', value: 'cert2' },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
expect(wrapper.vm.certsWithDefault).toStrictEqual(expectedCerts);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('returns warning status for non-existent certificate', () => {
|
|
106
|
+
const wrapper = createWrapper({
|
|
107
|
+
certs: ['cert1', 'cert2'],
|
|
108
|
+
value: { hosts: [''], secretName: 'non-existent-cert' },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(wrapper.vm.certificateStatus).toStrictEqual('warning');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('returns null status for existing certificate', () => {
|
|
115
|
+
const wrapper = createWrapper({
|
|
116
|
+
certs: ['cert1', 'cert2'],
|
|
117
|
+
value: { hosts: [''], secretName: 'cert1' },
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(wrapper.vm.certificateStatus).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns null status for default certificate', () => {
|
|
124
|
+
const wrapper = createWrapper({
|
|
125
|
+
certs: ['cert1', 'cert2'],
|
|
126
|
+
value: { hosts: [''], secretName: null },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(wrapper.vm.certificateStatus).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('returns correct tooltip for non-existent certificate', () => {
|
|
133
|
+
const wrapper = createWrapper({
|
|
134
|
+
certs: ['cert1', 'cert2'],
|
|
135
|
+
value: { hosts: [''], secretName: 'non-existent-cert' },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(wrapper.vm.certificateTooltip).toStrictEqual('%ingress.certificates.certificate.doesntExist%');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('returns null tooltip for existing certificate', () => {
|
|
142
|
+
const wrapper = createWrapper({
|
|
143
|
+
certs: ['cert1', 'cert2'],
|
|
144
|
+
value: { hosts: [''], secretName: 'cert1' },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(wrapper.vm.certificateTooltip).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('watches value prop changes', async() => {
|
|
151
|
+
const wrapper = createWrapper({ value: { hosts: ['host1'], secretName: 'cert1' } });
|
|
152
|
+
|
|
153
|
+
await wrapper.setProps({ value: { hosts: ['host2'], secretName: 'cert2' } });
|
|
154
|
+
expect(wrapper.vm.hosts).toStrictEqual(['host2']);
|
|
155
|
+
expect(wrapper.vm.secretVal).toStrictEqual('cert2');
|
|
156
|
+
expect(wrapper.vm.secretName).toStrictEqual('cert2');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('handles null secretName in value prop', async() => {
|
|
160
|
+
const wrapper = createWrapper({ value: { hosts: ['host1'], secretName: null } });
|
|
161
|
+
|
|
162
|
+
expect(wrapper.vm.secretVal).toStrictEqual(DEFAULT_CERT_VALUE);
|
|
163
|
+
expect(wrapper.vm.secretName).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -240,11 +240,12 @@ describe('component: rke2', () => {
|
|
|
240
240
|
|
|
241
241
|
// we need to mock the "save" method from the create-edit-view-mixin
|
|
242
242
|
// otherwise we get console errors
|
|
243
|
-
jest.spyOn(wrapper.vm, 'save').mockImplementation();
|
|
243
|
+
// jest.spyOn(wrapper.vm, 'save').mockImplementation();
|
|
244
244
|
|
|
245
245
|
await wrapper.vm._doSaveOverride(jest.fn());
|
|
246
|
+
const chartKey = wrapper.vm.chartVersionKey(HARVESTER_CLOUD_PROVIDER);
|
|
246
247
|
|
|
247
|
-
const cloudConfigPath = get(wrapper.vm.
|
|
248
|
+
const cloudConfigPath = get(wrapper.vm.userChartValues, `${ chartKey }.cloudConfigPath`);
|
|
248
249
|
|
|
249
250
|
expect(cloudConfigPath).toStrictEqual('my-k8s-distro-path/etc/config-files/cloud-provider-config');
|
|
250
251
|
});
|
|
@@ -7,6 +7,7 @@ import CreateEditView from '@shell/mixins/create-edit-view';
|
|
|
7
7
|
import FormValidation from '@shell/mixins/form-validation';
|
|
8
8
|
import { normalizeName } from '@shell/utils/kube';
|
|
9
9
|
import AccountAccess from '@shell/components/google/AccountAccess.vue';
|
|
10
|
+
import { handleConflict } from '@shell/plugins/dashboard-store/normalize';
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
13
|
CAPI,
|
|
@@ -226,6 +227,7 @@ export default {
|
|
|
226
227
|
allPSAs: [],
|
|
227
228
|
credentialId: '',
|
|
228
229
|
credential: null,
|
|
230
|
+
initialMachinePoolsValues: {},
|
|
229
231
|
machinePools: null,
|
|
230
232
|
rke2Versions: null,
|
|
231
233
|
k3sVersions: null,
|
|
@@ -856,6 +858,9 @@ export default {
|
|
|
856
858
|
set(newValue) {
|
|
857
859
|
this.$emit('update:value', newValue);
|
|
858
860
|
}
|
|
861
|
+
},
|
|
862
|
+
hideFooter() {
|
|
863
|
+
return this.needCredential && !this.credential;
|
|
859
864
|
}
|
|
860
865
|
},
|
|
861
866
|
|
|
@@ -946,6 +951,7 @@ export default {
|
|
|
946
951
|
this.registerBeforeHook(this.setRegistryConfig, 'set-registry-config');
|
|
947
952
|
this.registerBeforeHook(this.handleVsphereCpiSecret, 'sync-vsphere-cpi');
|
|
948
953
|
this.registerBeforeHook(this.handleVsphereCsiSecret, 'sync-vsphere-csi');
|
|
954
|
+
this.registerBeforeHook(this.setHarvesterChartValues, 'set-harvester-chart-values');
|
|
949
955
|
this.registerAfterHook(this.cleanupMachinePools, 'cleanup-machine-pools');
|
|
950
956
|
this.registerAfterHook(this.saveRoleBindings, 'save-role-bindings');
|
|
951
957
|
|
|
@@ -1214,7 +1220,7 @@ export default {
|
|
|
1214
1220
|
// @TODO what if the pool is missing?
|
|
1215
1221
|
const id = `pool${ ++this.lastIdx }`;
|
|
1216
1222
|
|
|
1217
|
-
|
|
1223
|
+
const poolData = {
|
|
1218
1224
|
id,
|
|
1219
1225
|
remove: false,
|
|
1220
1226
|
create: false,
|
|
@@ -1222,7 +1228,15 @@ export default {
|
|
|
1222
1228
|
pool: clone(pool),
|
|
1223
1229
|
config: config ? await this.$store.dispatch('management/clone', { resource: config }) : null,
|
|
1224
1230
|
configMissing
|
|
1225
|
-
}
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
// add data to machine pools array
|
|
1234
|
+
out.push(poolData);
|
|
1235
|
+
|
|
1236
|
+
// but we also store the initial data so that we can handle conflicts
|
|
1237
|
+
if (poolData?.config?.id) {
|
|
1238
|
+
this.initialMachinePoolsValues[poolData.config.id] = structuredClone(poolData.config);
|
|
1239
|
+
}
|
|
1226
1240
|
}
|
|
1227
1241
|
}
|
|
1228
1242
|
|
|
@@ -1317,17 +1331,25 @@ export default {
|
|
|
1317
1331
|
const _latestConfig = await this.$store.dispatch('management/request', { url: `/v1/${ machinePool.config.type }s/${ machinePool.config.id }` });
|
|
1318
1332
|
const latestConfig = await this.$store.dispatch('management/create', _latestConfig);
|
|
1319
1333
|
|
|
1320
|
-
const
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
|
|
1334
|
+
const _initialMachinePoolValue = this.initialMachinePoolsValues[machinePool?.config?.id] || {};
|
|
1335
|
+
const initialMachinePoolValue = await this.$store.dispatch('management/create', _initialMachinePoolValue);
|
|
1336
|
+
|
|
1337
|
+
// if there's the initial machine pool config, we are in a good position to apply the handleConflict function
|
|
1338
|
+
// to deal with out-of-sync data between machinePools configs. This also mutates the data inside machinePool.config through object reference
|
|
1339
|
+
const conflict = await handleConflict(
|
|
1340
|
+
initialMachinePoolValue,
|
|
1341
|
+
machinePool.config,
|
|
1342
|
+
latestConfig,
|
|
1343
|
+
{
|
|
1344
|
+
dispatch: this.$store.dispatch,
|
|
1345
|
+
getters: this.$store.getters
|
|
1346
|
+
},
|
|
1347
|
+
'management'
|
|
1348
|
+
);
|
|
1326
1349
|
|
|
1327
|
-
if
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
machinePool.config = merge(clonedLatestConfig, clonedCurrentConfig);
|
|
1350
|
+
// if there's conflicts, throw Error stops save process and surfaces error to user
|
|
1351
|
+
if (conflict) {
|
|
1352
|
+
throw Error(conflict);
|
|
1331
1353
|
}
|
|
1332
1354
|
}
|
|
1333
1355
|
},
|
|
@@ -1536,41 +1558,7 @@ export default {
|
|
|
1536
1558
|
}
|
|
1537
1559
|
|
|
1538
1560
|
try {
|
|
1539
|
-
const clusterId = get(this.credential, 'decodedData.clusterId') || '';
|
|
1540
|
-
|
|
1541
1561
|
this.applyChartValues(this.value.spec.rkeConfig);
|
|
1542
|
-
|
|
1543
|
-
const isUpgrade = this.isEdit && this.liveValue?.spec?.kubernetesVersion !== this.value?.spec?.kubernetesVersion;
|
|
1544
|
-
|
|
1545
|
-
if (this.agentConfig?.['cloud-provider-name'] === HARVESTER && clusterId && (this.isCreate || isUpgrade)) {
|
|
1546
|
-
const namespace = this.machinePools?.[0]?.config?.vmNamespace;
|
|
1547
|
-
|
|
1548
|
-
const res = await this.$store.dispatch('management/request', {
|
|
1549
|
-
url: `/k8s/clusters/${ clusterId }/v1/harvester/kubeconfig`,
|
|
1550
|
-
method: 'POST',
|
|
1551
|
-
data: {
|
|
1552
|
-
csiClusterRoleName: 'harvesterhci.io:csi-driver',
|
|
1553
|
-
clusterRoleName: 'harvesterhci.io:cloudprovider',
|
|
1554
|
-
namespace,
|
|
1555
|
-
serviceAccountName: this.value.metadata.name,
|
|
1556
|
-
},
|
|
1557
|
-
});
|
|
1558
|
-
|
|
1559
|
-
const kubeconfig = res.data;
|
|
1560
|
-
|
|
1561
|
-
const harvesterKubeconfigSecret = await this.createKubeconfigSecret(kubeconfig);
|
|
1562
|
-
|
|
1563
|
-
this.agentConfig['cloud-provider-config'] = `secret://fleet-default:${ harvesterKubeconfigSecret?.metadata?.name }`;
|
|
1564
|
-
|
|
1565
|
-
if (this.isCreate) {
|
|
1566
|
-
set(this.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.global.cattle.clusterName`, this.value.metadata.name);
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
const distroSubdir = this.value?.spec?.kubernetesVersion?.includes('k3s') ? DEFAULT_SUBDIRS.K8S_DISTRO_K3S : DEFAULT_SUBDIRS.K8S_DISTRO_RKE2;
|
|
1570
|
-
const distroRoot = this.value?.spec?.rkeConfig?.dataDirectories?.k8sDistro?.length ? this.value?.spec?.rkeConfig?.dataDirectories?.k8sDistro : `${ DEFAULT_COMMON_BASE_PATH }/${ distroSubdir }`;
|
|
1571
|
-
|
|
1572
|
-
set(this.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.cloudConfigPath`, `${ distroRoot }/etc/config-files/cloud-provider-config`);
|
|
1573
|
-
}
|
|
1574
1562
|
} catch (err) {
|
|
1575
1563
|
this.errors.push(err);
|
|
1576
1564
|
|
|
@@ -1617,6 +1605,62 @@ export default {
|
|
|
1617
1605
|
}
|
|
1618
1606
|
},
|
|
1619
1607
|
|
|
1608
|
+
async setHarvesterChartValues() {
|
|
1609
|
+
const isHarvester = this.agentConfig?.['cloud-provider-name'] === HARVESTER;
|
|
1610
|
+
|
|
1611
|
+
if (!isHarvester) {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
const clusterId = get(this.credential, 'decodedData.clusterId') || '';
|
|
1616
|
+
const isUpgrade = this.isEdit && this.liveValue?.spec?.kubernetesVersion !== this.value?.spec?.kubernetesVersion;
|
|
1617
|
+
|
|
1618
|
+
if (!this.value?.metadata?.name) {
|
|
1619
|
+
const err = this.t('cluster.harvester.kubeconfigSecret.nameRequired');
|
|
1620
|
+
|
|
1621
|
+
throw new Error(err);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
if (clusterId && (this.isCreate || isUpgrade)) {
|
|
1625
|
+
const namespace = this.machinePools?.[0]?.config?.vmNamespace;
|
|
1626
|
+
|
|
1627
|
+
const res = await this.$store.dispatch('management/request', {
|
|
1628
|
+
url: `/k8s/clusters/${ clusterId }/v1/harvester/kubeconfig`,
|
|
1629
|
+
method: 'POST',
|
|
1630
|
+
data: {
|
|
1631
|
+
csiClusterRoleName: 'harvesterhci.io:csi-driver',
|
|
1632
|
+
clusterRoleName: 'harvesterhci.io:cloudprovider',
|
|
1633
|
+
namespace,
|
|
1634
|
+
serviceAccountName: this.value.metadata.name,
|
|
1635
|
+
},
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
const kubeconfig = res.data;
|
|
1639
|
+
|
|
1640
|
+
const harvesterKubeconfigSecret = await this.createKubeconfigSecret(kubeconfig);
|
|
1641
|
+
|
|
1642
|
+
this.agentConfig['cloud-provider-config'] = `secret://fleet-default:${ harvesterKubeconfigSecret?.metadata?.name }`;
|
|
1643
|
+
|
|
1644
|
+
const harvesterCloudProviderKey = this.chartVersionKey(HARVESTER_CLOUD_PROVIDER);
|
|
1645
|
+
|
|
1646
|
+
if (this.isCreate) {
|
|
1647
|
+
set(this.userChartValues, `'${ harvesterCloudProviderKey }'.global.cattle.clusterName`, this.value.metadata.name);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const distroSubdir = this.value?.spec?.kubernetesVersion?.includes('k3s') ? DEFAULT_SUBDIRS.K8S_DISTRO_K3S : DEFAULT_SUBDIRS.K8S_DISTRO_RKE2;
|
|
1651
|
+
const distroRoot = this.value?.spec?.rkeConfig?.dataDirectories?.k8sDistro?.length ? this.value?.spec?.rkeConfig?.dataDirectories?.k8sDistro : `${ DEFAULT_COMMON_BASE_PATH }/${ distroSubdir }`;
|
|
1652
|
+
|
|
1653
|
+
set(this.userChartValues, `'${ harvesterCloudProviderKey }'.cloudConfigPath`, `${ distroRoot }/etc/config-files/cloud-provider-config`);
|
|
1654
|
+
}
|
|
1655
|
+
} catch (e) {
|
|
1656
|
+
const cause = e.errors ? e.errors.join('; ') : e?.message;
|
|
1657
|
+
const msg = this.t('cluster.harvester.kubeconfigSecret.error', { err: cause });
|
|
1658
|
+
|
|
1659
|
+
this.errors.push(msg);
|
|
1660
|
+
throw new Error(msg);
|
|
1661
|
+
}
|
|
1662
|
+
},
|
|
1663
|
+
|
|
1620
1664
|
// create a secret to reference the harvester cluster kubeconfig in rkeConfig
|
|
1621
1665
|
async createKubeconfigSecret(kubeconfig = '') {
|
|
1622
1666
|
const clusterName = this.value.metadata.name;
|
|
@@ -1893,6 +1937,7 @@ export default {
|
|
|
1893
1937
|
|
|
1894
1938
|
charts.forEach((name) => {
|
|
1895
1939
|
const key = this.chartVersionKey(name);
|
|
1940
|
+
|
|
1896
1941
|
const userValues = this.userChartValues[key];
|
|
1897
1942
|
|
|
1898
1943
|
if (userValues) {
|
|
@@ -2174,7 +2219,10 @@ export default {
|
|
|
2174
2219
|
@error="e=>errors.push(e)"
|
|
2175
2220
|
@cancel-credential="cancelCredential"
|
|
2176
2221
|
/>
|
|
2177
|
-
<div
|
|
2222
|
+
<div
|
|
2223
|
+
v-else
|
|
2224
|
+
class="authenticated"
|
|
2225
|
+
>
|
|
2178
2226
|
<SelectCredential
|
|
2179
2227
|
v-if="needCredential"
|
|
2180
2228
|
v-model:value="credentialId"
|
|
@@ -2568,7 +2616,7 @@ export default {
|
|
|
2568
2616
|
/>
|
|
2569
2617
|
</div>
|
|
2570
2618
|
<template
|
|
2571
|
-
v-if="
|
|
2619
|
+
v-if="hideFooter"
|
|
2572
2620
|
#form-footer
|
|
2573
2621
|
>
|
|
2574
2622
|
<div><!-- Hide the outer footer --></div>
|
|
@@ -2577,6 +2625,12 @@ export default {
|
|
|
2577
2625
|
</template>
|
|
2578
2626
|
|
|
2579
2627
|
<style lang="scss" scoped>
|
|
2628
|
+
.authenticated {
|
|
2629
|
+
display:flex;
|
|
2630
|
+
flex-direction: column;
|
|
2631
|
+
flex-grow: 1;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2580
2634
|
.min-height {
|
|
2581
2635
|
min-height: 40em;
|
|
2582
2636
|
}
|