@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
@@ -0,0 +1,149 @@
1
+ import { roleTemplateRules } from '@shell/utils/validators/role-template';
2
+ import { RBAC } from '@shell/config/types';
3
+
4
+ const mockGetters = { 'i18n/t': (key: string) => key };
5
+
6
+ describe('roleTemplateRules', () => {
7
+ it('adds no errors for valid rules with no type', () => {
8
+ const rules = [{
9
+ verbs: ['get'], resources: ['pods'], apiGroups: ['']
10
+ }];
11
+ const errors: string[] = [];
12
+
13
+ roleTemplateRules(rules, mockGetters, errors);
14
+
15
+ expect(errors).toStrictEqual([]);
16
+ });
17
+
18
+ it('adds missingVerb error when a rule has empty verbs', () => {
19
+ const rules = [{
20
+ verbs: [], resources: ['pods'], apiGroups: ['']
21
+ }];
22
+ const errors: string[] = [];
23
+
24
+ roleTemplateRules(rules, mockGetters, errors);
25
+
26
+ expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.missingVerb']);
27
+ });
28
+
29
+ it('adds noResourceAndNonResource error when a rule has both resources and nonResourceURLs', () => {
30
+ const rules = [{
31
+ verbs: ['get'],
32
+ resources: ['pods'],
33
+ nonResourceURLs: ['/healthz'],
34
+ apiGroups: [''],
35
+ }];
36
+ const errors: string[] = [];
37
+
38
+ roleTemplateRules(rules, mockGetters, errors);
39
+
40
+ expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.noResourceAndNonResource']);
41
+ });
42
+
43
+ it('adds missingResource error for RBAC.ROLE type when resources are empty', () => {
44
+ const rules = [{
45
+ verbs: ['get'], resources: [], nonResourceURLs: ['/healthz'], apiGroups: ['']
46
+ }];
47
+ const errors: string[] = [];
48
+
49
+ roleTemplateRules(rules, mockGetters, errors, [RBAC.ROLE]);
50
+
51
+ expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.missingResource']);
52
+ });
53
+
54
+ it('adds missingApiGroup error for RBAC.ROLE type when apiGroups are empty', () => {
55
+ const rules = [{
56
+ verbs: ['get'], resources: ['pods'], apiGroups: []
57
+ }];
58
+ const errors: string[] = [];
59
+
60
+ roleTemplateRules(rules, mockGetters, errors, [RBAC.ROLE]);
61
+
62
+ expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.missingApiGroup']);
63
+ });
64
+
65
+ it('adds noResourceAndNonResource error for non-RBAC.ROLE when rule has resources and nonResourceUrls', () => {
66
+ const rules = [{
67
+ verbs: ['get'],
68
+ resources: ['pods'],
69
+ nonResourceUrls: ['/healthz'],
70
+ apiGroups: [''],
71
+ }];
72
+ const errors: string[] = [];
73
+
74
+ roleTemplateRules(rules, mockGetters, errors, [RBAC.CLUSTER_ROLE]);
75
+
76
+ expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.noResourceAndNonResource']);
77
+ });
78
+
79
+ it('adds missingOneResource error when rule has neither resources nor nonResourceURLs', () => {
80
+ const rules = [{
81
+ verbs: ['get'], resources: [], nonResourceURLs: [], apiGroups: []
82
+ }];
83
+ const errors: string[] = [];
84
+
85
+ roleTemplateRules(rules, mockGetters, errors, [RBAC.CLUSTER_ROLE]);
86
+
87
+ expect(errors).toStrictEqual(['validation.roleTemplate.roleTemplateRules.missingOneResource']);
88
+ });
89
+
90
+ it('adds multiple errors when multiple rules are invalid', () => {
91
+ const rules = [
92
+ {
93
+ verbs: [], resources: ['pods'], apiGroups: ['']
94
+ },
95
+ {
96
+ verbs: ['get'], resources: [], nonResourceURLs: [], apiGroups: []
97
+ },
98
+ ];
99
+ const errors: string[] = [];
100
+
101
+ roleTemplateRules(rules, mockGetters, errors, [RBAC.CLUSTER_ROLE]);
102
+
103
+ expect(errors).toStrictEqual([
104
+ 'validation.roleTemplate.roleTemplateRules.missingVerb',
105
+ 'validation.roleTemplate.roleTemplateRules.missingOneResource',
106
+ ]);
107
+ });
108
+
109
+ it('handles empty rules array with no errors', () => {
110
+ const errors: string[] = [];
111
+
112
+ roleTemplateRules([], mockGetters, errors);
113
+
114
+ expect(errors).toStrictEqual([]);
115
+ });
116
+
117
+ it('uses default empty array for rules when not provided', () => {
118
+ const errors: string[] = [];
119
+
120
+ roleTemplateRules(undefined as any, mockGetters, errors);
121
+
122
+ expect(errors).toStrictEqual([]);
123
+ });
124
+
125
+ it('adds both missingResource and missingApiGroup errors for RBAC.ROLE with empty resources and apiGroups', () => {
126
+ const rules = [{
127
+ verbs: ['get'], resources: [], nonResourceURLs: ['/healthz'], apiGroups: []
128
+ }];
129
+ const errors: string[] = [];
130
+
131
+ roleTemplateRules(rules, mockGetters, errors, [RBAC.ROLE]);
132
+
133
+ expect(errors).toStrictEqual([
134
+ 'validation.roleTemplate.roleTemplateRules.missingResource',
135
+ 'validation.roleTemplate.roleTemplateRules.missingApiGroup',
136
+ ]);
137
+ });
138
+
139
+ it('does not add RBAC.ROLE-specific errors when type is not RBAC.ROLE', () => {
140
+ const rules = [{
141
+ verbs: ['get'], resources: ['pods'], apiGroups: ['']
142
+ }];
143
+ const errors: string[] = [];
144
+
145
+ roleTemplateRules(rules, mockGetters, errors, [RBAC.CLUSTER_ROLE]);
146
+
147
+ expect(errors).toStrictEqual([]);
148
+ });
149
+ });
@@ -0,0 +1,283 @@
1
+ import { servicePort, clusterIp, externalName } from '@shell/utils/validators/service';
2
+
3
+ const mockGetters = {
4
+ 'i18n/t': (key: string, args?: object) => (args ? `${ key }:${ JSON.stringify(args) }` : key),
5
+ 'i18n/exists': () => false,
6
+ };
7
+
8
+ describe('validators/service', () => {
9
+ describe('servicePort', () => {
10
+ it('returns errors unchanged when serviceType is ExternalName', () => {
11
+ const errors: string[] = [];
12
+ const result = servicePort({ type: 'ExternalName', ports: [] }, mockGetters, errors, {});
13
+
14
+ expect(result).toStrictEqual([]);
15
+ });
16
+
17
+ it('adds required error when ports is empty', () => {
18
+ const errors: string[] = [];
19
+ const result = servicePort({ type: 'ClusterIP', ports: [] }, mockGetters, errors, {});
20
+
21
+ expect(result).toStrictEqual(['validation.required:{"key":"Port Rules"}']);
22
+ });
23
+
24
+ it('adds required error when ports is null', () => {
25
+ const errors: string[] = [];
26
+ const result = servicePort({ type: 'ClusterIP', ports: null }, mockGetters, errors, {});
27
+
28
+ expect(result).toContain('validation.required:{"key":"Port Rules"}');
29
+ });
30
+
31
+ it('requires port name when there are multiple ports', () => {
32
+ const errors: string[] = [];
33
+ const ports = [
34
+ {
35
+ name: '', port: 80, targetPort: 8080, nodePort: null
36
+ },
37
+ {
38
+ name: '', port: 443, targetPort: 8443, nodePort: null
39
+ },
40
+ ];
41
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
42
+
43
+ expect(result).toContain('validation.service.ports.name.required:{"position":1}');
44
+ expect(result).toContain('validation.service.ports.name.required:{"position":2}');
45
+ });
46
+
47
+ it('does not require name when only one port', () => {
48
+ const errors: string[] = [];
49
+ const ports = [{
50
+ name: '', port: 80, targetPort: 8080
51
+ }];
52
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
53
+
54
+ expect(result).not.toContain('validation.service.ports.name.required:{"position":1}');
55
+ });
56
+
57
+ it('adds error when nodePort is not a valid integer', () => {
58
+ const errors: string[] = [];
59
+ const ports = [{
60
+ name: 'http', nodePort: 'abc', port: 80, targetPort: 8080
61
+ }];
62
+ const result = servicePort({ type: 'NodePort', ports }, mockGetters, errors, {});
63
+
64
+ expect(result).toContain('validation.service.ports.nodePort.requiredInt:{"position":1}');
65
+ });
66
+
67
+ it('does not add nodePort error when nodePort is a valid integer string', () => {
68
+ const errors: string[] = [];
69
+ const ports = [{
70
+ name: 'http', nodePort: '30000', port: 80, targetPort: 8080
71
+ }];
72
+ const result = servicePort({ type: 'NodePort', ports }, mockGetters, errors, {});
73
+
74
+ expect(result).not.toContain('validation.service.ports.nodePort.requiredInt:{"position":1}');
75
+ });
76
+
77
+ it('does not add nodePort error when nodePort is falsy', () => {
78
+ const errors: string[] = [];
79
+ const ports = [{
80
+ name: 'http', nodePort: null, port: 80, targetPort: 8080
81
+ }];
82
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
83
+
84
+ expect(result).not.toContain('validation.service.ports.nodePort.requiredInt:{"position":1}');
85
+ });
86
+
87
+ it('adds error when port is not a valid integer', () => {
88
+ const errors: string[] = [];
89
+ const ports = [{
90
+ name: 'http', port: 'notanum', targetPort: 8080
91
+ }];
92
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
93
+
94
+ expect(result).toContain('validation.service.ports.port.requiredInt:{"position":1}');
95
+ });
96
+
97
+ it('adds required error when port is missing', () => {
98
+ const errors: string[] = [];
99
+ const ports = [{
100
+ name: 'http', port: null, targetPort: 8080
101
+ }];
102
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
103
+
104
+ expect(result).toContain('validation.service.ports.port.required:{"position":1}');
105
+ });
106
+
107
+ it('adds required error when targetPort is missing', () => {
108
+ const errors: string[] = [];
109
+ const ports = [{
110
+ name: 'http', port: 80, targetPort: null
111
+ }];
112
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
113
+
114
+ expect(result).toContain('validation.service.ports.targetPort.required:{"position":1}');
115
+ });
116
+
117
+ it('does not add error for valid numeric targetPort within range', () => {
118
+ const errors: string[] = [];
119
+ const ports = [{
120
+ name: 'http', port: 80, targetPort: '8080'
121
+ }];
122
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
123
+
124
+ expect(result).not.toContain('validation.service.ports.targetPort.between:{"position":1}');
125
+ });
126
+
127
+ it('adds error when numeric targetPort is out of range (below 1)', () => {
128
+ const errors: string[] = [];
129
+ const ports = [{
130
+ name: 'http', port: 80, targetPort: '0'
131
+ }];
132
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
133
+
134
+ expect(result).toContain('validation.service.ports.targetPort.between:{"position":1}');
135
+ });
136
+
137
+ it('adds error when numeric targetPort is out of range (above 65535)', () => {
138
+ const errors: string[] = [];
139
+ const ports = [{
140
+ name: 'http', port: 80, targetPort: '65536'
141
+ }];
142
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
143
+
144
+ expect(result).toContain('validation.service.ports.targetPort.between:{"position":1}');
145
+ });
146
+
147
+ it('validates IANA service name for non-numeric targetPort', () => {
148
+ const errors: string[] = [];
149
+ // valid IANA name: alphanumeric+hyphen, not starting/ending with hyphen, contains letter
150
+ const ports = [{
151
+ name: 'http', port: 80, targetPort: 'valid-name'
152
+ }];
153
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
154
+
155
+ // valid IANA name should not add errors for targetPort
156
+ const targetPortErrors = result.filter((e: string) => e.includes('targetPort'));
157
+
158
+ expect(targetPortErrors).toHaveLength(0);
159
+ });
160
+
161
+ it('adds error for invalid IANA service name (too long)', () => {
162
+ const errors: string[] = [];
163
+ const ports = [{
164
+ name: 'http', port: 80, targetPort: 'a-very-long-name-here'
165
+ }]; // > 15 chars
166
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
167
+
168
+ const targetPortErrors = result.filter((e: string) => e.includes('targetPort') || e.includes('length'));
169
+
170
+ expect(targetPortErrors.length).toBeGreaterThan(0);
171
+ });
172
+
173
+ it('validates port name using DNS label rules', () => {
174
+ const errors: string[] = [];
175
+ const ports = [{
176
+ name: '-invalid', port: 80, targetPort: 8080
177
+ }];
178
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
179
+
180
+ const nameErrors = result.filter((e: string) => e.includes('startHyphen') || e.includes('name'));
181
+
182
+ expect(nameErrors.length).toBeGreaterThan(0);
183
+ });
184
+
185
+ it('returns no errors for a valid single port spec', () => {
186
+ const errors: string[] = [];
187
+ const ports = [{
188
+ name: 'http', port: 80, targetPort: '8080'
189
+ }];
190
+ const result = servicePort({ type: 'ClusterIP', ports }, mockGetters, errors, {});
191
+
192
+ expect(result).toStrictEqual([]);
193
+ });
194
+ });
195
+
196
+ describe('clusterIp', () => {
197
+ it('returns errors unchanged for ExternalName service type', () => {
198
+ const errors = ['existing'];
199
+ const result = clusterIp({ type: 'ExternalName' }, mockGetters, errors, {});
200
+
201
+ expect(result).toStrictEqual(['existing']);
202
+ });
203
+
204
+ it('returns errors unchanged for ClusterIP type (no additional validation)', () => {
205
+ const errors: string[] = [];
206
+ const result = clusterIp({ type: 'ClusterIP' }, mockGetters, errors, {});
207
+
208
+ expect(result).toStrictEqual([]);
209
+ });
210
+
211
+ it('returns errors unchanged for NodePort type', () => {
212
+ const errors: string[] = [];
213
+ const result = clusterIp({ type: 'NodePort' }, mockGetters, errors, {});
214
+
215
+ expect(result).toStrictEqual([]);
216
+ });
217
+
218
+ it('returns errors unchanged for LoadBalancer type', () => {
219
+ const errors: string[] = [];
220
+ const result = clusterIp({ type: 'LoadBalancer' }, mockGetters, errors, {});
221
+
222
+ expect(result).toStrictEqual([]);
223
+ });
224
+
225
+ it('skips validation for unsupported service types', () => {
226
+ const errors = ['pre-existing'];
227
+ const result = clusterIp({ type: 'Headless' }, mockGetters, errors, {});
228
+
229
+ expect(result).toStrictEqual(['pre-existing']);
230
+ });
231
+ });
232
+
233
+ describe('externalName', () => {
234
+ it('returns errors unchanged when serviceType is not ExternalName', () => {
235
+ const errors: string[] = [];
236
+ const result = externalName({ type: 'ClusterIP', externalName: '' }, mockGetters, errors, {});
237
+
238
+ expect(result).toStrictEqual([]);
239
+ });
240
+
241
+ it('returns errors unchanged when spec.type is undefined', () => {
242
+ const errors: string[] = [];
243
+ const result = externalName({ type: undefined }, mockGetters, errors, {});
244
+
245
+ expect(result).toStrictEqual([]);
246
+ });
247
+
248
+ it('adds error when externalName is missing for ExternalName service', () => {
249
+ const errors: string[] = [];
250
+ const result = externalName({ type: 'ExternalName', externalName: '' }, mockGetters, errors, {});
251
+
252
+ expect(result).toContain('validation.service.externalName.none');
253
+ });
254
+
255
+ it('adds error when externalName is null for ExternalName service', () => {
256
+ const errors: string[] = [];
257
+ const result = externalName({ type: 'ExternalName', externalName: null }, mockGetters, errors, {});
258
+
259
+ expect(result).toContain('validation.service.externalName.none');
260
+ });
261
+
262
+ it('returns no errors for valid hostname in ExternalName service', () => {
263
+ const errors: string[] = [];
264
+ const result = externalName({ type: 'ExternalName', externalName: 'my-service.example.com' }, mockGetters, errors, {});
265
+
266
+ expect(result).toStrictEqual([]);
267
+ });
268
+
269
+ it('returns errors for invalid hostname in ExternalName service', () => {
270
+ const errors: string[] = [];
271
+ const result = externalName({ type: 'ExternalName', externalName: '-invalid-.example.com' }, mockGetters, errors, {});
272
+
273
+ expect(result.length).toBeGreaterThan(0);
274
+ });
275
+
276
+ it('preserves pre-existing errors when adding new hostname errors', () => {
277
+ const errors = ['pre-existing-error'];
278
+ const result = externalName({ type: 'ExternalName', externalName: '-bad' }, mockGetters, errors, {});
279
+
280
+ expect(result).toContain('pre-existing-error');
281
+ });
282
+ });
283
+ });
@@ -4,6 +4,7 @@ import {
4
4
  isDomainWithoutProtocol,
5
5
  isLocalhost,
6
6
  hasTrailingForwardSlash,
7
+ isValidUrl,
7
8
  } from '@shell/utils/validators/setting';
