@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,382 @@
1
+ import { podAffinity } from '@shell/utils/validators/pod-affinity';
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/pod-affinity', () => {
9
+ describe('podAffinity', () => {
10
+ it('returns without error when spec is null', () => {
11
+ const errors: string[] = [];
12
+
13
+ podAffinity(null, mockGetters, errors);
14
+ expect(errors).toStrictEqual([]);
15
+ });
16
+
17
+ it('returns without error when spec is undefined', () => {
18
+ const errors: string[] = [];
19
+
20
+ podAffinity(undefined, mockGetters, errors);
21
+ expect(errors).toStrictEqual([]);
22
+ });
23
+
24
+ it('returns without error when spec has no affinity fields', () => {
25
+ const errors: string[] = [];
26
+
27
+ podAffinity({}, mockGetters, errors);
28
+ expect(errors).toStrictEqual([]);
29
+ });
30
+
31
+ it('returns without error when podAffinity is empty object', () => {
32
+ const errors: string[] = [];
33
+
34
+ podAffinity({ podAffinity: {} }, mockGetters, errors);
35
+ expect(errors).toStrictEqual([]);
36
+ });
37
+
38
+ describe('podAffinity.preferredDuringSchedulingIgnoredDuringExecution', () => {
39
+ it('validates weight must be between 1-100 (above 100)', () => {
40
+ const errors: string[] = [];
41
+
42
+ podAffinity({
43
+ podAffinity: {
44
+ preferredDuringSchedulingIgnoredDuringExecution: [
45
+ {
46
+ weight: 101,
47
+ podAffinityTerm: { topologyKey: 'kubernetes.io/hostname' }
48
+ }
49
+ ]
50
+ }
51
+ }, mockGetters, errors);
52
+
53
+ expect(errors.some((e) => e.includes('validation.number.between'))).toBe(true);
54
+ });
55
+
56
+ it('validates weight must be between 1-100 (below 1)', () => {
57
+ const errors: string[] = [];
58
+
59
+ podAffinity({
60
+ podAffinity: {
61
+ preferredDuringSchedulingIgnoredDuringExecution: [
62
+ {
63
+ weight: 0,
64
+ podAffinityTerm: { topologyKey: 'kubernetes.io/hostname' }
65
+ }
66
+ ]
67
+ }
68
+ }, mockGetters, errors);
69
+
70
+ expect(errors.some((e) => e.includes('validation.number.between'))).toBe(true);
71
+ });
72
+
73
+ it('accepts valid weight of 50', () => {
74
+ const errors: string[] = [];
75
+
76
+ podAffinity({
77
+ podAffinity: {
78
+ preferredDuringSchedulingIgnoredDuringExecution: [
79
+ {
80
+ weight: 50,
81
+ podAffinityTerm: { topologyKey: 'kubernetes.io/hostname' }
82
+ }
83
+ ]
84
+ }
85
+ }, mockGetters, errors);
86
+
87
+ expect(errors.filter((e) => e.includes('validation.number.between'))).toHaveLength(0);
88
+ });
89
+
90
+ it('validates topologyKey is required', () => {
91
+ const errors: string[] = [];
92
+
93
+ podAffinity({
94
+ podAffinity: {
95
+ preferredDuringSchedulingIgnoredDuringExecution: [
96
+ {
97
+ weight: 50,
98
+ podAffinityTerm: {}
99
+ }
100
+ ]
101
+ }
102
+ }, mockGetters, errors);
103
+
104
+ expect(errors.some((e) => e.includes('validation.podAffinity.topologyKey'))).toBe(true);
105
+ });
106
+
107
+ it('validates topologyKey format (no alphanumeric chars)', () => {
108
+ const errors: string[] = [];
109
+
110
+ podAffinity({
111
+ podAffinity: {
112
+ preferredDuringSchedulingIgnoredDuringExecution: [
113
+ {
114
+ weight: 50,
115
+ podAffinityTerm: { topologyKey: '!!!' }
116
+ }
117
+ ]
118
+ }
119
+ }, mockGetters, errors);
120
+
121
+ expect(errors.some((e) => e.includes('validation.podAffinity.topologyKey'))).toBe(true);
122
+ });
123
+
124
+ it('validates labelSelector matchExpressions operator', () => {
125
+ const errors: string[] = [];
126
+
127
+ podAffinity({
128
+ podAffinity: {
129
+ preferredDuringSchedulingIgnoredDuringExecution: [
130
+ {
131
+ weight: 50,
132
+ podAffinityTerm: {
133
+ topologyKey: 'kubernetes.io/hostname',
134
+ labelSelector: {
135
+ matchExpressions: [
136
+ { operator: 'InvalidOp', values: [] }
137
+ ]
138
+ }
139
+ }
140
+ }
141
+ ]
142
+ }
143
+ }, mockGetters, errors);
144
+
145
+ expect(errors.some((e) => e.includes('validation.podAffinity.matchExpressions.operator'))).toBe(true);
146
+ });
147
+
148
+ it('validates In/NotIn operators require values', () => {
149
+ const errors: string[] = [];
150
+
151
+ podAffinity({
152
+ podAffinity: {
153
+ preferredDuringSchedulingIgnoredDuringExecution: [
154
+ {
155
+ weight: 50,
156
+ podAffinityTerm: {
157
+ topologyKey: 'kubernetes.io/hostname',
158
+ labelSelector: {
159
+ matchExpressions: [
160
+ { operator: 'In', values: [] }
161
+ ]
162
+ }
163
+ }
164
+ }
165
+ ]
166
+ }
167
+ }, mockGetters, errors);
168
+
169
+ expect(errors.some((e) => e.includes('validation.podAffinity.matchExpressions.valuesMustBeDefined'))).toBe(true);
170
+ });
171
+
172
+ it('validates Exists/DoesNotExist operators require empty values', () => {
173
+ const errors: string[] = [];
174
+
175
+ podAffinity({
176
+ podAffinity: {
177
+ preferredDuringSchedulingIgnoredDuringExecution: [
178
+ {
179
+ weight: 50,
180
+ podAffinityTerm: {
181
+ topologyKey: 'kubernetes.io/hostname',
182
+ labelSelector: {
183
+ matchExpressions: [
184
+ { operator: 'Exists', values: ['some-value'] }
185
+ ]
186
+ }
187
+ }
188
+ }
189
+ ]
190
+ }
191
+ }, mockGetters, errors);
192
+
193
+ expect(errors.some((e) => e.includes('validation.podAffinity.matchExpressions.valueMustBeEmpty'))).toBe(true);
194
+ });
195
+
196
+ it('accepts valid In operator with values', () => {
197
+ const errors: string[] = [];
198
+
199
+ podAffinity({
200
+ podAffinity: {
201
+ preferredDuringSchedulingIgnoredDuringExecution: [
202
+ {
203
+ weight: 50,
204
+ podAffinityTerm: {
205
+ topologyKey: 'kubernetes.io/hostname',
206
+ labelSelector: {
207
+ matchExpressions: [
208
+ { operator: 'In', values: ['value1'] }
209
+ ]
210
+ }
211
+ }
212
+ }
213
+ ]
214
+ }
215
+ }, mockGetters, errors);
216
+
217
+ expect(errors.filter((e) => e.includes('matchExpressions'))).toHaveLength(0);
218
+ });
219
+
220
+ it('accepts valid DoesNotExist operator with no values', () => {
221
+ const errors: string[] = [];
222
+
223
+ podAffinity({
224
+ podAffinity: {
225
+ preferredDuringSchedulingIgnoredDuringExecution: [
226
+ {
227
+ weight: 50,
228
+ podAffinityTerm: {
229
+ topologyKey: 'kubernetes.io/hostname',
230
+ labelSelector: {
231
+ matchExpressions: [
232
+ { operator: 'DoesNotExist', values: [] }
233
+ ]
234
+ }
235
+ }
236
+ }
237
+ ]
238
+ }
239
+ }, mockGetters, errors);
240
+
241
+ expect(errors.filter((e) => e.includes('matchExpressions'))).toHaveLength(0);
242
+ });
243
+ });
244
+
245
+ describe('podAffinity.requiredDuringSchedulingIgnoredDuringExecution', () => {
246
+ it('validates topologyKey is required', () => {
247
+ const errors: string[] = [];
248
+
249
+ podAffinity({
250
+ podAffinity: {
251
+ requiredDuringSchedulingIgnoredDuringExecution: [
252
+ { labelSelector: {} }
253
+ ]
254
+ }
255
+ }, mockGetters, errors);
256
+
257
+ expect(errors.some((e) => e.includes('validation.podAffinity.topologyKey'))).toBe(true);
258
+ });
259
+
260
+ it('accepts valid requiredDuring term', () => {
261
+ const errors: string[] = [];
262
+
263
+ podAffinity({
264
+ podAffinity: {
265
+ requiredDuringSchedulingIgnoredDuringExecution: [
266
+ { topologyKey: 'kubernetes.io/hostname' }
267
+ ]
268
+ }
269
+ }, mockGetters, errors);
270
+
271
+ expect(errors).toHaveLength(0);
272
+ });
273
+ });
274
+
275
+ describe('podAntiAffinity', () => {
276
+ it('validates weight for preferredDuring antiAffinity', () => {
277
+ const errors: string[] = [];
278
+
279
+ podAffinity({
280
+ podAntiAffinity: {
281
+ preferredDuringSchedulingIgnoredDuringExecution: [
282
+ {
283
+ weight: 200,
284
+ podAffinityTerm: { topologyKey: 'kubernetes.io/hostname' }
285
+ }
286
+ ]
287
+ }
288
+ }, mockGetters, errors);
289
+
290
+ expect(errors.some((e) => e.includes('validation.number.between'))).toBe(true);
291
+ });
292
+
293
+ it('validates topologyKey for requiredDuring antiAffinity', () => {
294
+ const errors: string[] = [];
295
+
296
+ podAffinity({
297
+ podAntiAffinity: {
298
+ requiredDuringSchedulingIgnoredDuringExecution: [
299
+ {}
300
+ ]
301
+ }
302
+ }, mockGetters, errors);
303
+
304
+ expect(errors.some((e) => e.includes('validation.podAffinity.topologyKey'))).toBe(true);
305
+ });
306
+
307
+ it('returns without error for empty podAntiAffinity', () => {
308
+ const errors: string[] = [];
309
+
310
+ podAffinity({ podAntiAffinity: {} }, mockGetters, errors);
311
+ expect(errors).toStrictEqual([]);
312
+ });
313
+
314
+ it('accepts valid antiAffinity term', () => {
315
+ const errors: string[] = [];
316
+
317
+ podAffinity({
318
+ podAntiAffinity: {
319
+ requiredDuringSchedulingIgnoredDuringExecution: [
320
+ { topologyKey: 'kubernetes.io/zone' }
321
+ ]
322
+ }
323
+ }, mockGetters, errors);
324
+
325
+ expect(errors).toHaveLength(0);
326
+ });
327
+ });
328
+
329
+ describe('error context', () => {
330
+ it('includes index in error context for preferred affinity', () => {
331
+ const errors: string[] = [];
332
+
333
+ podAffinity({
334
+ podAffinity: {
335
+ preferredDuringSchedulingIgnoredDuringExecution: [
336
+ {
337
+ weight: 50,
338
+ podAffinityTerm: {}
339
+ }
340
+ ]
341
+ }
342
+ }, mockGetters, errors);
343
+
344
+ expect(errors[0]).toContain('"index":0');
345
+ });
346
+
347
+ it('includes group for affinity (affinityTitle)', () => {
348
+ const errors: string[] = [];
349
+
350
+ podAffinity({
351
+ podAffinity: {
352
+ preferredDuringSchedulingIgnoredDuringExecution: [
353
+ {
354
+ weight: 50,
355
+ podAffinityTerm: {}
356
+ }
357
+ ]
358
+ }
359
+ }, mockGetters, errors);
360
+
361
+ expect(errors[0]).toContain('validation.podAffinity.affinityTitle');
362
+ });
363
+
364
+ it('includes group for antiAffinity (antiAffinityTitle)', () => {
365
+ const errors: string[] = [];
366
+
367
+ podAffinity({
368
+ podAntiAffinity: {
369
+ preferredDuringSchedulingIgnoredDuringExecution: [
370
+ {
371
+ weight: 50,
372
+ podAffinityTerm: {}
373
+ }
374
+ ]
375
+ }
376
+ }, mockGetters, errors);
377
+
378
+ expect(errors[0]).toContain('validation.podAffinity.antiAffinityTitle');
379
+ });
380
+ });
381
+ });
382
+ });
@@ -0,0 +1,211 @@
1
+ import { ruleGroups, groupsAreValid } from '@shell/utils/validators/prometheusrule';
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/prometheusrule', () => {
9
+ describe('ruleGroups', () => {
10
+ it('adds error when groups is missing', () => {
11
+ const errors: string[] = [];
12
+
13
+ ruleGroups({}, mockGetters, errors, []);
14
+ expect(errors).toStrictEqual(['validation.prometheusRule.groups.required']);
15
+ });
16
+
17
+ it('adds error when groups is empty array', () => {
18
+ const errors: string[] = [];
19
+
20
+ ruleGroups({ groups: [] }, mockGetters, errors, []);
21
+ expect(errors).toStrictEqual(['validation.prometheusRule.groups.required']);
22
+ });
23
+
24
+ it('adds error when groups is null', () => {
25
+ const errors: string[] = [];
26
+
27
+ ruleGroups({ groups: null }, mockGetters, errors, []);
28
+ expect(errors).toStrictEqual(['validation.prometheusRule.groups.required']);
29
+ });
30
+
31
+ it('does not add error when groups has entries', () => {
32
+ const errors: string[] = [];
33
+
34
+ ruleGroups({ groups: [{ name: 'g1', rules: [] }] }, mockGetters, errors, []);
35
+ expect(errors).toStrictEqual([]);
36
+ });
37
+
38
+ it('returns errors array', () => {
39
+ const errors: string[] = [];
40
+ const result = ruleGroups({}, mockGetters, errors, []);
41
+
42
+ expect(result).toBe(errors);
43
+ });
44
+ });
45
+
46
+ describe('groupsAreValid', () => {
47
+ it('returns errors array', () => {
48
+ const errors: string[] = [];
49
+ const result = groupsAreValid([], mockGetters, errors, []);
50
+
51
+ expect(result).toBe(errors);
52
+ });
53
+
54
+ it('does not add errors for empty groups array', () => {
55
+ const errors: string[] = [];
56
+
57
+ groupsAreValid([], mockGetters, errors, []);
58
+ expect(errors).toStrictEqual([]);
59
+ });
60
+
61
+ it('adds error when group name is missing', () => {
62
+ const errors: string[] = [];
63
+
64
+ groupsAreValid([{ name: '', rules: [{ expr: 'up == 0' }] }], mockGetters, errors, []);
65
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.name'))).toBe(true);
66
+ });
67
+
68
+ it('adds error when group has no rules', () => {
69
+ const errors: string[] = [];
70
+
71
+ groupsAreValid([{ name: 'test-group', rules: [] }], mockGetters, errors, []);
72
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.singleEntry'))).toBe(true);
73
+ });
74
+
75
+ it('adds error when group rules is missing', () => {
76
+ const errors: string[] = [];
77
+
78
+ groupsAreValid([{ name: 'test-group' }], mockGetters, errors, []);
79
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.singleEntry'))).toBe(true);
80
+ });
81
+
82
+ it('uses 1-based index in group name error', () => {
83
+ const errors: string[] = [];
84
+
85
+ groupsAreValid([{ name: '', rules: [{ expr: 'up' }] }], mockGetters, errors, []);
86
+ expect(errors.some((e) => e.includes('"index":1'))).toBe(true);
87
+ });
88
+
89
+ it('uses 1-based index for second group in group name error', () => {
90
+ const errors: string[] = [];
91
+
92
+ groupsAreValid([
93
+ { name: 'valid', rules: [{ expr: 'up' }] },
94
+ { name: '', rules: [{ expr: 'down' }] },
95
+ ], mockGetters, errors, []);
96
+
97
+ expect(errors.some((e) => e.includes('"index":2'))).toBe(true);
98
+ });
99
+
100
+ describe('rule validation', () => {
101
+ it('adds error when alert rule has empty alert name', () => {
102
+ const errors: string[] = [];
103
+
104
+ groupsAreValid([{
105
+ name: 'group1',
106
+ rules: [{
107
+ alert: '', expr: 'up == 0', labels: { severity: 'warning' }
108
+ }]
109
+ }], mockGetters, errors, []);
110
+
111
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.rule.alertName'))).toBe(true);
112
+ });
113
+
114
+ it('does not error on alert rule with valid alert name', () => {
115
+ const errors: string[] = [];
116
+
117
+ groupsAreValid([{
118
+ name: 'group1',
119
+ rules: [{
120
+ alert: 'MyAlert', expr: 'up == 0', labels: { severity: 'warning' }
121
+ }]
122
+ }], mockGetters, errors, []);
123
+
124
+ expect(errors).toStrictEqual([]);
125
+ });
126
+
127
+ it('adds error when record rule has empty record name', () => {
128
+ const errors: string[] = [];
129
+
130
+ groupsAreValid([{
131
+ name: 'group1',
132
+ rules: [{ record: '', expr: 'sum(up)' }]
133
+ }], mockGetters, errors, []);
134
+
135
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.rule.recordName'))).toBe(true);
136
+ });
137
+
138
+ it('adds error when rule has no expr field', () => {
139
+ const errors: string[] = [];
140
+
141
+ groupsAreValid([{
142
+ name: 'group1',
143
+ rules: [{ alert: 'MyAlert', labels: { severity: 'warning' } }]
144
+ }], mockGetters, errors, []);
145
+
146
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.rule.expr'))).toBe(true);
147
+ });
148
+
149
+ it('adds error when rule has empty expr', () => {
150
+ const errors: string[] = [];
151
+
152
+ groupsAreValid([{
153
+ name: 'group1',
154
+ rules: [{
155
+ alert: 'MyAlert', expr: '', labels: { severity: 'warning' }
156
+ }]
157
+ }], mockGetters, errors, []);
158
+
159
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.rule.expr'))).toBe(true);
160
+ });
161
+
162
+ it('adds error when alert rule has no labels', () => {
163
+ const errors: string[] = [];
164
+
165
+ groupsAreValid([{
166
+ name: 'group1',
167
+ rules: [{ alert: 'MyAlert', expr: 'up == 0' }]
168
+ }], mockGetters, errors, []);
169
+
170
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.rule.labels'))).toBe(true);
171
+ });
172
+
173
+ it('adds error when alert rule has empty labels', () => {
174
+ const errors: string[] = [];
175
+
176
+ groupsAreValid([{
177
+ name: 'group1',
178
+ rules: [{
179
+ alert: 'MyAlert', expr: 'up == 0', labels: {}
180
+ }]
181
+ }], mockGetters, errors, []);
182
+
183
+ expect(errors.some((e) => e.includes('validation.prometheusRule.groups.valid.rule.labels'))).toBe(true);
184
+ });
185
+
186
+ it('does not add labels error for record rules', () => {
187
+ const errors: string[] = [];
188
+
189
+ groupsAreValid([{
190
+ name: 'group1',
191
+ rules: [{ record: 'my:record', expr: 'sum(up)' }]
192
+ }], mockGetters, errors, []);
193
+
194
+ expect(errors.filter((e) => e.includes('labels'))).toHaveLength(0);
195
+ });
196
+
197
+ it('uses 1-based index in rule errors', () => {
198
+ const errors: string[] = [];
199
+
200
+ groupsAreValid([{
201
+ name: 'group1',
202
+ rules: [{
203
+ alert: 'MyAlert', expr: 'up == 0', labels: {}
204
+ }]
205
+ }], mockGetters, errors, []);
206
+
207
+ expect(errors.some((e) => e.includes('"ruleIndex":1'))).toBe(true);
208
+ });
209
+ });
210
+ });
211
+ });