@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.
- package/apis/impl/apis.ts +6 -0
- package/apis/index.ts +26 -0
- package/apis/intf/resources-api/cluster-api.ts +18 -0
- package/apis/intf/resources-api/mgmt-api.ts +15 -0
- package/apis/intf/resources-api/resource-base.ts +107 -0
- package/apis/intf/resources-api/resource-constants.ts +147 -0
- package/apis/intf/resources-api/resources-api.ts +143 -0
- package/apis/intf/resources.ts +49 -0
- package/apis/intf/{modal.ts → shell-api/modal.ts} +21 -26
- package/apis/intf/shell-api/proxy.ts +216 -0
- package/apis/intf/{slide-in.ts → shell-api/slide-in.ts} +4 -3
- package/apis/intf/{system.ts → shell-api/system.ts} +4 -1
- package/apis/intf/shell.ts +12 -6
- package/apis/resources/__tests__/resources-api-class.test.ts +550 -0
- package/apis/resources/index.ts +22 -0
- package/apis/resources/resources-api-class.ts +187 -0
- package/apis/shell/__tests__/proxy.test.ts +369 -0
- package/apis/shell/index.ts +8 -1
- package/apis/shell/modal.ts +4 -1
- package/apis/shell/notifications.ts +9 -6
- package/apis/shell/proxy.ts +256 -0
- package/apis/shell/slide-in.ts +4 -1
- package/apis/vue-shim.d.ts +2 -1
- package/assets/data/aws-regions.json +4 -0
- package/assets/fonts/lato/LatoLatin-Black.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Black.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-BlackItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-BlackItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Bold.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Bold.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-BoldItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-BoldItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Heavy.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Heavy.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-HeavyItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-HeavyItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Italic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Italic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Light.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Light.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-LightItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-LightItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Medium.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Medium.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-MediumItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-MediumItalic.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Regular.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Regular.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-Semibold.woff +0 -0
- package/assets/fonts/lato/LatoLatin-Semibold.woff2 +0 -0
- package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff +0 -0
- package/assets/fonts/lato/LatoLatin-SemiboldItalic.woff2 +0 -0
- package/assets/styles/base/_variables.scss +2 -0
- package/assets/styles/fonts/_fontstack.scss +132 -8
- package/assets/translations/en-us.yaml +22 -5
- package/chart/monitoring/index.vue +10 -1
- package/components/ActionDropdownShell.vue +2 -1
- package/components/CruResourceFooter.vue +9 -5
- package/components/ExplorerProjectsNamespaces.vue +1 -1
- package/components/InstallHelmCharts.vue +2 -2
- package/components/LandingPagePreference.vue +14 -5
- package/components/Resource/Detail/Metadata/IdentifyingInformation/index.vue +15 -1
- package/components/Resource/Detail/Metadata/index.vue +6 -0
- package/components/Resource/Detail/ResourcePopover/index.vue +12 -1
- package/components/Resource/Detail/SpacedRow.vue +3 -1
- package/components/Resource/Detail/TitleBar/index.vue +10 -11
- package/components/ResourceList/Masthead.vue +12 -8
- package/components/SelectIconGrid.vue +0 -10
- package/components/SingleClusterInfo.vue +1 -0
- package/components/SortableTable/__tests__/sorting.test.ts +126 -0
- package/components/SortableTable/index.vue +6 -9
- package/components/SortableTable/selection.js +23 -5
- package/components/SortableTable/sorting.js +6 -3
- package/components/Wizard.vue +14 -13
- package/components/fleet/FleetBundles.vue +100 -12
- package/components/fleet/FleetClusterTargets/index.vue +37 -15
- package/components/fleet/__tests__/FleetClusterTargets.test.ts +149 -115
- package/components/fleet/__tests__/FleetClusters.test.ts +12 -12
- package/components/form/LabeledSelect.vue +20 -3
- package/components/form/NameNsDescription.vue +11 -0
- package/components/form/Security.vue +6 -2
- package/components/form/WorkloadPorts.vue +2 -7
- package/components/form/__tests__/Security.test.ts +76 -0
- package/components/formatter/Autoscaler.vue +4 -4
- package/components/formatter/ClusterKubeVersion.vue +27 -0
- package/components/formatter/ClusterLink.vue +1 -7
- package/components/formatter/ClusterProvider.vue +6 -10
- package/components/formatter/FleetSummaryGraph.vue +0 -3
- package/components/formatter/MachineSummaryGraph.vue +1 -1
- package/components/formatter/PodsUsage.vue +2 -2
- package/components/formatter/__tests__/Autoscaler.test.ts +19 -22
- package/components/formatter/__tests__/FleetSummaryGraph.test.ts +216 -0
- package/components/formatter/__tests__/PodsUsage.test.ts +6 -10
- package/components/nav/NamespaceFilter.vue +2 -2
- package/components/nav/TopLevelMenu.helper.ts +15 -3
- package/components/nav/TopLevelMenu.vue +16 -5
- package/components/nav/__tests__/TopLevelMenu.test.ts +145 -21
- package/components/templates/home.vue +18 -0
- package/components/templates/plain.vue +18 -0
- package/components/templates/standalone.vue +17 -0
- package/composables/useFormValidation.ts +93 -0
- package/composables/useVeeValidateField.test.ts +159 -0
- package/composables/useVeeValidateField.ts +67 -0
- package/config/pagination-table-headers.js +18 -1
- package/config/product/manager.js +82 -21
- package/config/router/routes.js +6 -0
- package/config/table-headers.js +20 -1
- package/config/types.js +2 -1
- package/core/__tests__/plugin-products.test.ts +904 -20
- package/core/plugin-products-base.ts +107 -7
- package/core/plugin-products.ts +4 -0
- package/core/plugin-types.ts +111 -1
- package/core/plugin.ts +15 -7
- package/core/productDebugger.js +9 -4
- package/core/types-provisioning.ts +43 -30
- package/core/types.ts +57 -20
- package/detail/__tests__/pod.test.ts +41 -0
- package/detail/harvesterhci.io.management.cluster.vue +6 -2
- package/detail/pod.vue +1 -1
- package/detail/provisioning.cattle.io.cluster.vue +4 -10
- package/edit/auth/__tests__/azuread.test.ts +217 -34
- package/edit/auth/azuread.vue +122 -14
- package/edit/auth/oidc.vue +2 -2
- package/edit/networking.k8s.io.ingress/DefaultBackend.vue +13 -4
- package/edit/networking.k8s.io.ingress/RulePath.vue +8 -4
- package/edit/networking.k8s.io.ingress/index.vue +75 -20
- package/edit/provisioning.cattle.io.cluster/__tests__/MachinePool.test.ts +104 -0
- package/edit/provisioning.cattle.io.cluster/index.vue +11 -7
- package/edit/provisioning.cattle.io.cluster/rke2.vue +8 -4
- package/edit/provisioning.cattle.io.cluster/tabs/MachinePool.vue +11 -0
- package/edit/provisioning.cattle.io.cluster/tabs/registries/RegistryConfigs.vue +37 -4
- package/edit/provisioning.cattle.io.cluster/tabs/registries/__tests__/RegistryConfigs.test.ts +132 -7
- package/edit/provisioning.cattle.io.cluster/tabs/registries/index.vue +2 -1
- package/edit/secret/__tests__/ssh.test.ts +5 -6
- package/edit/secret/basic.vue +31 -0
- package/edit/secret/index.vue +68 -17
- package/edit/secret/registry.vue +38 -0
- package/edit/secret/ssh.vue +29 -0
- package/edit/secret/tls.vue +30 -0
- package/edit/service.vue +4 -4
- package/edit/workload/Upgrading.vue +3 -3
- package/edit/workload/__tests__/Upgrading.test.ts +6 -9
- package/edit/workload/mixins/workload.js +2 -1
- package/list/fleet.cattle.io.bundle.vue +7 -104
- package/list/fleet.cattle.io.clusterregistrationtoken.vue +20 -0
- package/list/provisioning.cattle.io.cluster.vue +262 -180
- package/list/utils/management.cattle.io.cluster.utils.ts +128 -0
- package/mixins/__tests__/chart.test.ts +112 -0
- package/mixins/brand.js +2 -1
- package/mixins/chart.js +12 -8
- package/mixins/resource-fetch-api-pagination.js +41 -5
- package/models/__tests__/ext.cattle.io.kubeconfig.test.ts +67 -67
- package/models/__tests__/management.cattle.io.cluster.test.ts +1 -1
- package/models/__tests__/management.cattle.io.node.ts +6 -5
- package/models/__tests__/management.cattle.io.nodepool.ts +5 -4
- package/models/__tests__/provisioning.cattle.io.cluster.test.ts +32 -11
- package/models/base-cluster.x-k8s.io.js +26 -0
- package/models/cluster.js +1 -1
- package/models/cluster.x-k8s.io.machine.js +4 -22
- package/models/cluster.x-k8s.io.machinedeployment.js +2 -20
- package/models/cluster.x-k8s.io.machineset.js +2 -20
- package/models/compliance.cattle.io.clusterscan.js +130 -2
- package/models/ext.cattle.io.kubeconfig.ts +4 -7
- package/models/fleet-application.js +3 -1
- package/models/management.cattle.io.cluster.js +417 -40
- package/models/management.cattle.io.node.js +6 -4
- package/models/management.cattle.io.nodepool.js +1 -1
- package/models/networking.k8s.io.ingress.js +12 -4
- package/models/provisioning.cattle.io.cluster.js +47 -330
- package/models/rke.cattle.io.etcdsnapshot.js +1 -2
- package/package.json +11 -29
- package/pages/__tests__/readme.test.ts +49 -0
- package/pages/auth/setup.vue +2 -3
- package/pages/c/_cluster/apps/charts/__tests__/chart.test.ts +76 -0
- package/pages/c/_cluster/apps/charts/chart.vue +60 -8
- package/pages/c/_cluster/apps/charts/install.vue +10 -7
- package/pages/c/_cluster/explorer/__tests__/index.test.ts +23 -25
- package/pages/c/_cluster/explorer/index.vue +5 -49
- package/pages/c/_cluster/istio/__tests__/istio.index.test.ts +194 -0
- package/pages/c/_cluster/istio/index.vue +21 -6
- package/pages/c/_cluster/uiplugins/PluginInfoPanel.vue +1 -0
- package/pages/c/_cluster/uiplugins/__tests__/index.test.ts +719 -2
- package/pages/c/_cluster/uiplugins/index.vue +203 -197
- package/pages/diagnostic.vue +13 -17
- package/pages/fail-whale.vue +18 -0
- package/pages/home.vue +77 -260
- package/pages/readme.vue +88 -0
- package/plugins/dashboard-store/__tests__/resource-class.test.ts +88 -0
- package/plugins/dashboard-store/actions.js +40 -18
- package/plugins/dashboard-store/resource-class.js +5 -2
- package/plugins/steve/__tests__/subscribe.spec.ts +6 -3
- package/plugins/steve/steve-pagination-utils.ts +11 -3
- package/plugins/steve/subscribe.js +35 -5
- package/rancher-components/Form/LabeledInput/LabeledInput.test.ts +10 -4
- package/rancher-components/Form/LabeledInput/LabeledInput.vue +7 -52
- package/rancher-components/RcButton/RcButton.test.ts +37 -1
- package/rancher-components/RcButton/RcButton.vue +38 -8
- package/rancher-components/RcDropdown/RcDropdownTrigger.vue +10 -8
- package/store/__tests__/catalog.test.ts +115 -1
- package/store/__tests__/type-map.test.ts +556 -1
- package/store/action-menu.js +8 -3
- package/store/auth.js +1 -1
- package/store/aws.js +27 -16
- package/store/catalog.js +27 -3
- package/store/digitalocean.js +20 -38
- package/store/index.js +2 -0
- package/store/linode.js +25 -40
- package/store/pnap.js +1 -0
- package/store/type-map.js +111 -29
- package/tsconfig.paths.json +8 -8
- package/types/kube/kube-api.ts +14 -1
- package/types/rancher/steve.api.ts +12 -12
- package/types/resources/settings.d.ts +2 -1
- package/types/shell/index.d.ts +102 -2
- package/types/store/dashboard-store.types.ts +108 -11
- package/types/store/pagination.types.ts +6 -3
- package/utils/__tests__/alertmanagerconfig.test.ts +117 -0
- package/utils/__tests__/async.test.ts +87 -0
- package/utils/__tests__/aws.test.ts +140 -0
- package/utils/__tests__/banners.test.ts +176 -0
- package/utils/__tests__/chart.test.ts +64 -1
- package/utils/__tests__/color.test.ts +226 -0
- package/utils/__tests__/duration.test.ts +140 -0
- package/utils/__tests__/fleet.test.ts +340 -0
- package/utils/__tests__/ingress.test.ts +553 -0
- package/utils/__tests__/kube.test.ts +68 -0
- package/utils/__tests__/namespace-filter.test.ts +109 -0
- package/utils/__tests__/pagination-utils.test.ts +361 -0
- package/utils/__tests__/parse-externalid.test.ts +137 -0
- package/utils/__tests__/perf-setting.utils.test.ts +98 -0
- package/utils/__tests__/poller-sequential.test.ts +177 -0
- package/utils/__tests__/poller.test.ts +170 -0
- package/utils/__tests__/promise.test.ts +346 -0
- package/utils/__tests__/settings.test.ts +140 -0
- package/utils/__tests__/sort-utils.test.ts +301 -0
- package/utils/__tests__/string-utils.test.ts +798 -0
- package/utils/__tests__/string.test.ts +23 -1
- package/utils/__tests__/style.test.ts +154 -0
- package/utils/__tests__/svg-filter.test.ts +184 -0
- package/utils/__tests__/units.test.ts +417 -0
- package/utils/__tests__/versions.test.ts +128 -0
- package/utils/__tests__/xccdf.test.ts +391 -0
- package/utils/chart.js +36 -0
- package/utils/fleet.ts +13 -3
- package/utils/gatekeeper/__tests__/util.test.ts +174 -0
- package/utils/gc/__tests__/gc-interval.test.ts +119 -0
- package/utils/gc/__tests__/gc-root-store.test.ts +225 -0
- package/utils/gc/__tests__/gc-route-changed.test.ts +96 -0
- package/utils/gc/__tests__/gc.test.ts +487 -0
- package/utils/ingress.ts +9 -1
- package/utils/pagination-utils.ts +2 -1
- package/utils/string.js +25 -2
- package/utils/uiplugins.ts +5 -5
- package/utils/validators/__tests__/cluster-name.test.ts +110 -0
- package/utils/validators/__tests__/cron-schedule.test.ts +79 -0
- package/utils/validators/__tests__/index.test.ts +481 -0
- package/utils/validators/__tests__/kubernetes-name.test.ts +163 -0
- package/utils/validators/__tests__/misc-validators.test.ts +246 -0
- package/utils/validators/__tests__/pod-affinity.test.ts +382 -0
- package/utils/validators/__tests__/prometheusrule.test.ts +211 -0
- package/utils/validators/__tests__/role-template.test.ts +149 -0
- package/utils/validators/__tests__/service.test.ts +283 -0
- package/utils/validators/__tests__/setting.test.js +32 -0
- package/utils/validators/formRules/__tests__/index.test.ts +50 -0
- package/utils/validators/formRules/index.ts +5 -5
- package/utils/validators/machine-pool.ts +1 -1
- package/utils/validators/setting.js +18 -3
- package/utils/xccdf.ts +418 -0
- package/assets/fonts/lato/lato-v17-latin-700.woff +0 -0
- package/assets/fonts/lato/lato-v17-latin-700.woff2 +0 -0
- package/assets/fonts/lato/lato-v17-latin-regular.woff +0 -0
- 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 {
|
|
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 && !
|
|
175
|
+
const url: Validator = (val: string) => val && !isValidUrl(val) ? t('validation.setting.serverUrl.url') : undefined;
|
|
176
176
|
|
|
177
|
-
const genericUrl: Validator = (val: string) => val && !
|
|
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');
|
|
@@ -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
|
-
|
|
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('/');
|