@rancher/shell 3.0.12-rc.2 → 3.0.12-rc.3

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 (272) hide show
  1. package/apis/impl/apis.ts +6 -0
  2. package/apis/index.ts +26 -0
  3. package/apis/intf/resources-api/cluster-api.ts +18 -0
  4. package/apis/intf/resources-api/mgmt-api.ts +15 -0
  5. package/apis/intf/resources-api/resource-base.ts +107 -0
  6. package/apis/intf/resources-api/resource-constants.ts +147 -0
  7. package/apis/intf/resources-api/resources-api.ts +143 -0
  8. package/apis/intf/resources.ts +49 -0
  9. package/apis/intf/{modal.ts → shell-api/modal.ts} +21 -26
  10. package/apis/intf/shell-api/proxy.ts +216 -0
  11. package/apis/intf/{slide-in.ts → shell-api/slide-in.ts} +4 -3
  12. package/apis/intf/{system.ts → shell-api/system.ts} +4 -1
  13. package/apis/intf/shell.ts +12 -6
  14. package/apis/resources/__tests__/resources-api-class.test.ts +550 -0
  15. package/apis/resources/index.ts +22 -0
  16. package/apis/resources/resources-api-class.ts +187 -0
  17. package/apis/shell/__tests__/proxy.test.ts +369 -0
  18. package/apis/shell/index.ts +8 -1
  19. package/apis/shell/modal.ts +4 -1
  20. package/apis/shell/notifications.ts +9 -6
  21. package/apis/shell/proxy.ts +256 -0
  22. package/apis/shell/slide-in.ts +4 -1
  23. package/apis/vue-shim.d.ts +2 -1
  24. package/assets/data/aws-regions.json +4 -0
  25. package/assets/fonts/lato/LatoLatin-Black.woff +0 -0
  26. package/assets/fonts/lato/LatoLatin-Black.woff2 +0 -0
  27. package/assets/fonts/lato/LatoLatin-BlackItalic.woff +0 -0
  28. package/assets/fonts/lato/LatoLatin-BlackItalic.woff2 +0 -0
  29. package/assets/fonts/lato/LatoLatin-Bold.woff +0 -0
  30. package/assets/fonts/lato/LatoLatin-Bold.woff2 +0 -0
  31. package/assets/fonts/lato/LatoLatin-BoldItalic.woff +0 -0
  32. package/assets/fonts/lato/LatoLatin-BoldItalic.woff2 +0 -0
  33. package/assets/fonts/lato/LatoLatin-Heavy.woff +0 -0
  34. package/assets/fonts/lato/LatoLatin-Heavy.woff2 +0 -0
  35. package/assets/fonts/lato/LatoLatin-HeavyItalic.woff +0 -0
  36. package/assets/fonts/lato/LatoLatin-HeavyItalic.woff2 +0 -0
  37. package/assets/fonts/lato/LatoLatin-Italic.woff +0 -0
  38. package/assets/fonts/lato/LatoLatin-Italic.woff2 +0 -0
  39. package/assets/fonts/lato/LatoLatin-Light.woff +0 -0
  40. package/assets/fonts/lato/LatoLatin-Light.woff2 +0 -0
  41. package/assets/fonts/lato/LatoLatin-LightItalic.woff +0 -0
  42. package/assets/fonts/lato/LatoLatin-LightItalic.woff2 +0 -0
  43. package/assets/fonts/lato/LatoLatin-Medium.woff +0 -0
  44. package/assets/fonts/lato/LatoLatin-Medium.woff2 +0 -0
  45. package/assets/fonts/lato/LatoLatin-MediumItalic.woff +0 -0
  46. package/assets/fonts/lato/LatoLatin-MediumItalic.woff2 +0 -0
  47. package/assets/fonts/lato/LatoLatin-Regular.woff +0 -0
  48. package/assets/fonts/lato/LatoLatin-Regular.woff2 +0 -0
  49. package/assets/fonts/lato/LatoLatin-Semibold.woff +0 -0
  50. package/assets/fonts/lato/LatoLatin-Semibold.woff2 +0 -0
  51. package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff +0 -0
  52. package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff2 +0 -0
  53. package/assets/styles/base/_variables.scss +2 -0
  54. package/assets/styles/fonts/_fontstack.scss +132 -8
  55. package/assets/translations/en-us.yaml +22 -5
  56. package/chart/monitoring/index.vue +10 -1
  57. package/components/ActionDropdownShell.vue +2 -1
  58. package/components/CruResourceFooter.vue +9 -5
  59. package/components/ExplorerProjectsNamespaces.vue +1 -1
  60. package/components/InstallHelmCharts.vue +2 -2
  61. package/components/LandingPagePreference.vue +14 -5
  62. package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +15 -1
  63. package/components/Resource/Detail/Metadata/index.vue +6 -0
  64. package/components/Resource/Detail/ResourcePopover/index.vue +12 -1
  65. package/components/Resource/Detail/SpacedRow.vue +3 -1
  66. package/components/Resource/Detail/TitleBar/index.vue +10 -11
  67. package/components/ResourceList/Masthead.vue +12 -8
  68. package/components/SelectIconGrid.vue +0 -10
  69. package/components/SingleClusterInfo.vue +1 -0
  70. package/components/SortableTable/__tests__/sorting.test.ts +126 -0
  71. package/components/SortableTable/index.vue +6 -9
  72. package/components/SortableTable/selection.js +23 -5
  73. package/components/SortableTable/sorting.js +6 -3
  74. package/components/Wizard.vue +14 -13
  75. package/components/fleet/FleetBundles.vue +100 -12
  76. package/components/fleet/FleetClusterTargets/index.vue +37 -15
  77. package/components/fleet/__tests__/FleetClusterTargets.test.ts +149 -115
  78. package/components/fleet/__tests__/FleetClusters.test.ts +12 -12
  79. package/components/form/LabeledSelect.vue +20 -3
  80. package/components/form/NameNsDescription.vue +11 -0
  81. package/components/form/Security.vue +6 -2
  82. package/components/form/WorkloadPorts.vue +2 -7
  83. package/components/form/__tests__/Security.test.ts +76 -0
  84. package/components/formatter/Autoscaler.vue +4 -4
  85. package/components/formatter/ClusterKubeVersion.vue +27 -0
  86. package/components/formatter/ClusterLink.vue +1 -7
  87. package/components/formatter/ClusterProvider.vue +6 -10
  88. package/components/formatter/FleetSummaryGraph.vue +0 -3
  89. package/components/formatter/MachineSummaryGraph.vue +1 -1
  90. package/components/formatter/PodsUsage.vue +2 -2
  91. package/components/formatter/__tests__/Autoscaler.test.ts +19 -22
  92. package/components/formatter/__tests__/FleetSummaryGraph.test.ts +216 -0
  93. package/components/formatter/__tests__/PodsUsage.test.ts +6 -10
  94. package/components/nav/NamespaceFilter.vue +2 -2
  95. package/components/nav/TopLevelMenu.helper.ts +15 -3
  96. package/components/nav/TopLevelMenu.vue +16 -5
  97. package/components/nav/__tests__/TopLevelMenu.test.ts +145 -21
  98. package/components/templates/home.vue +18 -0
  99. package/components/templates/plain.vue +18 -0
  100. package/components/templates/standalone.vue +17 -0
  101. package/composables/useFormValidation.ts +93 -0
  102. package/composables/useVeeValidateField.test.ts +159 -0
  103. package/composables/useVeeValidateField.ts +67 -0
  104. package/config/pagination-table-headers.js +18 -1
  105. package/config/product/manager.js +82 -21
  106. package/config/router/routes.js +6 -0
  107. package/config/table-headers.js +20 -1
  108. package/config/types.js +2 -1
  109. package/core/__tests__/plugin-products.test.ts +904 -20
  110. package/core/plugin-products-base.ts +107 -7
  111. package/core/plugin-products.ts +4 -0
  112. package/core/plugin-types.ts +111 -1
  113. package/core/plugin.ts +15 -7
  114. package/core/productDebugger.js +9 -4
  115. package/core/types-provisioning.ts +43 -30
  116. package/core/types.ts +57 -20
  117. package/detail/__tests__/pod.test.ts +41 -0
  118. package/detail/harvesterhci.io.management.cluster.vue +6 -2
  119. package/detail/pod.vue +1 -1
  120. package/detail/provisioning.cattle.io.cluster.vue +4 -10
  121. package/edit/auth/__tests__/azuread.test.ts +217 -34
  122. package/edit/auth/azuread.vue +122 -14
  123. package/edit/auth/oidc.vue +2 -2
  124. package/edit/networking.k8s.io.ingress/DefaultBackend.vue +13 -4
  125. package/edit/networking.k8s.io.ingress/RulePath.vue +8 -4
  126. package/edit/networking.k8s.io.ingress/index.vue +75 -20
  127. package/edit/provisioning.cattle.io.cluster/__tests__/MachinePool.test.ts +104 -0
  128. package/edit/provisioning.cattle.io.cluster/index.vue +11 -7
  129. package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -4
  130. package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
  131. package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +37 -4
  132. package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +132 -7
  133. package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -1
  134. package/edit/secret/__tests__/ssh.test.ts +5 -6
  135. package/edit/secret/basic.vue +31 -0
  136. package/edit/secret/index.vue +68 -17
  137. package/edit/secret/registry.vue +38 -0
  138. package/edit/secret/ssh.vue +29 -0
  139. package/edit/secret/tls.vue +30 -0
  140. package/edit/service.vue +4 -4
  141. package/edit/workload/Upgrading.vue +3 -3
  142. package/edit/workload/__tests__/Upgrading.test.ts +6 -9
  143. package/edit/workload/mixins/workload.js +2 -1
  144. package/list/fleet.cattle.io.bundle.vue +7 -104
  145. package/list/fleet.cattle.io.clusterregistrationtoken.vue +20 -0
  146. package/list/provisioning.cattle.io.cluster.vue +262 -180
  147. package/list/utils/management.cattle.io.cluster.utils.ts +128 -0
  148. package/mixins/__tests__/chart.test.ts +112 -0
  149. package/mixins/brand.js +2 -1
  150. package/mixins/chart.js +12 -8
  151. package/mixins/resource-fetch-api-pagination.js +41 -5
  152. package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +67 -67
  153. package/models/__tests__/management.cattle.io.cluster.test.ts +1 -1
  154. package/models/__tests__/management.cattle.io.node.ts +6 -5
  155. package/models/__tests__/management.cattle.io.nodepool.ts +5 -4
  156. package/models/__tests__/provisioning.cattle.io.cluster.test.ts +32 -11
  157. package/models/base-cluster.x-k8s.io.js +26 -0
  158. package/models/cluster.js +1 -1
  159. package/models/cluster.x-k8s.io.machine.js +4 -22
  160. package/models/cluster.x-k8s.io.machinedeployment.js +2 -20
  161. package/models/cluster.x-k8s.io.machineset.js +2 -20
  162. package/models/compliance.cattle.io.clusterscan.js +130 -2
  163. package/models/ext.cattle.io.kubeconfig.ts +4 -7
  164. package/models/fleet-application.js +3 -1
  165. package/models/management.cattle.io.cluster.js +417 -40
  166. package/models/management.cattle.io.node.js +6 -4
  167. package/models/management.cattle.io.nodepool.js +1 -1
  168. package/models/networking.k8s.io.ingress.js +12 -4
  169. package/models/provisioning.cattle.io.cluster.js +47 -330
  170. package/models/rke.cattle.io.etcdsnapshot.js +1 -2
  171. package/package.json +11 -29
  172. package/pages/__tests__/readme.test.ts +49 -0
  173. package/pages/auth/setup.vue +2 -3
  174. package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +76 -0
  175. package/pages/c/_cluster/apps/charts/chart.vue +60 -8
  176. package/pages/c/_cluster/apps/charts/install.vue +10 -7
  177. package/pages/c/_cluster/explorer/__tests__/index.test.ts +23 -25
  178. package/pages/c/_cluster/explorer/index.vue +5 -49
  179. package/pages/c/_cluster/istio/__tests__/istio.index.test.ts +194 -0
  180. package/pages/c/_cluster/istio/index.vue +21 -6
  181. package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -0
  182. package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +719 -2
  183. package/pages/c/_cluster/uiplugins/index.vue +203 -197
  184. package/pages/diagnostic.vue +13 -17
  185. package/pages/fail-whale.vue +18 -0
  186. package/pages/home.vue +77 -260
  187. package/pages/readme.vue +88 -0
  188. package/plugins/dashboard-store/__tests__/resource-class.test.ts +88 -0
  189. package/plugins/dashboard-store/actions.js +40 -18
  190. package/plugins/dashboard-store/resource-class.js +5 -2
  191. package/plugins/steve/__tests__/subscribe.spec.ts +6 -3
  192. package/plugins/steve/steve-pagination-utils.ts +11 -3
  193. package/plugins/steve/subscribe.js +35 -5
  194. package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +10 -4
  195. package/rancher-components/Form/LabeledInput/LabeledInput.vue +7 -52
  196. package/rancher-components/RcButton/RcButton.test.ts +37 -1
  197. package/rancher-components/RcButton/RcButton.vue +38 -8
  198. package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -8
  199. package/store/__tests__/catalog.test.ts +115 -1
  200. package/store/__tests__/type-map.test.ts +556 -1
  201. package/store/action-menu.js +8 -3
  202. package/store/auth.js +1 -1
  203. package/store/aws.js +27 -16
  204. package/store/catalog.js +27 -3
  205. package/store/digitalocean.js +20 -38
  206. package/store/index.js +2 -0
  207. package/store/linode.js +25 -40
  208. package/store/pnap.js +1 -0
  209. package/store/type-map.js +111 -29
  210. package/tsconfig.paths.json +8 -8
  211. package/types/kube/kube-api.ts +14 -1
  212. package/types/rancher/steve.api.ts +12 -12
  213. package/types/resources/settings.d.ts +2 -1
  214. package/types/shell/index.d.ts +102 -2
  215. package/types/store/dashboard-store.types.ts +108 -11
  216. package/types/store/pagination.types.ts +6 -3
  217. package/utils/__tests__/alertmanagerconfig.test.ts +117 -0
  218. package/utils/__tests__/async.test.ts +87 -0
  219. package/utils/__tests__/aws.test.ts +140 -0
  220. package/utils/__tests__/banners.test.ts +176 -0
  221. package/utils/__tests__/chart.test.ts +64 -1
  222. package/utils/__tests__/color.test.ts +226 -0
  223. package/utils/__tests__/duration.test.ts +140 -0
  224. package/utils/__tests__/fleet.test.ts +340 -0
  225. package/utils/__tests__/ingress.test.ts +553 -0
  226. package/utils/__tests__/kube.test.ts +68 -0
  227. package/utils/__tests__/namespace-filter.test.ts +109 -0
  228. package/utils/__tests__/pagination-utils.test.ts +361 -0
  229. package/utils/__tests__/parse-externalid.test.ts +137 -0
  230. package/utils/__tests__/perf-setting.utils.test.ts +98 -0
  231. package/utils/__tests__/poller-sequential.test.ts +177 -0
  232. package/utils/__tests__/poller.test.ts +170 -0
  233. package/utils/__tests__/promise.test.ts +346 -0
  234. package/utils/__tests__/settings.test.ts +140 -0
  235. package/utils/__tests__/sort-utils.test.ts +301 -0
  236. package/utils/__tests__/string-utils.test.ts +798 -0
  237. package/utils/__tests__/string.test.ts +23 -1
  238. package/utils/__tests__/style.test.ts +154 -0
  239. package/utils/__tests__/svg-filter.test.ts +184 -0
  240. package/utils/__tests__/units.test.ts +417 -0
  241. package/utils/__tests__/versions.test.ts +128 -0
  242. package/utils/__tests__/xccdf.test.ts +391 -0
  243. package/utils/chart.js +36 -0
  244. package/utils/fleet.ts +13 -3
  245. package/utils/gatekeeper/__tests__/util.test.ts +174 -0
  246. package/utils/gc/__tests__/gc-interval.test.ts +119 -0
  247. package/utils/gc/__tests__/gc-root-store.test.ts +225 -0
  248. package/utils/gc/__tests__/gc-route-changed.test.ts +96 -0
  249. package/utils/gc/__tests__/gc.test.ts +487 -0
  250. package/utils/ingress.ts +9 -1
  251. package/utils/pagination-utils.ts +2 -1
  252. package/utils/string.js +25 -2
  253. package/utils/uiplugins.ts +5 -5
  254. package/utils/validators/__tests__/cluster-name.test.ts +110 -0
  255. package/utils/validators/__tests__/cron-schedule.test.ts +79 -0
  256. package/utils/validators/__tests__/index.test.ts +481 -0
  257. package/utils/validators/__tests__/kubernetes-name.test.ts +163 -0
  258. package/utils/validators/__tests__/misc-validators.test.ts +246 -0
  259. package/utils/validators/__tests__/pod-affinity.test.ts +382 -0
  260. package/utils/validators/__tests__/prometheusrule.test.ts +211 -0
  261. package/utils/validators/__tests__/role-template.test.ts +149 -0
  262. package/utils/validators/__tests__/service.test.ts +283 -0
  263. package/utils/validators/__tests__/setting.test.js +32 -0
  264. package/utils/validators/formRules/__tests__/index.test.ts +50 -0
  265. package/utils/validators/formRules/index.ts +5 -5
  266. package/utils/validators/machine-pool.ts +1 -1
  267. package/utils/validators/setting.js +18 -3
  268. package/utils/xccdf.ts +418 -0
  269. package/assets/fonts/lato/lato-v17-latin-700.woff +0 -0
  270. package/assets/fonts/lato/lato-v17-latin-700.woff2 +0 -0
  271. package/assets/fonts/lato/lato-v17-latin-regular.woff +0 -0
  272. package/assets/fonts/lato/lato-v17-latin-regular.woff2 +0 -0
