Merge pull request #14319 from torchiaf/14317-url-validation

Fleet improve Repository URL and OCI registry validation
This commit is contained in:
Francesco Torchia 2025-07-30 14:35:22 +02:00 committed by GitHub
commit 68041cc26c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 110 additions and 24 deletions

View File

@ -6761,10 +6761,10 @@ validation:
flowOutput:
both: Requires "Output" or "Cluster Output" to be selected.
global: Requires "Cluster Output" to be selected.
git:
url: URL must be a HTTP(s) or SSH url with no trailing spaces
repository:
url: It must be a valid HTTP(s) or SSH URL with no trailing spaces
oci:
url: URL must be an OCI url with no trailing spaces
url: It must be a valid OCI URL with no trailing spaces
output:
logdna:
apiKey: Required an "Api Key" to be set.

View File

@ -107,10 +107,7 @@ export default {
targetsCreated: '',
fvFormRuleSets: [{
path: 'spec.repo',
rules: [
'required',
'urlRepository'
],
rules: ['urlRepository'],
}],
touched: null,
};

View File

@ -404,7 +404,7 @@ export default {
case SOURCE_TYPE.REPO:
this.fvFormRuleSets = [{
path: 'spec.helm.repo',
rules: ['required', 'urlRepository'],
rules: ['urlRepository'],
}, {
path: 'spec.helm.chart',
rules: ['required'],
@ -416,7 +416,7 @@ export default {
case SOURCE_TYPE.OCI:
this.fvFormRuleSets = [{
path: 'spec.helm.repo',
rules: ['required', 'ociRegistry'],
rules: ['ociRegistry'],
}, {
path: 'spec.helm.version',
rules: ['semanticVersion'],
@ -425,7 +425,7 @@ export default {
case SOURCE_TYPE.TARBALL:
this.fvFormRuleSets = [{
path: 'spec.helm.chart',
rules: ['required', 'urlRepository'],
rules: ['urlRepository'],
}];
break;
}

View File

@ -97,32 +97,54 @@ describe('formRules', () => {
});
describe('urlRepository', () => {
const message = JSON.stringify({ message: 'validation.git.url' });
const message = JSON.stringify({ message: 'validation.repository.url' });
const testCases = [
// Valid HTTP(s)
['https://github.com/rancher/dashboard.git', undefined],
['http://github.com/rancher/dashboard.git', undefined],
['https://github.com/rancher/dashboard', undefined],
['https://github.com/rancher/dashboard/', undefined],
['https://github.com/rancher/%20dashboard/', undefined],
['https://github.com/rancher/dashboard/%20', undefined],
['https://localhost:8005', undefined],
// Valid SSH
['git@github.com:rancher/dashboard.git', undefined],
['git@github.com:rancher/dashboard', undefined],
['git@github.com:rancher/dashboard/', undefined],
['git@github.com:rancher/%20dashboard/', undefined],
['git@github.com:rancher/dashboard/%20', undefined],
// Not valid HTTP(s)
['https://github.com/rancher/ dashboard.git', message],
['http://github.com/rancher/ dashboard.git', message],
['http://github.com/ rancher/dashboard.git', message],
['http://github.com /rancher/dashboard.git', message],
['https://github.com/rancher/dashboard ', message],
['https%20://github.com/rancher/dashboard ', message],
['ht%20tps://github.com/rancher/dashboard ', message],
['https://git%20hub.com/rancher/dashboard/%20', message],
['https://https://', message],
['http:/ww.abc.com', message],
['http:ww.abc.com', message],
['foo://github.com/rancher/dashboard/', message],
['github.com/rancher/dashboard/', message],
// Not valid SSH
['git@github.com:rancher/ dashboard.git', message],
['git@github.com:rancher/dashboard ', message],
['git@github.com:rancher/ dashboard', message],
['git @github.com:rancher/dashboard', message],
['git@github.com: rancher/dashboard', message],
['git@github.comrancher/dashboard', message],
['git@githubcomrancher/dashboard', message],
['%20git@github.comrancher/dashboard', message],
['git@git%20hub.comrancher/dashboard', message],
['git@.git', message],
['git@', message],
[undefined, undefined]
[undefined, message],
['', message]
];
it.each(testCases)(
@ -139,20 +161,26 @@ describe('formRules', () => {
const message = JSON.stringify({ message: 'validation.oci.url' });
const testCases = [
// Valid
['oci://bucket/object', undefined],
['oci://registry.example.com', undefined],
['oci://myregistry.dev:5000', undefined],
['oci://192.168.1.100', undefined],
['oci://my.domain.com/my/image:tag', undefined],
['oci://localhost:5000', undefined],
['oci://region.objectstorage.example.com/n', undefined],
['oci://a', undefined],
['oci://UPPERCASE/path', undefined],
// Invalid
['http://example.com/oci', message],
['https://oci.cloud.com', message],
['ftp://oci.server.net', message],
['/path/to/oci', message],
['path/to/oci', message],
['oci://a', message],
['oci:/missing/slash', message],
['oci:', message],
['oci://', message],
['oci://space between', message],
['oci://oci://duplicate/protocol', message],
['oci ://registry.example.com/foo/bar', message],
['oci://registry.example. com/foo/bar', message],
['oci://registry.example.com/ foo/bar', message],
['oci://resource multiple spaces', message],
['', message],
[undefined, message],

View File

@ -1,4 +1,5 @@
import semver from 'semver';
import { parse } from '@shell/utils/url';
import { RBAC } from '@shell/config/types';
import { HCI } from '@shell/config/labels-annotations';
import isEmpty from 'lodash/isEmpty';
@ -174,22 +175,82 @@ export default function(
const genericUrl: Validator = (val: string) => val && !isUrl(val) ? t('validation.genericUrl') : undefined;
const urlRepository: Validator = (url: string) => {
const regexPart1 = /^((http|git|ssh|http(s)|file|\/?)|(git@[\w\.]+))(:(\/\/)?)/gm;
const regexPart2 = /^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(.git){0,1}(\/)?$/gm;
const message = t('validation.repository.url');
if (url) {
const urlPart2 = url.replaceAll(regexPart1, '');
if (!url) {
return message;
}
return !urlPart2 || url === urlPart2 || !regexPart2.test(urlPart2.replaceAll('%20', '')) ? t('validation.git.url') : undefined;
if (url.includes(' ')) {
return message;
}
const {
protocol,
authority,
host,
path
} = parse(url);
// Test duplicate protocol
if (!host || protocol === host) {
return message;
}
// Test http(s) protocol
if (protocol && (!/^(http|http(s))/gm.test(protocol) || (!url.startsWith('https://') && !url.startsWith('http://')))) {
return message;
}
// Test ssh, authority must be valid (SSH user + host)
if (!protocol && !authority.endsWith(':')) {
return message;
}
// Encoded space characters (%20) are allowed only in the path
const hostAndPath = `${ host }${ path.replaceAll('%20', '') }`;
// Test host/path
if (!/^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(.git){0,1}(\/)?$/gm.test(hostAndPath)) {
return message;
}
return undefined;
};
const ociRegistry: Validator = (url: string) => {
const regex = /^oci:\/\/\S+$/gm;
const message = t('validation.oci.url');
return !regex.test(url) ? t('validation.oci.url') : undefined;
if (!url) {
return message;
}
if (url.includes(' ')) {
return message;
}
const {
protocol,
host,
path
} = parse(url);
// Test duplicate protocol
if (!host || protocol === host) {
return message;
}
// Test oci protocol
if (!url.startsWith('oci://')) {
return message;
}
// Test host/path
if (!/^([\w\.@\:\/\-]+)([\d\/\w.-]+?)(\/)?$/gm.test(`${ host }${ path }`)) {
return message;
}
return undefined;
};
const version: Validator = (value: string) => {