@rancher/shell 0.3.16 → 0.3.18

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 (174) hide show
  1. package/assets/images/wechat-qr-code.jpg +0 -0
  2. package/assets/translations/en-us.yaml +75 -16
  3. package/assets/translations/zh-hans.yaml +151 -15
  4. package/chart/__tests__/S3.test.ts +50 -0
  5. package/chart/rancher-backup/S3.vue +21 -0
  6. package/chart/rancher-backup/index.vue +4 -0
  7. package/components/AsyncButton.vue +1 -1
  8. package/components/CommunityLinks.vue +1 -0
  9. package/components/FileDiff.vue +92 -85
  10. package/components/Inactivity.vue +10 -0
  11. package/components/LazyImage.vue +2 -2
  12. package/components/PromptRestore.vue +7 -5
  13. package/components/ResourceDetail/Masthead.vue +1 -1
  14. package/components/ResourceDetail/index.vue +8 -14
  15. package/components/ResourceList/index.vue +1 -1
  16. package/components/ResourceTable.vue +50 -2
  17. package/components/YamlEditor.vue +1 -0
  18. package/components/__tests__/PromptRestore.test.ts +72 -0
  19. package/components/auth/AzureWarning.vue +1 -1
  20. package/components/auth/RoleDetailEdit.vue +1 -0
  21. package/components/fleet/FleetResources.vue +3 -64
  22. package/components/form/FileImageSelector.vue +9 -0
  23. package/components/form/FileSelector.vue +2 -1
  24. package/components/form/MatchExpressions.vue +1 -3
  25. package/components/form/NameNsDescription.vue +28 -12
  26. package/components/form/NodeAffinity.vue +2 -2
  27. package/components/form/PodAffinity.vue +2 -2
  28. package/components/form/ResourceTabs/index.vue +8 -2
  29. package/components/form/Select.vue +16 -0
  30. package/components/form/__tests__/FileImageSelector.test.ts +42 -0
  31. package/components/form/__tests__/FileSelector.test.ts +76 -0
  32. package/components/form/__tests__/NodeAffinity.test.ts +38 -0
  33. package/components/form/__tests__/PodAffinity.test.ts +46 -0
  34. package/components/formatter/ClusterLink.vue +8 -4
  35. package/components/formatter/ClusterProvider.vue +3 -1
  36. package/components/formatter/ImageName.vue +23 -0
  37. package/components/formatter/PodImages.vue +7 -1
  38. package/components/formatter/__tests__/ClusterLink.test.ts +101 -0
  39. package/components/formatter/__tests__/ClusterProvider.test.ts +24 -0
  40. package/components/nav/Header.vue +2 -2
  41. package/components/nav/WindowManager/ContainerShell.vue +60 -36
  42. package/components/nav/WindowManager/__tests__/ContainerShell.test.ts +561 -0
  43. package/config/__test__/home-links.test.ts +62 -0
  44. package/config/home-links.js +15 -3
  45. package/config/labels-annotations.js +7 -2
  46. package/config/persistentVolume.ts +108 -0
  47. package/config/product/manager.js +5 -1
  48. package/config/router.js +0 -4
  49. package/config/settings.ts +4 -0
  50. package/config/table-headers.js +6 -5
  51. package/config/types.js +2 -0
  52. package/config/uiplugins.js +50 -5
  53. package/core/plugin-helpers.js +39 -15
  54. package/core/plugin.ts +9 -0
  55. package/core/plugins.js +1 -1
  56. package/core/types-provisioning.ts +253 -0
  57. package/core/types.ts +21 -3
  58. package/detail/autoscaling.horizontalpodautoscaler/index.vue +50 -1
  59. package/detail/fleet.cattle.io.gitrepo.vue +10 -2
  60. package/detail/node.vue +6 -6
  61. package/detail/pod.vue +38 -9
  62. package/detail/provisioning.cattle.io.cluster.vue +46 -7
  63. package/detail/workload/index.vue +49 -18
  64. package/edit/__tests__/fleet.cattle.io.gitrepo.test.ts +62 -0
  65. package/edit/__tests__/ui.cattle.io.navlink.test.ts +110 -0
  66. package/edit/auth/github.vue +1 -0
  67. package/edit/autoscaling.horizontalpodautoscaler/hpa-scaling-rule.vue +130 -0
  68. package/edit/autoscaling.horizontalpodautoscaler/index.vue +79 -0
  69. package/edit/fleet.cattle.io.clustergroup.vue +14 -3
  70. package/edit/fleet.cattle.io.gitrepo.vue +18 -1
  71. package/edit/namespace.vue +9 -1
  72. package/edit/networking.k8s.io.ingress/RulePath.vue +0 -2
  73. package/edit/persistentvolume/__tests__/persistentvolume.test.ts +82 -0
  74. package/edit/persistentvolume/index.vue +2 -1
  75. package/edit/persistentvolume/plugins/csi.vue +3 -1
  76. package/edit/persistentvolume/plugins/longhorn.vue +12 -12
  77. package/edit/provisioning.cattle.io.cluster/AgentConfiguration.vue +1 -30
  78. package/edit/provisioning.cattle.io.cluster/RegistryConfigs.vue +15 -11
  79. package/edit/provisioning.cattle.io.cluster/__tests__/rke2.test.ts +79 -1
  80. package/edit/provisioning.cattle.io.cluster/index.vue +53 -1
  81. package/edit/provisioning.cattle.io.cluster/rke2.vue +335 -151
  82. package/edit/storage.k8s.io.storageclass/index.vue +1 -2
  83. package/edit/ui.cattle.io.navlink.vue +213 -186
  84. package/initialize/App.js +3 -13
  85. package/initialize/layouts.ts +26 -0
  86. package/layouts/default.vue +1 -1
  87. package/list/group.principal.vue +1 -1
  88. package/list/provisioning.cattle.io.cluster.vue +8 -1
  89. package/middleware/authenticated.js +101 -5
  90. package/mixins/brand.js +39 -3
  91. package/mixins/child-hook.js +2 -2
  92. package/mixins/create-edit-view/impl.js +4 -4
  93. package/models/chart.js +1 -1
  94. package/models/fleet.cattle.io.cluster.js +33 -4
  95. package/models/fleet.cattle.io.gitrepo.js +113 -38
  96. package/models/management.cattle.io.kontainerdriver.js +14 -0
  97. package/models/persistentvolume.js +2 -111
  98. package/models/pod.js +30 -0
  99. package/models/provisioning.cattle.io.cluster.js +9 -1
  100. package/models/rke.cattle.io.etcdsnapshot.js +10 -7
  101. package/package.json +2 -2
  102. package/pages/about.vue +8 -2
  103. package/pages/auth/login.vue +1 -1
  104. package/pages/auth/logout.vue +11 -3
  105. package/pages/c/_cluster/apps/charts/index.vue +5 -2
  106. package/pages/c/_cluster/apps/charts/install.vue +5 -0
  107. package/pages/c/_cluster/auth/group.principal/assign-edit.vue +1 -1
  108. package/pages/c/_cluster/auth/roles/index.vue +1 -1
  109. package/pages/c/_cluster/explorer/index.vue +2 -11
  110. package/pages/c/_cluster/manager/cloudCredential/_id.vue +0 -1
  111. package/pages/c/_cluster/manager/cloudCredential/create.vue +0 -1
  112. package/pages/c/_cluster/settings/brand.vue +11 -8
  113. package/pages/c/_cluster/uiplugins/AddExtensionRepos.vue +177 -0
  114. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +19 -3
  115. package/pages/c/_cluster/uiplugins/RemoveUIPlugins.vue +90 -21
  116. package/pages/c/_cluster/uiplugins/SetupUIPlugins.vue +107 -37
  117. package/pages/c/_cluster/uiplugins/index.vue +160 -44
  118. package/pages/docs/_doc.vue +9 -3
  119. package/pages/home.vue +6 -6
  120. package/pages/support/index.vue +10 -4
  121. package/pkg/auto-import.js +1 -1
  122. package/plugins/clean-tooltip-directive.js +1 -1
  123. package/plugins/dashboard-store/__tests__/actions.spec.ts +165 -0
  124. package/plugins/dashboard-store/__tests__/getters.spec.ts +100 -0
  125. package/plugins/dashboard-store/__tests__/{mutations.spec.js → mutations.spec.ts} +2 -2
  126. package/plugins/dashboard-store/actions.js +1 -1
  127. package/plugins/dashboard-store/resource-class.js +39 -2
  128. package/plugins/plugin.js +9 -1
  129. package/plugins/steve/__tests__/getters.spec.ts +93 -0
  130. package/plugins/steve/getters.js +21 -1
  131. package/plugins/steve/subscribe.js +1 -3
  132. package/rancher-components/BadgeState/BadgeState.vue +5 -1
  133. package/rancher-components/Banner/Banner.test.ts +51 -1
  134. package/rancher-components/Banner/Banner.vue +134 -53
  135. package/rancher-components/Card/Card.test.ts +37 -0
  136. package/rancher-components/Card/Card.vue +24 -7
  137. package/rancher-components/Form/Checkbox/Checkbox.test.ts +20 -29
  138. package/rancher-components/Form/Checkbox/Checkbox.vue +45 -20
  139. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +2 -8
  140. package/rancher-components/Form/LabeledInput/LabeledInput.vue +22 -10
  141. package/rancher-components/Form/Radio/RadioButton.test.ts +31 -0
  142. package/rancher-components/Form/Radio/RadioButton.vue +30 -13
  143. package/rancher-components/Form/Radio/RadioGroup.vue +26 -7
  144. package/rancher-components/Form/TextArea/TextAreaAutoGrow.vue +7 -6
  145. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.test.ts +25 -38
  146. package/rancher-components/Form/ToggleSwitch/ToggleSwitch.vue +23 -11
  147. package/rancher-components/LabeledTooltip/LabeledTooltip.vue +19 -5
  148. package/rancher-components/StringList/StringList.test.ts +453 -49
  149. package/rancher-components/StringList/StringList.vue +44 -26
  150. package/scripts/extension/publish +2 -2
  151. package/scripts/typegen.sh +11 -2
  152. package/server/server-middleware.js +4 -12
  153. package/store/index.js +14 -3
  154. package/store/prefs.js +0 -3
  155. package/store/store-types.js +2 -0
  156. package/store/type-map.js +17 -29
  157. package/types/api.d.ts +1 -0
  158. package/types/fleet.d.ts +1 -0
  159. package/types/shell/index.d.ts +931 -85
  160. package/types/userPreferences.d.ts +1 -1
  161. package/utils/__mocks__/socket.js +21 -0
  162. package/utils/grafana.js +23 -11
  163. package/utils/kube.js +9 -0
  164. package/utils/object.js +27 -0
  165. package/utils/selector.js +2 -1
  166. package/utils/settings.ts +2 -2
  167. package/utils/validators/formRules/index.ts +3 -3
  168. package/vue.config.js +3 -2
  169. package/components/.DS_Store +0 -0
  170. package/components/__tests__/.DS_Store +0 -0
  171. package/creators/pkg/package-lock.json +0 -37
  172. package/pages/safeMode.vue +0 -17
  173. package/plugins/steve/urloptions.js +0 -47
  174. package/yarn-error.log +0 -196