@@ -87,21 +87,21 @@ export default {
87
87
  path: 'spec.rules.http.paths.path', rules: ['absolutePath'], translationKey: 'ingress.rules.path.label'
88
88
  },
89
89
  {
90
- path: 'spec.rules.http.paths.backend.service.port.number', rules: ['required'], translationKey: 'ingress.rules.port.label'
90
+ path: 'spec.rules.http.paths.backend.service.port', rules: ['portRequired', 'portRange'], translationKey: 'ingress.rules.port.label'
91
91
  },
92
92
  {
93
93
  path: 'spec.rules.http.paths.backend.service.name', rules: ['required'], translationKey: 'ingress.rules.target.label'
94
94
  },
95
95
  { path: 'spec', rules: ['backEndOrRules'] },
96
96
  {
97
- path: 'spec.defaultBackend.service.name', rules: ['required'], translationKey: 'ingress.defaultBackend.targetService.label'
97
+ path: 'spec.defaultBackend.service.name', rules: ['defaultBackendNameRequired'], translationKey: 'ingress.defaultBackend.targetService.label'
98
98
  },
99
99
  {
100
- path: 'spec.defaultBackend.service.port.number', rules: ['required', 'requiredInt', 'portNumber'], translationKey: 'ingress.defaultBackend.port.label'
100
+ path: 'spec.defaultBackend.service.port', rules: ['defaultBackendPortRequired', 'portRange'], translationKey: 'ingress.defaultBackend.port.label'
101
101
  },