8
9
 
9
10
  describe('isServerUrl', () => {
@@ -83,10 +84,41 @@ describe('hasTrailingForwardSlash', () => {
83
84
  ['http://example.com/', true],
84
85
  ['HTTPS://EXAMPLE.COM/', true],
85
86
  ['https://example.com/path/', true],
87
+ ['https://rancher-ui/', true],
86
88
  ['https://example.com', false],
87
89
  ['http://example.com/path', false],
90
+ ['https://rancher-ui', false],
88
91
  ['example.com/', false],
89
92
  ])('should validate that hasTrailingForwardSlash("%s") returns %s', (input, expected) => {
90
93
  expect(hasTrailingForwardSlash(input)).toBe(expected);
91
94
  });
92
95
  });
96
+
97
+ describe('isValidUrl', () => {
98
+ it.each([
99
+ ['https://example.com', true],
100
+ ['http://example.com', true],
101
+ ['https://example.com/', true],
102
+ ['https://example.com/path?q=1#frag', true],
103
+ ['https://example.com:8443', true],
104
+ ['https://localhost', true],
105
+ ['https://localhost:8443', true],
106
+ ['https://127.0.0.1', true],
107
+ ['https://192.168.1.1:443', true],
108
+ // Single-label hostnames are valid in private DNS (Tailscale, internal DNS, /etc/hosts).
109
+ ['https://rancher-ui', true],
110
+ ['https://rancher-ui/', true],
111
+ ['https://rancher-ui:8443', true],
112
+ ['http://my-host', true],
113
+ // Invalid
114
+ ['not a url', false],
115
+ ['example.com', false],
116
+ ['https://', false],
117
+ ['', false],
118
+ [null, false],
119
+ [undefined, false],
120
+ [42, false],
121
+ ])('should validate that isValidUrl("%s") returns %s', (input, expected) => {
122
+ expect(isValidUrl(input)).toBe(expected);
123
+ });
124
+ });
@@ -81,6 +81,8 @@ describe('formRules', () => {
81
81
  const testCases = [
82
82
  ['https://test.com', undefined],
83
83
  ['https://test.com/', message],
84
+ ['https://rancher-ui', undefined],
85
+ ['https://rancher-ui/', message],
84
86
  ['https://', undefined],
85
87
  ['/', undefined],
86
88
  [undefined, undefined]
@@ -96,6 +98,54 @@ describe('formRules', () => {
96
98
  );
97
99
  });
98
100
 
101
+ describe('url', () => {
102
+ const message = JSON.stringify({ message: 'validation.setting.serverUrl.url' });
103
+ const testCases: [string | undefined, string | undefined][] = [
104
+ // Valid (including bare-hostname URLs in private networks)
105
+ ['https://example.com', undefined],
106
+ ['http://example.com', undefined],
107
+ ['https://example.com:8443/path', undefined],
108
+ ['https://localhost', undefined],
109
+ ['https://192.168.1.1', undefined],
110
+ ['https://rancher-ui', undefined],
111
+ ['https://rancher-ui:8443', undefined],
112
+
113
+ // Invalid
114
+ ['not a url', message],
115
+ ['example.com', message],
116
+ ['https://', message],
117
+
118
+ // Empty values pass — `required` rule covers that case separately
119
+ ['', undefined],
120
+ [undefined, undefined]
121
+ ];
122
+
123
+ it.each(testCases)(
124
+ 'should return undefined or correct message for url(%p)',
125
+ (url, expected) => {
126
+ expect(formRules.url(url)).toStrictEqual(expected);
127
+ }
128
+ );
129
+ });
130
+
131
+ describe('genericUrl', () => {
132
+ const message = JSON.stringify({ message: 'validation.genericUrl' });
133
+ const testCases: [string | undefined, string | undefined][] = [
134
+ ['https://example.com', undefined],
135
+ ['https://rancher-ui', undefined],
136
+ ['not a url', message],
137
+ ['', undefined],
138
+ [undefined, undefined]
139
+ ];
140
+
141
+ it.each(testCases)(
142
+ 'should return undefined or correct message for genericUrl(%p)',
143
+ (url, expected) => {
144
+ expect(formRules.genericUrl(url)).toStrictEqual(expected);
145
+ }
146
+ );
147
+ });
148
+
99
149
  describe('urlRepository', () => {
100
150
  const message = JSON.stringify({ message: 'validation.repository.url' });
101
151
  const testCases = [
@@ -4,10 +4,10 @@ import { RBAC } from '@shell/config/types';
4
4
  import { HCI } from '@shell/config/labels-annotations';
5
5
  import isEmpty from 'lodash/isEmpty';
6
6
  import has from 'lodash/has';
7
- import isUrl from 'is-url';
8
- // import uniq from 'lodash/uniq';
9
7
  import { Translation } from '@shell/types/t';
10
- import { isHttps, isLocalhost, hasTrailingForwardSlash, isDomainWithoutProtocol } from '@shell/utils/validators/setting';
8
+ import {
9
+ isHttps, isLocalhost, hasTrailingForwardSlash, isDomainWithoutProtocol, isValidUrl
10
+ } from '@shell/utils/validators/setting';
11
11
  import { cronScheduleRule } from '@shell/utils/validators/cron-schedule';
12
12
 
13
13
  // import uniq from 'lodash/uniq';
@@ -172,9 +172,9 @@ export default function(
172
172
 
173
173
  const trailingForwardSlash: Validator = (val: string) => hasTrailingForwardSlash(val) ? t('validation.setting.serverUrl.trailingForwardSlash') : undefined;
174
174
 
175
- const url: Validator = (val: string) => val && !isUrl(val) ? t('validation.setting.serverUrl.url') : undefined;
175
+ const url: Validator = (val: string) => val && !isValidUrl(val) ? t('validation.setting.serverUrl.url') : undefined;
176
176
 
177
- const genericUrl: Validator = (val: string) => val && !isUrl(val) ? t('validation.genericUrl') : undefined;
177
+ const genericUrl: Validator = (val: string) => val && !isValidUrl(val) ? t('validation.genericUrl') : undefined;
178
178
 
179
179
  const urlRepository: Validator = (url: string) => {
180
180
  const message = t('validation.repository.url');
@@ -12,7 +12,7 @@ const RULESETS = [
12
12
  },
13
13
  {
14
14
  path: FIELDS.NAME,
15
- rules: ['required'],
15
+ rules: ['required', 'uniquePoolName'],
16
16
  },
17
17
  {
18
18
  path: FIELDS.AUTOSCALER_MIN,
@@ -1,5 +1,3 @@
1
- import isUrl from 'is-url';
2
-
3
1
  // Note that these function cover specific use cases and you need to make sure it works for your use case before using them.
4
2
  // ie they would consider empty values as valid, not all endpoint formatting is enforced
5
3
 
@@ -23,4 +21,21 @@ export const isDomainWithoutProtocol = (value) => (/^(?=.{1,254}$)(?![a-z][a-z0-
23
21
 
24
22
  export const isLocalhost = (value) => (/^(?:https?:\/\/)?(?:localhost|127\.0\.0\.1)/i).test(value);
25
23
 
26
- export const hasTrailingForwardSlash = (value) => isUrl(value) && value?.toLowerCase().endsWith('/');
24
+ /**
25
+ * Validates that `value` parses as a URL with a hostname, including
26
+ * single-label hostnames (e.g. `https://rancher-ui`) used in private networks.
27
+ */
28
+ export const isValidUrl = (value) => {
29
+ if (typeof value !== 'string' || !value) {
30
+ return false;
31
+ }
32
+ try {
33
+ const url = new URL(value);
34
+
35
+ return !!url.hostname;
36
+ } catch {
37
+ return false;
38
+ }
39
+ };
40
+
41
+ export const hasTrailingForwardSlash = (value) => isValidUrl(value) && value?.toLowerCase().endsWith('/');