@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.
Files changed (97) hide show
  1. package/assets/styles/base/_variables.scss +17 -11
  2. package/assets/styles/themes/_dark.scss +2 -0
  3. package/assets/styles/themes/_light.scss +8 -2
  4. package/assets/translations/en-us.yaml +26 -4
  5. package/components/CodeMirror.vue +1 -1
  6. package/components/Drawer/Chrome.vue +0 -1
  7. package/components/Drawer/ResourceDetailDrawer/__tests__/composables.test.ts +26 -2
  8. package/components/Drawer/ResourceDetailDrawer/composables.ts +4 -1
  9. package/components/Drawer/ResourceDetailDrawer/index.vue +1 -0
  10. package/components/Loading.vue +1 -1
  11. package/components/PaginatedResourceTable.vue +46 -1
  12. package/components/PromptRestore.vue +22 -44
  13. package/components/Resource/Detail/Metadata/IdentifyingInformation/composable.ts +10 -2
  14. package/components/Resource/Detail/Metadata/IdentifyingInformation/identifying-fields.ts +21 -2
  15. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +8 -1
  16. package/components/Resource/Detail/Metadata/KeyValue.vue +12 -10
  17. package/components/Resource/Detail/Metadata/Rectangle.vue +3 -1
  18. package/components/Resource/Detail/SpacedRow.vue +1 -1
  19. package/components/Resource/Detail/TitleBar/composables.ts +4 -3
  20. package/components/Resource/Detail/TitleBar/index.vue +2 -2
  21. package/components/ResourceDetail/Masthead/legacy.vue +1 -1
  22. package/components/ResourceDetail/index.vue +5 -3
  23. package/components/ResourceList/index.vue +1 -0
  24. package/components/ResourceTable.vue +6 -1
  25. package/components/ResourceYaml.vue +1 -1
  26. package/components/RichTranslation.vue +106 -0
  27. package/components/SlideInPanelManager.vue +3 -7
  28. package/components/SortableTable/index.vue +1 -1
  29. package/components/SortableTable/selection.js +0 -1
  30. package/components/Tabbed/index.vue +6 -1
  31. package/components/__tests__/PromptRestore.test.ts +1 -65
  32. package/components/__tests__/RichTranslation.test.ts +115 -0
  33. package/components/fleet/dashboard/ResourcePanel.vue +2 -1
  34. package/components/form/FileImageSelector.vue +1 -1
  35. package/components/form/NameNsDescription.vue +1 -0
  36. package/components/form/Networking.vue +24 -19
  37. package/components/form/ResourceLabeledSelect.vue +4 -3
  38. package/components/form/SelectOrCreateAuthSecret.vue +6 -3
  39. package/components/form/__tests__/Networking.test.ts +116 -0
  40. package/components/formatter/PodImages.vue +1 -1
  41. package/components/formatter/__tests__/LiveDate.test.ts +10 -2
  42. package/components/google/AccountAccess.vue +44 -46
  43. package/components/nav/Group.vue +4 -1
  44. package/composables/resources.ts +2 -2
  45. package/config/labels-annotations.js +2 -0
  46. package/config/pagination-table-headers.js +8 -1
  47. package/config/product/explorer.js +27 -2
  48. package/config/product/manager.js +0 -1
  49. package/config/query-params.js +10 -0
  50. package/config/router/routes.js +21 -1
  51. package/config/system-namespaces.js +1 -1
  52. package/config/table-headers.js +30 -1
  53. package/config/types.js +1 -1
  54. package/config/version.js +1 -1
  55. package/detail/provisioning.cattle.io.cluster.vue +3 -47
  56. package/dialog/RotateEncryptionKeyDialog.vue +10 -30
  57. package/edit/auth/ldap/__tests__/config.test.ts +14 -0
  58. package/edit/auth/ldap/config.vue +24 -0
  59. package/edit/compliance.cattle.io.clusterscan.vue +1 -1
  60. package/edit/configmap.vue +4 -1
  61. package/edit/networking.k8s.io.ingress/Certificate.vue +12 -12
  62. package/edit/networking.k8s.io.ingress/__tests__/Certificate.test.ts +165 -0
  63. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +3 -2
  64. package/edit/provisioning.cattle.io.cluster/rke2.vue +102 -48
  65. package/edit/provisioning.cattle.io.cluster/tabs/Basics.vue +22 -13
  66. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +2 -2
  67. package/edit/secret/basic.vue +1 -0
  68. package/edit/secret/index.vue +126 -15
  69. package/list/projectsecret.vue +345 -0
  70. package/list/secret.vue +109 -0
  71. package/mixins/__tests__/brand.spec.ts +2 -2
  72. package/mixins/create-edit-view/impl.js +10 -1
  73. package/mixins/resource-fetch-api-pagination.js +9 -9
  74. package/mixins/resource-fetch.js +3 -1
  75. package/models/cluster.x-k8s.io.machinedeployment.js +11 -2
  76. package/models/fleet.cattle.io.cluster.js +2 -2
  77. package/models/provisioning.cattle.io.cluster.js +24 -28
  78. package/models/secret.js +157 -2
  79. package/package.json +2 -2
  80. package/pages/c/_cluster/apps/charts/index.vue +46 -35
  81. package/pages/c/_cluster/explorer/projectsecret.vue +34 -0
  82. package/pages/c/_cluster/fleet/index.vue +0 -1
  83. package/pages/explorer/resource/detail/projectsecret.vue +9 -0
  84. package/pages/explorer/resource/detail/secret.vue +18 -5
  85. package/plugins/dashboard-store/__tests__/normalize.test.ts +223 -0
  86. package/plugins/dashboard-store/__tests__/resource-class.test.ts +191 -0
  87. package/plugins/dashboard-store/__tests__/utils/normalize-usecases.ts +1526 -0
  88. package/plugins/dashboard-store/normalize.js +29 -17
  89. package/plugins/dashboard-store/resource-class.js +52 -17
  90. package/plugins/steve/steve-pagination-utils.ts +14 -3
  91. package/types/kube/kube-api.ts +12 -0
  92. package/types/shell/index.d.ts +616 -558
  93. package/types/store/pagination.types.ts +16 -6
  94. package/utils/__tests__/create-yaml.test.ts +235 -0
  95. package/utils/create-yaml.js +103 -9
  96. package/utils/pagination-utils.ts +18 -0
  97. package/models/etcdbackup.js +0 -45