102
102
  { path: 'spec.tls.hosts', rules: ['required', 'wildcardHostname'] }
103
103
  ],
104
- fvReportedValidationPaths: ['spec.rules.http.paths.backend.service.port.number', 'spec.rules.http.paths.path', 'spec.rules.http.paths.backend.service.name']
104
+ fvReportedValidationPaths: ['spec.rules.http.paths.backend.service.port', 'spec.rules.http.paths.path', 'spec.rules.http.paths.backend.service.name']
105
105
  };
106
106
  },
107
107
 
@@ -125,35 +125,89 @@ export default {
125
125
  }
126
126
  };
127
127
 
128
- return { backEndOrRules };
128
+ const portLabel = this.t('ingress.rules.port.label');
129
+
130
+ // Built-in `required` won't work: it passes for empty objects like {} or { name: '' }.
131
+ const portRequired = (port) => {
132
+ if (typeof port === 'string' || typeof port === 'number') {
133
+ if (!port) {
134
+ return this.t('validation.required', { key: portLabel });
135
+ }
136
+ } else if (!port || (!port.number && !port.name)) {
137
+ return this.t('validation.required', { key: portLabel });
138
+ }
139
+ };
140
+
141
+ const portRange = (port) => {
142
+ let num;
143
+
144
+ if (typeof port === 'number') {
145
+ num = port;
146
+ } else if (typeof port === 'string') {
147
+ num = Number.parseInt(port);
148
+ } else if (port?.number) {
149
+ num = port.number;
150
+ }
151
+
152
+ if (num !== undefined && !Number.isNaN(num) && (num < 1 || num > 65535)) {
153
+ return this.t('validation.number.between', {
154
+ key: portLabel, min: '1', max: '65535'
155
+ });
156
+ }
157
+ };
158
+
159
+ const hasDefaultBackendService = () => {
160
+ const backend = get(this.value?.spec, this.value.defaultBackendPath);
161
+
162
+ return !!get(backend, this.value.serviceNamePath);
163
+ };
164
+
165
+ const nameLabel = this.t('ingress.defaultBackend.targetService.label');
166
+
167
+ // Only enforce required on the default backend when a service is selected.
168
+ // Selecting "None" means the user wants to remove the backend; willSave() handles cleanup.
169
+ const defaultBackendNameRequired = (name) => {
170
+ if (hasDefaultBackendService() && !name) {
171
+ return this.t('validation.required', { key: nameLabel });
172
+ }
173
+ };
174
+
175
+ const defaultBackendPortRequired = (port) => {
176
+ if (!hasDefaultBackendService()) {
177
+ return;
178
+ }
179
+
180
+ return portRequired(port);
181
+ };
182
+
183
+ return {
184
+ backEndOrRules,
185
+ portRequired,
186
+ portRange,
187
+ defaultBackendNameRequired,
188
+ defaultBackendPortRequired,
189
+ };
129
190
  },