@@ -6,10 +6,12 @@ import merge from 'lodash/merge';
6
6
  import { mapGetters } from 'vuex';
7
7
  import CreateEditView from '@shell/mixins/create-edit-view';
8
8
  import FormValidation from '@shell/mixins/form-validation';
9
+ import { normalizeName } from '@shell/utils/kube';
9
10
 
10
11
  import {
11
12
  CAPI,
12
13
  MANAGEMENT,
14
+ NAMESPACE,
13
15
  NORMAN,
14
16
  SCHEMA,
15
17
  DEFAULT_WORKSPACE,
@@ -42,7 +44,7 @@ import { LabeledInput } from '@components/Form/LabeledInput';
42
44
  import LabeledSelect from '@shell/components/form/LabeledSelect';
43
45
  import Loading from '@shell/components/Loading';
44
46
  import MatchExpressions from '@shell/components/form/MatchExpressions';
45
- import NameNsDescription, { normalizeName } from '@shell/components/form/NameNsDescription';
47
+ import NameNsDescription from '@shell/components/form/NameNsDescription';
46
48
  import { RadioGroup } from '@components/Form/Radio';
47
49
  import Tab from '@shell/components/Tabbed/Tab';
48
50
  import Tabbed from '@shell/components/Tabbed';
@@ -69,7 +71,9 @@ import S3Config from './S3Config';
69
71
  import SelectCredential from './SelectCredential';
70
72
  import AdvancedSection from '@shell/components/AdvancedSection.vue';
71
73
  import { ELEMENTAL_SCHEMA_IDS, KIND, ELEMENTAL_CLUSTER_PROVIDER } from '../../config/elemental-types';
72
- import AgentConfiguration, { cleanAgentConfiguration } from './AgentConfiguration';
74
+ import AgentConfiguration from './AgentConfiguration';
75
+ import { getApplicableExtensionEnhancements } from '@shell/core/plugin-helpers';
76
+ import { ExtensionPoint, TabLocation } from '@shell/core/types';
73
77
 
74
78
  const PUBLIC = 'public';
75
79
  const PRIVATE = 'private';
@@ -154,122 +158,9 @@ export default {
154
158
  },
155
159
 
156
160
  async fetch() {
157
- // Check presence of PSP in RKE2, which is where we show the templates
158
- this.psps = await this.checkPsps();
159
-
160
- if ( !this.rke2Versions ) {
161
- const hash = {
162
- rke2Versions: this.$store.dispatch('management/request', { url: '/v1-rke2-release/releases' }),
163
- k3sVersions: this.$store.dispatch('management/request', { url: '/v1-k3s-release/releases' }),
164
- };
165
-
166
- if ( this.$store.getters['management/canList'](MANAGEMENT.POD_SECURITY_POLICY_TEMPLATE) ) {
167
- hash.allPSPs = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.POD_SECURITY_POLICY_TEMPLATE });
168
- }
169
-
170
- if (this.$store.getters['management/canList'](MANAGEMENT.PSA)) {
171
- hash.allPSAs = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.PSA });
172
- }
173
-
174
- // Get the latest versions from the global settings if possible
175
- const globalSettings = await this.$store.getters['management/all'](MANAGEMENT.SETTING) || [];
176
- const defaultRke2Setting = globalSettings.find((setting) => setting.id === 'rke2-default-version') || {};
177
- const defaultK3sSetting = globalSettings.find((setting) => setting.id === 'k3s-default-version') || {};
178
-
179
- let defaultRke2 = defaultRke2Setting?.value || defaultRke2Setting?.default;
180
- let defaultK3s = defaultK3sSetting?.value || defaultK3sSetting?.default;
181
-
182
- // RKE2: Use the channel if we can not get the version from the settings
183
- if (!defaultRke2) {
184
- hash.rke2Channels = this.$store.dispatch('management/request', { url: '/v1-rke2-release/channels' });
185
- }
186
-
187
- // K3S: Use the channel if we can not get the version from the settings
188
- if (!defaultK3s) {
189
- hash.k3sChannels = this.$store.dispatch('management/request', { url: '/v1-k3s-release/channels' });
190
- }
191
-
192
- const res = await allHash(hash);
193
-
194
- this.allPSPs = res.allPSPs || [];
195
- this.allPSAs = res.allPSAs || [];
196
- this.rke2Versions = res.rke2Versions.data || [];
197
- this.k3sVersions = res.k3sVersions.data || [];
198
-
199
- if (!defaultRke2) {
200
- const rke2Channels = res.rke2Channels.data || [];
201
-
202
- defaultRke2 = rke2Channels.find((x) => x.id === 'default')?.latest;
203
- }
204
-
205
- if (!defaultK3s) {
206
- const k3sChannels = res.k3sChannels.data || [];
207
-
208
- defaultK3s = k3sChannels.find((x) => x.id === 'default')?.latest;
209
- }
210
-
211
- if ( !this.rke2Versions.length && !this.k3sVersions.length ) {
212
- throw new Error('No version info found in KDM');
213
- }
214
-
215
- // Store default versions
216
- this.defaultRke2 = defaultRke2;
217
- this.defaultK3s = defaultK3s;
218
- }
219
-
220
- if ( !this.value.spec ) {
221
- set(this.value, 'spec', {});
222
- }
223
-
224
- if ( !this.value.spec.machineSelectorConfig ) {
225
- set(this.value.spec, 'machineSelectorConfig', []);
226
- }
227
-
228
- if ( !this.value.spec.machineSelectorConfig.find((x) => !x.machineLabelSelector) ) {
229
- this.value.spec.machineSelectorConfig.unshift({ config: {} });
230
- }
231
-
232
- if ( this.value.spec.cloudCredentialSecretName ) {
233
- await this.$store.dispatch('rancher/findAll', { type: NORMAN.CLOUD_CREDENTIAL });
234
- this.credentialId = `${ this.value.spec.cloudCredentialSecretName }`;
235
- }
236
-
237
- if ( !this.value.spec.kubernetesVersion ) {
238
- set(this.value.spec, 'kubernetesVersion', this.defaultVersion);
239
- }
240
-
241
- if ( this.rkeConfig.etcd?.s3?.bucket ) {
242
- this.s3Backup = true;
243
- }
244
-
245
- if ( !this.rkeConfig.etcd ) {
246
- set(this.rkeConfig, 'etcd', {
247
- disableSnapshots: false,
248
- s3: null,
249
- snapshotRetention: 5,
250
- snapshotScheduleCron: '0 */5 * * *',
251
- });
252
- } else if (typeof this.rkeConfig.etcd.disableSnapshots === 'undefined') {
253
- const disableSnapshots = !this.rkeConfig.etcd.snapshotRetention && !this.rkeConfig.etcd.snapshotScheduleCron;
254
-
255
- set(this.rkeConfig.etcd, 'disableSnapshots', disableSnapshots);
256
- }
257
-
258
- if ( !this.machinePools ) {
259
- await this.initMachinePools(this.value.spec.rkeConfig.machinePools);
260
- if ( this.mode === _CREATE && !this.machinePools.length ) {
261
- await this.addMachinePool();
262
- }
263
- }
264
-
265
- if ( this.value.spec.defaultPodSecurityPolicyTemplateName === undefined ) {
266
- set(this.value.spec, 'defaultPodSecurityPolicyTemplateName', '');
267
- }
268
-
269
- if ( this.value.spec.defaultPodSecurityAdmissionConfigurationTemplateName === undefined ) {
270
- set(this.value.spec, 'defaultPodSecurityAdmissionConfigurationTemplateName', '');
271
- }
272
-
161
+ this.psps = await this.getPsps();
162
+ await this.fetchRke2Versions();
163
+ await this.initSpecs();
273
164
  await this.initAddons();