@@ -1,5 +1,5 @@
1
1
  <script>
2
- import { SNAPSHOT, NORMAN } from '@shell/config/types';
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
- if ( this.cluster.isRke1) {
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
- etcdBackups = etcdBackups.filter((backup) => backup.clusterId === this.cluster.metadata.name);
69
+ etcdBackups = etcdBackups.filter((backup) => backup.clusterId === this.cluster.id);
71
70
 
72
- return etcdBackups;
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
- if (isRke2) {
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
- this.close(buttonDone);
108
- } else {
109
- await this.cluster.mgmt.doAction('rotateEncryptionKey');
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
- this.close(buttonDone);
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: 'rancher-compliance-system/default-clusterscanprofiles' });
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;
@@ -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
- await this.value.saveYaml(yaml);
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.secretName;
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.secretName = e && typeof e === 'object' ? e.label : e;
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="secretName"
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.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.cloudConfigPath`);
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
- out.push({
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 clonedCurrentConfig = await this.$store.dispatch('management/clone', { resource: machinePool.config });
1321
- const clonedLatestConfig = await this.$store.dispatch('management/clone', { resource: latestConfig });
1322
-
1323
- // We don't allow the user to edit any of the fields in metadata from the UI so it's safe to override it with the
1324
- // metadata defined by the latest backend value. This is primarily used to ensure the resourceVersion is up to date.
1325
- delete clonedCurrentConfig.metadata;
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 (this.provider === VMWARE_VSPHERE || this.provider === GOOGLE) {
1328
- machinePool.config = mergeWithReplace(clonedLatestConfig, clonedCurrentConfig, { mutateOriginal: true });
1329
- } else {
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 v-else>
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="needCredential && !credentialId"
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
  }