130
191
  tabErrors() {
131
192
  return {
132
- rules: this.fvGetPathErrors(['spec.rules.host', 'spec.rules.http.paths.path', 'spec.rules.http.paths.backend.service.port.number', 'spec.rules.http.paths.backend.service.name'])?.length > 0,
133
- defaultBackend: this.fvGetPathErrors(['spec.defaultBackend.service.name', 'spec.defaultBackend.service.port.number'])?.length > 0
193
+ rules: this.fvGetPathErrors(['spec.rules.host', 'spec.rules.http.paths.path', 'spec.rules.http.paths.backend.service.port', 'spec.rules.http.paths.backend.service.name'])?.length > 0,
194
+ defaultBackend: this.fvGetPathErrors(['spec.defaultBackend.service.name', 'spec.defaultBackend.service.port'])?.length > 0
134
195
  };
135
196
  },
136
197
  rulesPathRules() {
137
198
  return {
138
199
  requestHost: this.fvGetAndReportPathRules('spec.rules.host'),
139
200
  path: this.fvGetAndReportPathRules('spec.rules.http.paths.path'),
140
- port: this.fvGetAndReportPathRules('spec.rules.http.paths.backend.service.port.number'),
201
+ port: this.fvGetAndReportPathRules('spec.rules.http.paths.backend.service.port'),
141
202
  target: this.fvGetAndReportPathRules('spec.rules.http.paths.backend.service.name'),
142
203
 
143
204
  };
144
205
  },
145
206
  defaultBackendPathRules() {
146
- const rulesExist = (this.value?.spec?.rules || []).length > 0;
147
- const defaultBackendExist = !!this.value?.spec?.defaultBackend?.service;
148
-
149
- if (!rulesExist || defaultBackendExist) {
150
- return {
151
- name: this.fvGetAndReportPathRules('spec.defaultBackend.service.name'),
152
- port: this.fvGetAndReportPathRules('spec.defaultBackend.service.port.number'),
153
- };
154
- }
155
-
156
- return { name: [], port: [] };
207
+ return {
208
+ name: this.fvGetAndReportPathRules('spec.defaultBackend.service.name'),
209
+ port: this.fvGetAndReportPathRules('spec.defaultBackend.service.port'),
210
+ };
157
211
  },