274
165
  await this.initRegistry();
275
166
 
@@ -279,17 +170,7 @@ export default {
279
170
  this.userChartValues[key] = value;
280
171
  });
281
172
 
282
- // Ensure we have empty models for the two agent configurations
283
-
284
- // Cluster Agent Configuration
285
- if ( !this.value.spec[CLUSTER_AGENT_CUSTOMIZATION]) {
286
- set(this.value.spec, CLUSTER_AGENT_CUSTOMIZATION, {});
287
- }
288
-
289
- // Fleet Agent Configuration
290
- if ( !this.value.spec[FLEET_AGENT_CUSTOMIZATION] ) {
291
- set(this.value.spec, FLEET_AGENT_CUSTOMIZATION, {});
292
- }
173
+ this.setAgentConfiguration();
293
174
  },
294
175
 
295
176
  data() {
@@ -362,7 +243,9 @@ export default {
362
243
  truncateHostnames: truncateLimit === NETBIOS_TRUNCATION_LENGTH,
363
244
  truncateLimit,
364
245
  busy: false,
365
- machinePoolValidation: {} // map of validation states for each machine pool
246
+ machinePoolValidation: {}, // map of validation states for each machine pool
247
+ allNamespaces: [],
248
+ extensionTabs: getApplicableExtensionEnhancements(this, ExtensionPoint.TAB, TabLocation.CLUSTER_CREATE_RKE2, this.$route, this),
366
249
  };
367
250
  },