158
212
  serviceTargets() {
159
213
  return this.ingressHelper.findAndMapServiceTargets(this.services);
@@ -188,7 +242,8 @@ export default {
188
242
  willSave() {
189
243
  const backend = get(this.value.spec, this.value.defaultBackendPath);
190
244
  const serviceName = get(backend, this.value.serviceNamePath);
191
- const servicePort = get(backend, this.value.servicePortPath);
245
+ const servicePort = get(backend, this.value.servicePortPath) ||
246
+ get(backend, this.value.servicePortNamePath);
192
247
 
193
248
  if (backend && (!serviceName || !servicePort)) {
194
249
  const path = this.value.defaultBackendPath;
@@ -0,0 +1,104 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import MachinePool from '@shell/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue';
3
+
4
+ const TRANSLATION_KEY = '%cluster.machinePool.name.unique%';
5
+
6
+ function createPool(name: string, { remove = false } = {}) {
7
+ return {
8
+ id: `pool-${ name }`,
9
+ remove,
10
+ create: false,
11
+ update: true,
12
+ pool: {
13
+ name,
14
+ etcdRole: false,
15
+ controlPlaneRole: false,
16
+ workerRole: true,
17
+ quantity: 1,
18
+ },
19
+ config: null,
20
+ };
21
+ }
22
+
23
+ function mountMachinePool(currentPool: ReturnType<typeof createPool>, allPools: ReturnType<typeof createPool>[]) {
24
+ return shallowMount(MachinePool, {
25
+ props: {
26
+ value: currentPool,
27
+ mode: 'create',
28
+ provider: 'custom',
29
+ idx: 0,
30
+ machinePools: allPools,
31
+ poolId: currentPool.id,
32
+ poolCreateMode: true,
33
+ },
34
+ global: {
35
+ mocks: {
36
+ $store: {
37
+ getters: {
38
+ 'i18n/t': (key: string) => key,
39
+ 'i18n/exists': () => false,
40
+ 'type-map/hasCustomMachineConfigComponent': () => false,
41
+ 'type-map/importMachineConfig': () => null,
42
+ 'features/get': () => false,
43
+ },
44
+ dispatch: jest.fn(),
45
+ },
46
+ },
47
+ stubs: {
48
+ LabeledInput: true,
49
+ Checkbox: true,
50
+ Taints: true,
51
+ KeyValue: true,
52
+ AdvancedSection: true,
53
+ Banner: true,
54
+ UnitInput: true,
55
+ },
56
+ },
57
+ });
58
+ }
59
+
60
+ describe('component: MachinePool', () => {
61
+ describe('uniquePoolName validation', () => {
62
+ it('should return undefined when the name is empty', () => {
63
+ const pool = createPool('');
64
+ const wrapper = mountMachinePool(pool, [pool]);
65
+
66
+ expect(wrapper.vm.fvExtraRules.uniquePoolName('')).toBeUndefined();
67
+ });
68
+
69
+ it('should return undefined when the pool name is unique', () => {
70
+ const pool1 = createPool('pool1');
71
+ const pool2 = createPool('pool2');
72
+ const wrapper = mountMachinePool(pool1, [pool1, pool2]);
73
+
74
+ expect(wrapper.vm.fvExtraRules.uniquePoolName('pool1')).toBeUndefined();
75
+ });
76
+
77
+ it('should return an error message when the pool name is duplicated', () => {
78
+ const pool1 = createPool('same-name');
79
+ const pool2 = createPool('same-name');
80
+ const wrapper = mountMachinePool(pool1, [pool1, pool2]);
81
+
82
+ expect(wrapper.vm.fvExtraRules.uniquePoolName('same-name')).toStrictEqual(TRANSLATION_KEY);
83
+ });
84
+
85
+ it('should ignore pools marked for removal', () => {
86
+ const pool1 = createPool('same-name');
87
+ const pool2 = createPool('same-name', { remove: true });
88
+ const wrapper = mountMachinePool(pool1, [pool1, pool2]);
89
+
90
+ expect(wrapper.vm.fvExtraRules.uniquePoolName('same-name')).toBeUndefined();
91
+ });
92
+
93
+ it.each([
94
+ ['Pool1', 'pool1'],
95
+ ['POOL', 'pool'],
96
+ ])('should flag names that differ only by case as duplicates (%s vs %s)', (nameA, nameB) => {
97
+ const pool1 = createPool(nameA);
98
+ const pool2 = createPool(nameB);
99
+ const wrapper = mountMachinePool(pool1, [pool1, pool2]);
100
+
101
+ expect(wrapper.vm.fvExtraRules.uniquePoolName(nameA)).toStrictEqual(TRANSLATION_KEY);
102
+ });
103
+ });
104
+ });
@@ -5,7 +5,7 @@ import { Banner } from '@components/Banner';
5
5
  import CruResource from '@shell/components/CruResource';
6
6
  import SelectIconGrid from '@shell/components/SelectIconGrid';
7
7
  import {
8
- CHART, FROM_CLUSTER, SUB_TYPE, RKE_TYPE, _EDIT, _IMPORT, _CONFIG, _VIEW
8
+ CHART, FROM_CLUSTER, SUB_TYPE, RKE_TYPE, _EDIT, _IMPORT, _CONFIG, _VIEW, _CREATE
9
9
  } from '@shell/config/query-params';
10
10
  import { mapGetters } from 'vuex';
11
11
  import { sortBy } from '@shell/utils/sort';
@@ -80,16 +80,20 @@ export default {
80
80
  },
81
81
 
82
82
  async fetch() {
83
- const hash = {
84
- // These aren't explicitly used, but need to be listening for change events
85
- mgmtClusters: this.$store.dispatch('management/findAll', { type: MANAGEMENT.CLUSTER }),
86
- provClusters: this.$store.dispatch('management/findAll', { type: CAPI.RANCHER_CLUSTER }),
87
- };
83
+ const hash = {};
84
+
85
+ if (this.mode === _CREATE) {
86
+ // After we create we wait for these to exist, so start watching
87
+ await this.$store.dispatch('management/watch', { type: MANAGEMENT.CLUSTER, registerType: true });
88
+ await this.$store.dispatch('management/watch', { type: CAPI.RANCHER_CLUSTER, registerType: true });
89
+ } else {
90
+ hash.mgmtClusters = this.value.waitForMgmt();
91
+ }
88
92
 
89
93
  // No need to fetch charts when editing an RKE1 cluster
90
94
  // The computed property `isRke1` in this file is based on the RKE1/RKE2 toggle, which is not applicable in this case
91
95
  // Instead, we should rely on the value from the model: `this.value.isRke1`
92
- if (!this.value.isRke1 || (this.value.isRke1 && this.mode !== 'edit')) {
96
+ if (!this.value.isRke1 || (this.value.isRke1 && this.mode !== _EDIT)) {
93
97
  hash['catalog'] = this.$store.dispatch('catalog/load');
94
98
  }
95
99
 
@@ -174,7 +174,7 @@ export default {
174
174
  Object.entries(this.chartValues).forEach(([name, value]) => {
175
175
  const key = this.chartVersionKey(name);
176
176
 
177
- this.set(this.userChartValues, key, value);
177
+ this.userChartValues[key] = value;
178
178
  });
179
179
  this.setAgentConfiguration();
180
180
  },
@@ -298,6 +298,7 @@ export default {
298
298
  isEmpty,
299
299
  AGENT_CONFIGURATION_TYPES,
300
300
  basicsValid: true,
301
+ registryConfigValid: true,
301
302
  originalIngressController: this.value.spec.rkeConfig.machineGlobalConfig?.[INGRESS_CONTROLLER] || INGRESS_NONE,
302
303
  };
303
304
  },
@@ -906,7 +907,8 @@ export default {
906
907
  return this.validationPassed &&
907
908
  this.fvFormIsValid &&
908
909
  this.etcdConfigValid &&
909
- this.basicsValid;
910
+ this.basicsValid &&
911
+ this.registryConfigValid;
910
912
  },
911
913
  nginxSupported() {
912
914
  if (this.serverArgs?.disable?.options.includes(RKE2_INGRESS_NGINX)) {
@@ -2518,14 +2520,14 @@ export default {
2518
2520
  >
2519
2521
  <template
2520
2522
  v-for="(obj, idx) in machinePools"
2521
- :key="idx"
2523
+ :key="obj.id"
2522
2524
  >
2523
2525
  <Tab
2524
2526
  v-if="!obj.remove"
2525
2527
  :key="obj.id"
2526
2528
  :weight="-1 * idx"
2527
2529
  :name="obj.id"
2528
- :label="obj.pool.name || '(Not Named)'"
2530
+ :label="obj.pool.name || t('cluster.machinePool.name.notNamed')"
2529
2531
  :show-header="false"
2530
2532
  :error="!machinePoolValidation[obj.id]"
2531
2533
  >
@@ -2688,6 +2690,7 @@ export default {
2688
2690
  <Tab
2689
2691
  :name="REGISTRIES_TAB_NAME"
2690
2692
  label-key="cluster.tabs.registry"
2693
+ :error="!registryConfigValid"
2691
2694
  >
2692
2695
  <Registries
2693
2696
  v-if="isActiveTabRegistries"
@@ -2703,6 +2706,7 @@ export default {
2703
2706
  @custom-registry-changed="toggleCustomRegistry"
2704
2707
  @registry-host-changed="handleRegistryHostChanged"
2705
2708
  @registry-secret-changed="handleRegistrySecretChanged"
2709
+ @registry-validation-changed="(val) => registryConfigValid = val"
2706
2710
  />
2707
2711
  </Tab>
2708
2712
 
@@ -106,6 +106,16 @@ export default {
106
106
  const max = this.value?.pool?.autoscalingMaxSize || 0;
107
107
 
108
108
  return max - min >= 0 ? undefined : this.t('cluster.machinePool.autoscaler.validation.isAutoscalerMaxGreaterThanMin');
109
+ },
110
+ uniquePoolName: (name) => {
111
+ if (!name) {
112
+ return undefined;
113
+ }
114
+
115
+ const otherPools = (this.machinePools || []).filter((p) => !p.remove && p !== this.value);
116
+ const isDuplicate = otherPools.some((p) => p.pool.name?.toLowerCase() === name.toLowerCase());
117
+
118
+ return isDuplicate ? this.t('cluster.machinePool.name.unique') : undefined;
109
119
  }
110
120
  }
111
121
  };
@@ -295,6 +305,7 @@ export default {
295
305
  :label="t('cluster.machinePool.name.label')"
296
306
  :required="true"
297
307
  :disabled="!value.config || !!value.config.id || busy"
308
+ :require-dirty="false"
298
309
  :rules="fvGetAndReportPathRules(MACHINE_POOL_VALIDATION.FIELDS.NAME)"
299
310
  data-testid="machine-pool-name-input"
300
311
  />
@@ -7,11 +7,11 @@ import SelectOrCreateAuthSecret from '@shell/components/form/SelectOrCreateAuthS
7
7
  import CreateEditView from '@shell/mixins/create-edit-view';
8
8
  import SecretSelector from '@shell/components/form/SecretSelector';
9
9
  import { SECRET_TYPES as TYPES } from '@shell/config/secret';
10
- import { isBase64 } from '@shell/utils/string';
10
+ import { isBase64EncodedCert } from '@shell/utils/string';
11
11
  import { base64Decode, base64Encode } from '@shell/utils/crypto';
12
12
 
13
13
  export default {
14
- emits: ['updateConfigs'],
14
+ emits: ['updateConfigs', 'validation-changed'],
15
15
 
16
16
  components: {
17
17
  ArrayListGrouped,
@@ -62,6 +62,34 @@ export default {
62
62
  return TYPES.TLS;
63
63
  },
64
64
  },
65
+
66
+ caBundleRules() {
67
+ return [
68
+ (value) => {
69
+ if (!value) {
70
+ return undefined;
71
+ }
72
+
73
+ const isPem = value.trimStart().startsWith('-----BEGIN ');
74
+ const isValidBase64 = isBase64EncodedCert(value);
75
+
76
+ return (!isPem && !isValidBase64) ? this.t('registryConfig.caBundle.validationError') : undefined;
77
+ }
78
+ ];
79
+ },
80
+
81
+ // Derives validation state from entries directly, so it auto-updates when entries are added or removed
82
+ allCaBundlesValid() {
83
+ return this.entries.every((entry) => {
84
+ return this.caBundleRules.every((rule) => !rule(entry.caBundle));
85
+ });
86
+ },
87
+ },
88
+
89
+ watch: {
90
+ allCaBundlesValid(valid) {
91
+ this.$emit('validation-changed', valid);
92
+ },
65
93
  },
66
94
 
67
95
  mounted() {
@@ -78,7 +106,8 @@ export default {
78
106
 
79
107
  const caBundle = configMap[hostname].caBundle ?? this.defaultAddValue.caBundle;
80
108
 
81
- configMap[hostname].caBundle = isBase64(caBundle) ? base64Decode(caBundle) : caBundle;
109
+ // Decode base64 for display so the user sees readable PEM text
110
+ configMap[hostname].caBundle = isBase64EncodedCert(caBundle) ? base64Decode(caBundle) : caBundle;
82
111
 
83
112
  configMap[hostname].tlsSecretName = configMap[hostname].tlsSecretName ?? this.defaultAddValue.tlsSecretName;
84
113
  }
@@ -103,7 +132,8 @@ export default {
103
132
 
104
133
  configs[h] = {
105
134
  ...entry,
106
- caBundle: entry.caBundle ? base64Encode(entry.caBundle) : null
135
+ // If the value is already base64, use as-is to avoid double-encoding
136
+ caBundle: entry.caBundle ? (isBase64EncodedCert(entry.caBundle) ? entry.caBundle : base64Encode(entry.caBundle)) : null
107
137
  };
108
138
 
109
139
  delete configs[h].hostname;
@@ -192,6 +222,9 @@ export default {
192
222
  type="multiline"
193
223
  label="CA Cert Bundle"
194
224
  :mode="mode"
225
+ :rules="caBundleRules"
226
+ :require-dirty="false"
227
+ :tooltip="t('registryConfig.caBundle.tooltip')"
195
228
  />
196
229
 
197
230
  <div>
@@ -4,6 +4,9 @@ import { _EDIT } from '@shell/config/query-params';
4
4
  import { PROV_CLUSTER } from '@shell/edit/provisioning.cattle.io.cluster/__tests__/utils/cluster';
5
5
  import RegistryConfigs from '@shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue';
6
6
 
7
+ const VALID_BASE64_CERT = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t';
8
+ const VALID_PEM_TEXT = '-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJA';
9
+
7
10
  describe('component: RegistryConfigs', () => {
8
11
  let wrapper: Wrapper<InstanceType<typeof RegistryConfigs> & { [key: string]: any }>;
9
12
 
@@ -18,14 +21,14 @@ describe('component: RegistryConfigs', () => {
18
21
  SelectOrCreateAuthSecret: true,
19
22
  SecretSelector: true,
20
23
  },
21
- mocks: { $store: { getters: { 'i18n/t': jest.fn() } } }
24
+ mocks: { $store: { getters: { 'i18n/t': jest.fn((key: string) => key) } } }
22
25
  }
23
26
  };
24
27
 
25
28
  describe('key CA Cert Bundle', () => {
26
29
  it.each([
27
- ['source is plain text', 'Zm9vYmFy', 'foobar'],
28
- ['source is base64', 'foobar', 'foobar'],
30
+ ['source is base64', VALID_BASE64_CERT, '-----BEGIN CERTIFICATE-----'],
31
+ ['source is plain text', 'foobar', 'foobar'],
29
32
  ])('should display key, %p', (_, sourceCaBundle, displayedCaBundle) => {
30
33
  const value = clone(PROV_CLUSTER);
31
34
 
@@ -43,10 +46,10 @@ describe('component: RegistryConfigs', () => {
43
46
  expect(registry.props().value).toBe(displayedCaBundle);
44
47
  });
45
48
 
46
- it('should update key in base64 format', async() => {
49
+ it('should base64 encode plain PEM text on save', async() => {
47
50
  const value = clone(PROV_CLUSTER);
48
51
 
49
- value.spec.rkeConfig.registries.configs = { foo: { caBundle: 'Zm9vYmFy' } };
52
+ value.spec.rkeConfig.registries.configs = { foo: { caBundle: VALID_BASE64_CERT } };
50
53
 
51
54
  mountOptions.propsData.value = value;
52
55
 
@@ -57,10 +60,132 @@ describe('component: RegistryConfigs', () => {
57
60
 
58
61
  const registry = wrapper.findComponent('[data-testid^="registry-caBundle"]');
59
62
 
60
- registry.vm.$emit('update:value', 'ssh key');
63
+ registry.vm.$emit('update:value', VALID_PEM_TEXT);
61
64
  wrapper.vm.update();
62
65
 
63
- expect(wrapper.emitted('updateConfigs')[0][0]['foo']['caBundle']).toBe('c3NoIGtleQ==');
66
+ expect(wrapper.emitted('updateConfigs')[0][0]['foo']['caBundle']).toBe('LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJrVENCK3dJSkE=');
67
+ });
68
+
69
+ it('should keep base64 value as-is on save', async() => {
70
+ const value = clone(PROV_CLUSTER);
71
+
72
+ value.spec.rkeConfig.registries.configs = { foo: { caBundle: VALID_BASE64_CERT } };
73
+
74
+ mountOptions.propsData.value = value;
75
+
76
+ wrapper = mount(
77
+ RegistryConfigs,
78
+ mountOptions
79
+ );
80
+
81
+ const registry = wrapper.findComponent('[data-testid^="registry-caBundle"]');
82
+
83
+ registry.vm.$emit('update:value', VALID_BASE64_CERT);
84
+ wrapper.vm.update();
85
+
86
+ expect(wrapper.emitted('updateConfigs')[0][0]['foo']['caBundle']).toBe(VALID_BASE64_CERT);
87
+ });
88
+ });
89
+
90
+ describe('cA Cert Bundle validation', () => {
91
+ it('should pass validation for valid base64 value', () => {
92
+ const value = clone(PROV_CLUSTER);
93
+
94
+ value.spec.rkeConfig.registries.configs = {};
95
+ mountOptions.propsData.value = value;
96
+
97
+ wrapper = mount(RegistryConfigs, mountOptions);
98
+
99
+ const rule = wrapper.vm.caBundleRules[0];
100
+
101
+ expect(rule(VALID_BASE64_CERT)).toBeUndefined();
102
+ });
103
+
104
+ it('should pass validation for PEM text', () => {
105
+ const value = clone(PROV_CLUSTER);
106
+
107
+ value.spec.rkeConfig.registries.configs = {};
108
+ mountOptions.propsData.value = value;
109
+
110
+ wrapper = mount(RegistryConfigs, mountOptions);
111
+
112
+ const rule = wrapper.vm.caBundleRules[0];
113
+
114
+ expect(rule(VALID_PEM_TEXT)).toBeUndefined();
115
+ });
116
+
117
+ it('should pass validation for empty value', () => {
118
+ const value = clone(PROV_CLUSTER);
119
+
120
+ value.spec.rkeConfig.registries.configs = {};
121
+ mountOptions.propsData.value = value;
122
+
123
+ wrapper = mount(RegistryConfigs, mountOptions);
124
+
125
+ const rule = wrapper.vm.caBundleRules[0];
126
+
127
+ expect(rule('')).toBeUndefined();
128
+ expect(rule(null)).toBeUndefined();
129
+ expect(rule(undefined)).toBeUndefined();
130
+ });
131
+
132
+ it('should fail validation for invalid value', () => {
133
+ const value = clone(PROV_CLUSTER);
134
+
135
+ value.spec.rkeConfig.registries.configs = {};
136
+ mountOptions.propsData.value = value;
137
+
138
+ wrapper = mount(RegistryConfigs, mountOptions);
139
+
140
+ const rule = wrapper.vm.caBundleRules[0];
141
+
142
+ expect(rule('not-valid-base64!')).toContain('registryConfig.caBundle.validationError');
143
+ });
144
+
145
+ it('should report allCaBundlesValid as true when all entries have valid caBundles', () => {
146
+ const value = clone(PROV_CLUSTER);
147
+
148
+ value.spec.rkeConfig.registries.configs = {
149
+ 'reg1.example.com': { caBundle: VALID_BASE64_CERT },
150
+ 'reg2.example.com': { caBundle: VALID_BASE64_CERT },
151
+ };
152
+ mountOptions.propsData.value = value;
153
+
154
+ wrapper = mount(RegistryConfigs, mountOptions);
155
+
156
+ expect(wrapper.vm.allCaBundlesValid).toBe(true);
157
+ });
158
+
159
+ it('should report allCaBundlesValid as false when any entry has an invalid caBundle', () => {
160
+ const value = clone(PROV_CLUSTER);
161
+
162
+ value.spec.rkeConfig.registries.configs = {
163
+ 'reg1.example.com': { caBundle: VALID_BASE64_CERT },
164
+ 'reg2.example.com': { caBundle: 'not-valid!' },
165
+ };
166
+ mountOptions.propsData.value = value;
167
+
168
+ wrapper = mount(RegistryConfigs, mountOptions);
169
+
170
+ expect(wrapper.vm.allCaBundlesValid).toBe(false);
171
+ });
172
+
173
+ it('should update validation when an invalid entry is removed', () => {
174
+ const value = clone(PROV_CLUSTER);
175
+
176
+ value.spec.rkeConfig.registries.configs = {
177
+ 'reg1.example.com': { caBundle: VALID_BASE64_CERT },
178
+ 'reg2.example.com': { caBundle: 'not-valid!' },
179
+ };
180
+ mountOptions.propsData.value = value;
181
+
182
+ wrapper = mount(RegistryConfigs, mountOptions);
183
+
184
+ expect(wrapper.vm.allCaBundlesValid).toBe(false);
185
+
186
+ wrapper.vm.entries.splice(1, 1);
187
+
188
+ expect(wrapper.vm.allCaBundlesValid).toBe(true);
64
189
  });
65
190
  });
66
191
  });
@@ -8,7 +8,7 @@ import RegistryConfigs from '@shell/edit/provisioning.cattle.io.cluster/tabs/reg
8
8
  import RegistryMirrors from '@shell/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryMirrors';
9
9
 
10
10
  export default {
11
- emits: ['custom-registry-changed', 'registry-host-changed', 'registry-secret-changed', 'input', 'update-configs-changed'],
11
+ emits: ['custom-registry-changed', 'registry-host-changed', 'registry-secret-changed', 'input', 'update-configs-changed', 'registry-validation-changed'],
12
12
  components: {
13
13
  LabeledInput,
14
14
  Banner,
@@ -142,6 +142,7 @@ export default {
142
142
  :cluster-register-before-hook="registerBeforeHook"
143
143
  @update:value="$emit('input', $event)"
144
144
  @updateConfigs="$emit('update-configs-changed', $event)"
145
+ @validation-changed="$emit('registry-validation-changed', $event)"
145
146
  />
146
147
  </AdvancedSection>
147
148
  </div>
@@ -1,18 +1,17 @@
1
1
  import { mount } from '@vue/test-utils';
2
+ import { createStore } from 'vuex';
2
3
  import { _VIEW, _EDIT, _CREATE } from '@shell/config/query-params';
3
4
  import Ssh from '@shell/edit/secret/ssh.vue';
4
5
 
5
- const mockedStore = () => {
6
- return { getters: { 'i18n/t': jest.fn() } };
7
- };
8
-
9
6
  const mockedRoute = { query: {} };
10
7
 
11
8
  const requiredSetup = () => {
9
+ const store = createStore({ getters: { 'i18n/t': () => jest.fn() } });
10
+
12
11
  return {
13
12
  global: {
14
- mocks: {
15
- $store: mockedStore(),
13
+ plugins: [store],
14
+ mocks: {
16
15
  $route: mockedRoute,
17
16
  $fetchState: {},
18
17
  }