368
251
 
@@ -370,6 +253,7 @@ export default {
370
253
  ...mapGetters({ allCharts: 'catalog/charts' }),
371
254
  ...mapGetters(['currentCluster']),
372
255
  ...mapGetters({ features: 'features/get' }),
256
+ ...mapGetters(['namespaces']),
373
257
 
374
258
  PUBLIC: () => PUBLIC,
375
259
  PRIVATE: () => PRIVATE,
@@ -734,6 +618,33 @@ export default {
734
618
  return (this.machinePools || []).filter((x) => !x.remove);
735
619
  },
736
620
 
621
+ /**
622
+ * Extension provider where being provisioned by an extension
623
+ */
624
+ extensionProvider() {
625
+ const extClass = this.$plugin.getDynamic('provisioner', this.provider);
626
+
627
+ if (extClass) {
628
+ return new extClass({
629
+ dispatch: this.$store.dispatch,
630
+ getters: this.$store.getters,
631
+ axios: this.$store.$axios,
632
+ $plugin: this.$store.app.$plugin,
633
+ $t: this.t,
634
+ isCreate: this.isCreate
635
+ });
636
+ }
637
+
638
+ return undefined;
639
+ },
640
+
641
+ /**
642
+ * Is a namespace needed? Only supported for providers from extensions, otherwise default is no
643
+ */
644
+ needsNamespace() {
645
+ return this.extensionProvider ? !!this.extensionProvider.namespaced : false;
646
+ },
647
+
737
648
  machineConfigSchema() {
738
649
  let schema;
739
650
 
@@ -745,6 +656,19 @@ export default {
745
656
  schema = `${ CAPI.MACHINE_CONFIG_GROUP }.${ this.provider }config`;
746
657
  }
747
658
 
659
+ // If this is an extension provider then the extension can provide the schema
660
+ const extensionSchema = this.extensionProvider?.machineConfigSchema;
661
+
662
+ if (extensionSchema) {
663
+ // machineConfigSchema can either be the schema name (string) or the schema itself (object)
664
+ if (typeof extensionSchema === 'object') {
665
+ return extensionSchema;
666
+ }
667
+
668
+ // Name of schema to use
669
+ schema = extensionSchema;
670
+ }
671
+
748
672
  return this.$store.getters['management/schemaFor'](schema);
749
673
  },
750
674
 
@@ -1033,7 +957,7 @@ export default {
1033
957
  validationPassed() {
1034
958
  const validRequiredPools = this.hasMachinePools ? this.hasRequiredNodes() : true;
1035
959
 
1036
- let base = (this.provider === 'custom' || this.isElementalCluster || !!this.credentialId);
960
+ let base = (this.provider === 'custom' || this.isElementalCluster || !!this.credentialId || !this.needCredential);
1037
961
 
1038
962
  // and in all of the validation statuses for each machine pool
1039
963
  Object.values(this.machinePoolValidation).forEach((v) => (base = base && v));
@@ -1121,19 +1045,198 @@ export default {
1121
1045
  created() {
1122
1046
  this.registerBeforeHook(this.saveMachinePools, 'save-machine-pools');
1123
1047
  this.registerBeforeHook(this.setRegistryConfig, 'set-registry-config');
1124
- this.registerBeforeHook(this.agentConfigurationCleanup, 'cleanup-agent-config');
1125
1048
  this.registerAfterHook(this.cleanupMachinePools, 'cleanup-machine-pools');
1126
1049
  this.registerAfterHook(this.saveRoleBindings, 'save-role-bindings');
1050
+
1051
+ // Register any hooks for this extension provider
1052
+ if (this.extensionProvider?.registerSaveHooks) {
1053
+ this.extensionProvider.registerSaveHooks(this.registerBeforeHook, this.registerAfterHook, this.value);
1054
+ }
1127
1055
  },
1128
1056
 
1129
1057
  methods: {
1130
1058
  nlToBr,
1131
1059
  set,
1132
1060
 
1061
+ /**
1062
+ * Initialize all the cluster specs
1063
+ */
1064
+ async initSpecs() {
1065
+ if ( !this.value.spec ) {
1066
+ set(this.value, 'spec', {});
1067
+ }
1068
+
1069
+ if ( !this.value.spec.machineSelectorConfig ) {
1070
+ set(this.value.spec, 'machineSelectorConfig', []);
1071
+ }
1072
+
1073
+ if ( !this.value.spec.machineSelectorConfig.find((x) => !x.machineLabelSelector) ) {
1074
+ this.value.spec.machineSelectorConfig.unshift({ config: {} });
1075
+ }
1076
+
1077
+ if ( this.value.spec.cloudCredentialSecretName ) {
1078
+ await this.$store.dispatch('rancher/findAll', { type: NORMAN.CLOUD_CREDENTIAL });
1079
+ this.credentialId = `${ this.value.spec.cloudCredentialSecretName }`;
1080
+ }
1081
+
1082
+ if ( !this.value.spec.kubernetesVersion ) {
1083
+ set(this.value.spec, 'kubernetesVersion', this.defaultVersion);
1084
+ }
1085
+
1086
+ if ( this.rkeConfig.etcd?.s3?.bucket ) {
1087
+ this.s3Backup = true;
1088
+ }
1089
+
1090
+ if ( !this.rkeConfig.etcd ) {
1091
+ set(this.rkeConfig, 'etcd', {
1092
+ disableSnapshots: false,
1093
+ s3: null,
1094
+ snapshotRetention: 5,
1095
+ snapshotScheduleCron: '0 */5 * * *',
1096
+ });
1097
+ } else if (typeof this.rkeConfig.etcd.disableSnapshots === 'undefined') {
1098
+ const disableSnapshots = !this.rkeConfig.etcd.snapshotRetention && !this.rkeConfig.etcd.snapshotScheduleCron;
1099
+
1100
+ set(this.rkeConfig.etcd, 'disableSnapshots', disableSnapshots);
1101
+ }
1102
+
1103
+ // Namespaces if required - this is mainly for custom provisioners via extensions that want
1104
+ // to allow creating their resources in a different namespace
1105
+ if (this.needsNamespace) {
1106
+ this.allNamespaces = await this.$store.dispatch('management/findAll', { type: NAMESPACE });
1107
+ }
1108
+
1109
+ if ( !this.machinePools ) {
1110
+ await this.initMachinePools(this.value.spec.rkeConfig.machinePools);
1111
+ if ( this.mode === _CREATE && !this.machinePools.length ) {
1112
+ await this.addMachinePool();
1113
+ }
1114
+ }
1115
+
1116
+ if ( this.value.spec.defaultPodSecurityPolicyTemplateName === undefined ) {
1117
+ set(this.value.spec, 'defaultPodSecurityPolicyTemplateName', '');
1118
+ }
1119
+
1120
+ if ( this.value.spec.defaultPodSecurityAdmissionConfigurationTemplateName === undefined ) {
1121
+ set(this.value.spec, 'defaultPodSecurityAdmissionConfigurationTemplateName', '');
1122
+ }
1123
+ },
1124
+
1125
+ /**
1126
+ * Fetch RKE versions and their configurations to be mapped to the form
1127
+ */
1128
+ async fetchRke2Versions() {
1129
+ if ( !this.rke2Versions ) {
1130
+ const hash = {
1131
+ rke2Versions: this.$store.dispatch('management/request', { url: '/v1-rke2-release/releases' }),
1132
+ k3sVersions: this.$store.dispatch('management/request', { url: '/v1-k3s-release/releases' }),
1133
+ };
1134
+
1135
+ if ( this.$store.getters['management/canList'](MANAGEMENT.POD_SECURITY_POLICY_TEMPLATE) ) {
1136
+ hash.allPSPs = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.POD_SECURITY_POLICY_TEMPLATE });
1137
+ }
1138
+
1139
+ if (this.$store.getters['management/canList'](MANAGEMENT.PSA)) {
1140
+ hash.allPSAs = await this.$store.dispatch('management/findAll', { type: MANAGEMENT.PSA });
1141
+ }
1142
+
1143
+ // Get the latest versions from the global settings if possible
1144
+ const globalSettings = await this.$store.getters['management/all'](MANAGEMENT.SETTING) || [];
1145
+ const defaultRke2Setting = globalSettings.find((setting) => setting.id === 'rke2-default-version') || {};
1146
+ const defaultK3sSetting = globalSettings.find((setting) => setting.id === 'k3s-default-version') || {};
1147
+
1148
+ let defaultRke2 = defaultRke2Setting?.value || defaultRke2Setting?.default;
1149
+ let defaultK3s = defaultK3sSetting?.value || defaultK3sSetting?.default;
1150
+
1151
+ // RKE2: Use the channel if we can not get the version from the settings
1152
+ if (!defaultRke2) {
1153
+ hash.rke2Channels = this.$store.dispatch('management/request', { url: '/v1-rke2-release/channels' });
1154
+ }
1155
+
1156
+ // K3S: Use the channel if we can not get the version from the settings
1157
+ if (!defaultK3s) {
1158
+ hash.k3sChannels = this.$store.dispatch('management/request', { url: '/v1-k3s-release/channels' });
1159
+ }
1160
+
1161
+ const res = await allHash(hash);
1162
+
1163
+ this.allPSPs = res.allPSPs || [];
1164
+ this.allPSAs = res.allPSAs || [];
1165
+ this.rke2Versions = res.rke2Versions.data || [];
1166
+ this.k3sVersions = res.k3sVersions.data || [];
1167
+
1168
+ if (!defaultRke2) {
1169
+ const rke2Channels = res.rke2Channels.data || [];
1170
+
1171
+ defaultRke2 = rke2Channels.find((x) => x.id === 'default')?.latest;
1172
+ }
1173
+
1174
+ if (!defaultK3s) {
1175
+ const k3sChannels = res.k3sChannels.data || [];
1176
+
1177
+ defaultK3s = k3sChannels.find((x) => x.id === 'default')?.latest;
1178
+ }
1179
+
1180
+ if ( !this.rke2Versions.length && !this.k3sVersions.length ) {
1181
+ throw new Error('No version info found in KDM');
1182
+ }
1183
+
1184
+ // Store default versions
1185
+ this.defaultRke2 = defaultRke2;
1186
+ this.defaultK3s = defaultK3s;
1187
+ }
1188
+ },
1189
+
1190
+ cleanAgentConfiguration(model, key) {
1191
+ if (!model || !model[key]) {
1192
+ return;
1193
+ }
1194
+
1195
+ const v = model[key];
1196
+
1197
+ if (Array.isArray(v) && v.length === 0) {
1198
+ delete model[key];
1199
+ } else if (v && typeof v === 'object') {
1200
+ Object.keys(v).forEach((k) => {
1201
+ // delete these auxiliary props used in podAffinity and nodeAffinity that shouldn't be sent to the server
1202
+ if (k === '_namespaceOption' || k === '_namespaces' || k === '_anti' || k === '_id') {
1203
+ delete v[k];
1204
+ }
1205
+
1206
+ // prevent cleanup of "namespaceSelector" when an empty object because it represents all namespaces in pod/node affinity
1207
+ // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#podaffinityterm-v1-core
1208
+ if (k !== 'namespaceSelector') {
1209
+ this.cleanAgentConfiguration(v, k);
1210
+ }
1211
+ });
1212
+
1213
+ if (Object.keys(v).length === 0) {
1214
+ delete model[key];
1215
+ }
1216
+ }
1217
+ },
1218
+
1219
+ /**
1220
+ * Clean agent configuration objects, so we only send values when the user has configured something
1221
+ */
1133
1222
  agentConfigurationCleanup() {
1134
- // Clean agent configuration objects, so we only send values when the user has configured something
1135
- cleanAgentConfiguration(this.value.spec, CLUSTER_AGENT_CUSTOMIZATION);
1136
- cleanAgentConfiguration(this.value.spec, FLEET_AGENT_CUSTOMIZATION);
1223
+ this.cleanAgentConfiguration(this.value.spec, CLUSTER_AGENT_CUSTOMIZATION);
1224
+ this.cleanAgentConfiguration(this.value.spec, FLEET_AGENT_CUSTOMIZATION);
1225
+ },
1226
+
1227
+ /**
1228
+ * Ensure we have empty models for the two agent configurations
1229
+ */
1230
+ setAgentConfiguration() {
1231
+ // Cluster Agent Configuration
1232
+ if ( !this.value.spec[CLUSTER_AGENT_CUSTOMIZATION]) {
1233
+ set(this.value.spec, CLUSTER_AGENT_CUSTOMIZATION, {});
1234
+ }
1235
+
1236
+ // Fleet Agent Configuration
1237
+ if ( !this.value.spec[FLEET_AGENT_CUSTOMIZATION] ) {
1238
+ set(this.value.spec, FLEET_AGENT_CUSTOMIZATION, {});
1239
+ }
1137
1240
  },
1138
1241
 
1139
1242
  /**
@@ -1213,18 +1316,27 @@ export default {
1213
1316
  },
1214
1317
 
1215
1318
  async addMachinePool(idx) {
1319
+ // this.machineConfigSchema is the schema for the Machine Pool's machine configuration for the given provider
1216
1320
  if ( !this.machineConfigSchema ) {
1217
1321
  return;
1218
1322
  }
1219
1323
 
1220
1324
  const numCurrentPools = this.machinePools.length || 0;
1221
1325
 
1222
- const config = await this.$store.dispatch('management/createPopulated', {
1223
- type: this.machineConfigSchema.id,
1224
- metadata: { namespace: DEFAULT_WORKSPACE }
1225
- });
1326
+ let config;
1327
+
1328
+ if (this.extensionProvider?.createMachinePoolMachineConfig) {
1329
+ config = await this.extensionProvider.createMachinePoolMachineConfig(idx, this.machinePools, this.value);
1330
+ } else {
1331
+ // Default - use the schema
1332
+ config = await this.$store.dispatch('management/createPopulated', {
1333
+ type: this.machineConfigSchema.id,
1334
+ metadata: { namespace: DEFAULT_WORKSPACE }
1335
+ });
1226
1336
 
1227
- config.applyDefaults(idx, this.machinePools);
1337
+ // If there is no specific model, the applyDefaults does nothing by default
1338
+ config.applyDefaults(idx, this.machinePools);
1339
+ }
1228
1340
 
1229
1341
  const name = `pool${ ++this.lastIdx }`;
1230
1342
  const pool = {
@@ -1233,6 +1345,7 @@ export default {
1233
1345
  remove: false,
1234
1346
  create: true,
1235
1347
  update: false,
1348
+ uid: name,
1236
1349
  pool: {
1237
1350
  name,
1238
1351
  etcdRole: numCurrentPools === 0,
@@ -1243,7 +1356,7 @@ export default {
1243
1356
  quantity: 1,
1244
1357
  unhealthyNodeTimeout: '0m',
1245
1358
  machineConfigRef: {
1246
- kind: this.machineConfigSchema.attributes.kind,
1359
+ kind: this.machineConfigSchema.attributes?.kind,
1247
1360
  name: null,
1248
1361
  },
1249
1362
  },
@@ -1301,6 +1414,11 @@ export default {
1301
1414
  async saveMachinePools() {
1302
1415
  const finalPools = [];
1303
1416
 
1417
+ // If the extension provider wants to do this, let them
1418
+ if (this.extensionProvider?.saveMachinePoolConfigs) {
1419
+ return await this.extensionProvider.saveMachinePoolConfigs(this.machinePools, this.value);
1420
+ }
1421
+
1304
1422
  for ( const entry of this.machinePools ) {
1305
1423
  if ( entry.remove ) {
1306
1424
  continue;
@@ -1420,7 +1538,24 @@ export default {
1420
1538
  async saveOverride(btnCb) {
1421
1539
  this.$set(this, 'busy', true);
1422
1540
 
1423
- return await this._doSaveOverride((done) => {
1541
+ // If the provider is from an extension, let it do the provision step
1542
+ if (this.extensionProvider?.provision) {
1543
+ const errors = await this.extensionProvider?.provision(this.value, this.machinePools);
1544
+ const okay = (errors || []).length === 0;
1545
+
1546
+ this.errors = errors;
1547
+ this.$set(this, 'busy', false);
1548
+
1549
+ btnCb(okay);
1550
+
1551
+ if (okay) {
1552
+ // If saved okay, go to the done route
1553
+ return this.done();
1554
+ }
1555
+ }
1556
+
1557
+ // Default save
1558
+ return this._doSaveOverride((done) => {
1424
1559
  this.$set(this, 'busy', false);
1425
1560
 
1426
1561
  return btnCb(done);
@@ -1428,6 +1563,9 @@ export default {
1428
1563
  },
1429
1564
 
1430
1565
  async _doSaveOverride(btnCb) {
1566
+ // We cannot use the hook, because it is triggered on YAML toggle without restore initialized data
1567
+ this.agentConfigurationCleanup();
1568
+
1431
1569
  if ( this.errors ) {
1432
1570
  clear(this.errors);
1433
1571
  }
@@ -1507,7 +1645,11 @@ export default {
1507
1645
  const harvesterKubeconfigSecret = await this.createKubeconfigSecret(kubeconfig);
1508
1646
 
1509
1647
  set(this.agentConfig, 'cloud-provider-config', `secret://fleet-default:${ harvesterKubeconfigSecret?.metadata?.name }`);
1510
- set(this.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.clusterName`, this.value.metadata.name);
1648
+
1649
+ if (this.isCreate) {
1650
+ set(this.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.global.cattle.clusterName`, this.value.metadata.name);
1651
+ }
1652
+
1511
1653
  set(this.chartValues, `${ HARVESTER_CLOUD_PROVIDER }.cloudConfigPath`, '/var/lib/rancher/rke2/etc/config-files/cloud-provider-config');
1512
1654
  }
1513
1655
  } catch (err) {
@@ -1523,10 +1665,10 @@ export default {
1523
1665
  delete this.value.spec.rkeConfig.machineGlobalConfig.profile;
1524
1666
  }
1525
1667
 
1526
- // store the current data for fleet and cluster agent so that we can re-apply it later if the save fails
1527
- // we also have a before hook (check created() hooks) where the cleanup of the data occurs
1528
- const clusterAgentDeploymentCustomization = JSON.parse(JSON.stringify(this.value.spec[CLUSTER_AGENT_CUSTOMIZATION]));
1529
- const fleetAgentDeploymentCustomization = JSON.parse(JSON.stringify(this.value.spec[FLEET_AGENT_CUSTOMIZATION]));
1668
+ // Store the current data for fleet and cluster agent so that we can re-apply it later if the save fails
1669
+ // The cleanup occurs before save with agentConfigurationCleanup()
1670
+ const clusterAgentDeploymentCustomization = this.value.spec[CLUSTER_AGENT_CUSTOMIZATION] ? JSON.parse(JSON.stringify(this.value.spec[CLUSTER_AGENT_CUSTOMIZATION])) : null;
1671
+ const fleetAgentDeploymentCustomization = this.value.spec[FLEET_AGENT_CUSTOMIZATION] ? JSON.parse(JSON.stringify(this.value.spec[FLEET_AGENT_CUSTOMIZATION])) : null;
1530
1672
 
1531
1673
  await this.save(btnCb);
1532
1674
 
@@ -1538,6 +1680,24 @@ export default {
1538
1680
  set(this.value.spec, FLEET_AGENT_CUSTOMIZATION, fleetAgentDeploymentCustomization);
1539
1681
  }
1540
1682
  },
1683
+
1684
+ async actuallySave(url) {
1685
+ if (this.extensionProvider?.saveCluster) {
1686
+ return await this.extensionProvider?.saveCluster(this.value, this.schema);
1687
+ }
1688
+
1689
+ if ( this.isCreate ) {
1690
+ url = url || this.schema.linkFor('collection');
1691
+ const res = await this.value.save({ url });
1692
+
1693
+ if (res) {
1694
+ Object.assign(this.value, res);
1695
+ }
1696
+ } else {
1697
+ await this.value.save();
1698
+ }
1699
+ },
1700
+
1541
1701
  // create a secret to reference the harvester cluster kubeconfig in rkeConfig
1542
1702
  async createKubeconfigSecret(kubeconfig = '') {
1543
1703
  const clusterName = this.value.metadata.name;
@@ -1982,10 +2142,9 @@ export default {
1982
2142
  },
1983
2143
 
1984
2144
  /**
1985
- * Check if current cluster has PSP enabled
1986
- * Consider exclusively RKE2 provisioned clusters in edit mode
2145
+ * Get provisioned RKE2 cluster PSPs in edit mode
1987
2146
  */
1988
- async checkPsps() {
2147
+ async getPsps() {
1989
2148
  // As server returns 500 we exclude all the possible cases
1990
2149
  if (
1991
2150
  this.mode !== _CREATE &&
@@ -2142,7 +2301,8 @@ export default {
2142
2301
  v-if="!isView"
2143
2302
  v-model="value"
2144
2303
  :mode="mode"
2145
- :namespaced="false"
2304
+ :namespaced="needsNamespace"
2305
+ :namespace-options="allNamespaces"
2146
2306
  name-label="cluster.name.label"
2147
2307
  name-placeholder="cluster.name.placeholder"
2148
2308
  description-label="cluster.description.label"
@@ -2916,7 +3076,9 @@ export default {
2916
3076
  label-key="cluster.agentConfig.tabs.cluster"
2917
3077
  >
2918
3078
  <AgentConfiguration
3079
+ v-if="value.spec.clusterAgentDeploymentCustomization"
2919
3080
  v-model="value.spec.clusterAgentDeploymentCustomization"
3081
+ data-testid="rke2-cluster-agent-config"
2920
3082
  type="cluster"
2921
3083
  :mode="mode"
2922
3084
  />
@@ -2928,7 +3090,9 @@ export default {
2928
3090
  label-key="cluster.agentConfig.tabs.fleet"
2929
3091
  >
2930
3092
  <AgentConfiguration
3093
+ v-if="value.spec.fleetAgentDeploymentCustomization"
2931
3094
  v-model="value.spec.fleetAgentDeploymentCustomization"
3095
+ data-testid="rke2-fleet-agent-config"
2932
3096
  type="fleet"
2933
3097
  :mode="mode"
2934
3098
  />
@@ -3026,6 +3190,26 @@ export default {
3026
3190
  v-model="value"
3027
3191
  :mode="mode"
3028
3192
  />
3193
+
3194
+ <!-- Extension tabs -->
3195
+ <Tab
3196
+ v-for="tab, i in extensionTabs"
3197
+ :key="`${tab.name}${i}`"
3198
+ :name="tab.name"
3199
+ :label="tab.label"
3200
+ :label-key="tab.labelKey"
3201
+ :weight="tab.weight"
3202
+ :tooltip="tab.tooltip"
3203
+ :show-header="tab.showHeader"
3204
+ :display-alert-icon="tab.displayAlertIcon"
3205
+ :error="tab.error"
3206
+ :badge="tab.badge"
3207
+ >
3208
+ <component
3209
+ :is="tab.component"
3210
+ :resource="value"
3211
+ />
3212
+ </Tab>
3029
3213
  </Tabbed>
3030
3214
  </div>
3031
3215
 
@@ -11,8 +11,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
11
11
  import { _CREATE, _VIEW } from '@shell/config/query-params';
12
12
  import { PROVISIONER_OPTIONS } from '@shell/models/storage.k8s.io.storageclass';
13
13
  import { mapFeature, UNSUPPORTED_STORAGE_DRIVERS } from '@shell/store/features';
14
- import { CSI_DRIVER } from '@shell/config/types';
15
- import { LONGHORN_DRIVER } from '@shell/models/persistentvolume';
14
+ import { CSI_DRIVER, LONGHORN_DRIVER } from '@shell/config/types';
16
15
 
17
16
  export default {
18
17
  name: 'StorageClass',