add validation for cluster api and remove cluster validating webhook
Signed-off-by: carlory <baofa.fan@daocloud.io>
This commit is contained in:
parent
2060616c4a
commit
f697c03c74
|
|
@ -69,20 +69,6 @@ metadata:
|
|||
labels:
|
||||
app: validating-config
|
||||
webhooks:
|
||||
- name: cluster.karmada.io
|
||||
rules:
|
||||
- operations: ["CREATE", "UPDATE"]
|
||||
apiGroups: ["cluster.karmada.io"]
|
||||
apiVersions: ["*"]
|
||||
resources: ["clusters"]
|
||||
scope: "Cluster"
|
||||
clientConfig:
|
||||
url: https://karmada-webhook.karmada-system.svc:443/validate-cluster
|
||||
caBundle: {{caBundle}}
|
||||
failurePolicy: Fail
|
||||
sideEffects: None
|
||||
admissionReviewVersions: ["v1"]
|
||||
timeoutSeconds: 3
|
||||
- name: propagationpolicy.karmada.io
|
||||
rules:
|
||||
- operations: ["CREATE", "UPDATE"]
|
||||
|
|
|
|||
|
|
@ -87,20 +87,6 @@ metadata:
|
|||
labels:
|
||||
app: validating-config
|
||||
webhooks:
|
||||
- name: cluster.karmada.io
|
||||
rules:
|
||||
- operations: ["CREATE", "UPDATE"]
|
||||
apiGroups: ["cluster.karmada.io"]
|
||||
apiVersions: ["*"]
|
||||
resources: ["clusters"]
|
||||
scope: "Cluster"
|
||||
clientConfig:
|
||||
url: https://{{ $name }}-webhook.{{ $namespace }}.svc:443/validate-cluster
|
||||
{{- include "karmada.webhook.caBundle" . | nindent 6 }}
|
||||
failurePolicy: Fail
|
||||
sideEffects: None
|
||||
admissionReviewVersions: ["v1"]
|
||||
timeoutSeconds: 3
|
||||
- name: propagationpolicy.karmada.io
|
||||
rules:
|
||||
- operations: ["CREATE", "UPDATE"]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import (
|
|||
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
"github.com/karmada-io/karmada/pkg/util/validation"
|
||||
"github.com/karmada-io/karmada/pkg/apis/cluster/validation"
|
||||
)
|
||||
|
||||
// Validate checks Options and return a slice of found errs.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import (
|
|||
"github.com/karmada-io/karmada/pkg/util/gclient"
|
||||
"github.com/karmada-io/karmada/pkg/version"
|
||||
"github.com/karmada-io/karmada/pkg/version/sharedcommand"
|
||||
"github.com/karmada-io/karmada/pkg/webhook/cluster"
|
||||
"github.com/karmada-io/karmada/pkg/webhook/clusteroverridepolicy"
|
||||
"github.com/karmada-io/karmada/pkg/webhook/clusterpropagationpolicy"
|
||||
"github.com/karmada-io/karmada/pkg/webhook/configuration"
|
||||
|
|
@ -84,7 +83,6 @@ func Run(ctx context.Context, opts *options.Options) error {
|
|||
|
||||
klog.Info("registering webhooks to the webhook server")
|
||||
hookServer := hookManager.GetWebhookServer()
|
||||
hookServer.Register("/validate-cluster", &webhook.Admission{Handler: &cluster.ValidatingAdmission{}})
|
||||
hookServer.Register("/mutate-propagationpolicy", &webhook.Admission{Handler: &propagationpolicy.MutatingAdmission{}})
|
||||
hookServer.Register("/validate-propagationpolicy", &webhook.Admission{Handler: &propagationpolicy.ValidatingAdmission{}})
|
||||
hookServer.Register("/mutate-clusterpropagationpolicy", &webhook.Admission{Handler: &clusterpropagationpolicy.MutatingAdmission{}})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2014 The Kubernetes Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
)
|
||||
|
||||
// This code logic is lifted from https://github.com/kubernetes/kubernetes/blob/master/pkg/apis/core/validation/validation.go#L5003
|
||||
|
||||
// ValidateClusterTaints tests if given taints have valid data.
|
||||
func ValidateClusterTaints(taints []corev1.Taint, fldPath *field.Path) field.ErrorList {
|
||||
allErrors := field.ErrorList{}
|
||||
|
||||
uniqueTaints := map[corev1.TaintEffect]sets.String{}
|
||||
|
||||
for i, currTaint := range taints {
|
||||
idxPath := fldPath.Index(i)
|
||||
// validate the taint key
|
||||
allErrors = append(allErrors, unversionedvalidation.ValidateLabelName(currTaint.Key, idxPath.Child("key"))...)
|
||||
// validate the taint value
|
||||
if errs := validation.IsValidLabelValue(currTaint.Value); len(errs) != 0 {
|
||||
allErrors = append(allErrors, field.Invalid(idxPath.Child("value"), currTaint.Value, strings.Join(errs, ";")))
|
||||
}
|
||||
// validate the taint effect
|
||||
allErrors = append(allErrors, validateTaintEffect(&currTaint.Effect, false, idxPath.Child("effect"))...)
|
||||
|
||||
// validate if taint is unique by <key, effect>
|
||||
if len(uniqueTaints[currTaint.Effect]) > 0 && uniqueTaints[currTaint.Effect].Has(currTaint.Key) {
|
||||
duplicatedError := field.Duplicate(idxPath, currTaint)
|
||||
duplicatedError.Detail = "taints must be unique by key and effect pair"
|
||||
allErrors = append(allErrors, duplicatedError)
|
||||
continue
|
||||
}
|
||||
|
||||
// add taint to existingTaints for uniqueness check
|
||||
if len(uniqueTaints[currTaint.Effect]) == 0 {
|
||||
uniqueTaints[currTaint.Effect] = sets.String{}
|
||||
}
|
||||
uniqueTaints[currTaint.Effect].Insert(currTaint.Key)
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateTaintEffect(effect *corev1.TaintEffect, allowEmpty bool, fldPath *field.Path) field.ErrorList {
|
||||
if !allowEmpty && len(*effect) == 0 {
|
||||
return field.ErrorList{field.Required(fldPath, "")}
|
||||
}
|
||||
|
||||
allErrors := field.ErrorList{}
|
||||
switch *effect {
|
||||
// TODO: Replace next line with subsequent commented-out line when implement TaintEffectNoExecute.
|
||||
case corev1.TaintEffectNoSchedule:
|
||||
// case corev1.TaintEffectNoSchedule, corev1.TaintEffectNoExecute:
|
||||
default:
|
||||
validValues := []string{
|
||||
string(corev1.TaintEffectNoSchedule),
|
||||
// TODO: Uncomment this block when implement TaintEffectNoExecute.
|
||||
// string(corev1.TaintEffectNoExecute),
|
||||
}
|
||||
allErrors = append(allErrors, field.NotSupported(fldPath, *effect, validValues))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
kubevalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
api "github.com/karmada-io/karmada/pkg/apis/cluster"
|
||||
)
|
||||
|
||||
const clusterNameMaxLength int = 48
|
||||
|
||||
// ValidateClusterName tests whether the cluster name passed is valid.
|
||||
// If the cluster name is not valid, a list of error strings is returned. Otherwise an empty list (or nil) is returned.
|
||||
// Rules of a valid cluster name:
|
||||
// - Must be a valid label value as per RFC1123.
|
||||
// * An alphanumeric (a-z, and 0-9) string, with a maximum length of 63 characters,
|
||||
// with the '-' character allowed anywhere except the first or last character.
|
||||
// - Length must be less than 48 characters.
|
||||
// * Since cluster name used to generate execution namespace by adding a prefix, so reserve 15 characters for the prefix.
|
||||
func ValidateClusterName(name string) []string {
|
||||
if len(name) == 0 {
|
||||
return []string{"must be not empty"}
|
||||
}
|
||||
if len(name) > clusterNameMaxLength {
|
||||
return []string{fmt.Sprintf("must be no more than %d characters", clusterNameMaxLength)}
|
||||
}
|
||||
|
||||
return kubevalidation.IsDNS1123Label(name)
|
||||
}
|
||||
|
||||
var supportedSyncModes = sets.NewString(string(api.Pull), string(api.Push))
|
||||
|
||||
// ValidateCluster tests if required fields in the Cluster are set.
|
||||
func ValidateCluster(cluster *api.Cluster) field.ErrorList {
|
||||
allErrs := apimachineryvalidation.ValidateObjectMeta(&cluster.ObjectMeta, false, func(name string, prefix bool) []string { return ValidateClusterName(name) }, field.NewPath("metadata"))
|
||||
allErrs = append(allErrs, ValidateClusterSpec(&cluster.Spec, field.NewPath("spec"))...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateClusterUpdate tests if required fields in the Cluster are set.
|
||||
func ValidateClusterUpdate(newCluster, oldCluster *api.Cluster) field.ErrorList {
|
||||
allErrs := apimachineryvalidation.ValidateObjectMetaUpdate(&newCluster.ObjectMeta, &oldCluster.ObjectMeta, field.NewPath("metadata"))
|
||||
allErrs = append(allErrs, ValidateCluster(newCluster)...)
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateClusterSpec tests if required fields in the ClusterSpec are set.
|
||||
func ValidateClusterSpec(spec *api.ClusterSpec, fldPath *field.Path) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if spec.SyncMode == "" {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("syncMode"), ""))
|
||||
}
|
||||
if !supportedSyncModes.Has(string(spec.SyncMode)) {
|
||||
allErrs = append(allErrs, field.NotSupported(fldPath.Child("syncMode"), spec.SyncMode, supportedSyncModes.List()))
|
||||
}
|
||||
|
||||
if spec.APIEndpoint != "" {
|
||||
allErrs = append(allErrs, ValidateClusterAPIEndpoint(fldPath.Child("apiEndpoint"), spec.APIEndpoint, false)...)
|
||||
}
|
||||
|
||||
if spec.SecretRef != nil {
|
||||
if spec.SecretRef.Namespace == "" {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("secretRef").Child("namespace"), ""))
|
||||
}
|
||||
if spec.SecretRef.Name == "" {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("secretRef").Child("name"), ""))
|
||||
}
|
||||
}
|
||||
|
||||
if spec.ImpersonatorSecretRef != nil {
|
||||
if spec.ImpersonatorSecretRef.Namespace == "" {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("impersonatorSecretRef").Child("namespace"), ""))
|
||||
}
|
||||
if spec.ImpersonatorSecretRef.Name == "" {
|
||||
allErrs = append(allErrs, field.Required(fldPath.Child("impersonatorSecretRef").Child("name"), ""))
|
||||
}
|
||||
}
|
||||
|
||||
if spec.ProxyURL != "" {
|
||||
allErrs = append(allErrs, ValidateClusterProxyURL(fldPath.Child("proxyURL"), spec.ProxyURL)...)
|
||||
}
|
||||
|
||||
if len(spec.Taints) > 0 {
|
||||
allErrs = append(allErrs, ValidateClusterTaints(spec.Taints, fldPath.Child("taints"))...)
|
||||
}
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateClusterAPIEndpoint validates cluster's apiEndpoint
|
||||
func ValidateClusterAPIEndpoint(fldPath *field.Path, apiEndpoint string, forceHTTPS bool) field.ErrorList {
|
||||
var allErrs field.ErrorList
|
||||
const form = "; desired format: hostname, hostname:port, IP or IP:port"
|
||||
if u, err := url.Parse(apiEndpoint); err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, apiEndpoint, "apiEndpoint must be a valid URL: "+err.Error()+form))
|
||||
} else {
|
||||
if forceHTTPS && u.Scheme != "https" {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, u.Scheme, "'https' is the only allowed URL scheme"+form))
|
||||
}
|
||||
if len(u.Host) == 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, u.Host, "host must be provided"+form))
|
||||
}
|
||||
if u.User != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, u.User.String(), "user information is not permitted in the URL"))
|
||||
}
|
||||
if len(u.Fragment) != 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, u.Fragment, "fragments are not permitted in the URL"))
|
||||
}
|
||||
if len(u.RawQuery) != 0 {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, u.RawQuery, "query parameters are not permitted in the URL"))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateClusterProxyURL validates cluster's proxyURL.
|
||||
func ValidateClusterProxyURL(fldPath *field.Path, proxyURL string) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
if u, err := url.Parse(proxyURL); err != nil {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, proxyURL, "apiEndpoint must be a valid URL: "+err.Error()))
|
||||
} else {
|
||||
switch u.Scheme {
|
||||
case "http", "https", "socks5":
|
||||
default:
|
||||
allErrs = append(allErrs, field.Invalid(fldPath, proxyURL, fmt.Sprintf("unsupported scheme %q, must be http, https, or socks5", u.Scheme)))
|
||||
}
|
||||
}
|
||||
return allErrs
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package validation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
api "github.com/karmada-io/karmada/pkg/apis/cluster"
|
||||
)
|
||||
|
||||
func TestValidateCluster(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
cluster api.Cluster
|
||||
expectError bool
|
||||
}{
|
||||
"zero-length name": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: ""}, Spec: api.ClusterSpec{SyncMode: api.Push}},
|
||||
expectError: true,
|
||||
},
|
||||
"invalid name": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "^Invalid"}, Spec: api.ClusterSpec{SyncMode: api.Push}},
|
||||
expectError: true,
|
||||
},
|
||||
"invalid name that is too long": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: strings.Repeat("a", 48+1)}, Spec: api.ClusterSpec{SyncMode: api.Push}},
|
||||
expectError: true,
|
||||
},
|
||||
"no sync mode": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ClusterSpec{}},
|
||||
expectError: true,
|
||||
},
|
||||
"unsupported sync mode": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ClusterSpec{SyncMode: api.ClusterSyncMode("^Invalid")}},
|
||||
expectError: true,
|
||||
},
|
||||
"invalid apiEndpoint": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ClusterSpec{SyncMode: api.Push, APIEndpoint: "^Invalid"}},
|
||||
expectError: true,
|
||||
},
|
||||
"empty secretRef": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ClusterSpec{SyncMode: api.Push, SecretRef: &api.LocalSecretReference{}}},
|
||||
expectError: true,
|
||||
},
|
||||
"empty impersonatorSecretRef": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ClusterSpec{SyncMode: api.Push, ImpersonatorSecretRef: &api.LocalSecretReference{}}},
|
||||
expectError: true,
|
||||
},
|
||||
"invalid proxyURL": {
|
||||
cluster: api.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: api.ClusterSpec{SyncMode: api.Push, ProxyURL: "^Invalid"}},
|
||||
expectError: true,
|
||||
},
|
||||
"unsupported taint effect": {
|
||||
cluster: api.Cluster{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
|
||||
Spec: api.ClusterSpec{
|
||||
SyncMode: api.Push,
|
||||
Taints: []corev1.Taint{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: "bar",
|
||||
Effect: corev1.TaintEffect("^Invalid"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
errs := ValidateCluster(&testCase.cluster)
|
||||
if len(errs) == 0 && testCase.expectError {
|
||||
t.Errorf("expected failure for %q, but there were none", name)
|
||||
return
|
||||
}
|
||||
if len(errs) != 0 && !testCase.expectError {
|
||||
t.Errorf("expected success for %q, but there were errors: %v", name, errs)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -87,20 +87,6 @@ metadata:
|
|||
labels:
|
||||
app: validating-config
|
||||
webhooks:
|
||||
- name: cluster.karmada.io
|
||||
rules:
|
||||
- operations: ["CREATE", "UPDATE"]
|
||||
apiGroups: ["cluster.karmada.io"]
|
||||
apiVersions: ["*"]
|
||||
resources: ["clusters"]
|
||||
scope: "Cluster"
|
||||
clientConfig:
|
||||
url: https://karmada-webhook.%s.svc:443/validate-cluster
|
||||
caBundle: %s
|
||||
failurePolicy: Fail
|
||||
sideEffects: None
|
||||
admissionReviewVersions: ["v1"]
|
||||
timeoutSeconds: 3
|
||||
- name: propagationpolicy.karmada.io
|
||||
rules:
|
||||
- operations: ["CREATE", "UPDATE"]
|
||||
|
|
@ -170,7 +156,7 @@ webhooks:
|
|||
failurePolicy: Fail
|
||||
sideEffects: None
|
||||
admissionReviewVersions: ["v1"]
|
||||
timeoutSeconds: 3`, namespace, caBundle, namespace, caBundle, namespace, caBundle, namespace, caBundle, namespace, caBundle, namespace, caBundle)
|
||||
timeoutSeconds: 3`, namespace, caBundle, namespace, caBundle, namespace, caBundle, namespace, caBundle, namespace, caBundle)
|
||||
}
|
||||
|
||||
func createValidatingWebhookConfiguration(c *kubernetes.Clientset, staticYaml string) error {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
|
||||
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
|
||||
"github.com/karmada-io/karmada/pkg/apis/cluster/validation"
|
||||
karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
|
||||
"github.com/karmada-io/karmada/pkg/karmadactl/options"
|
||||
"github.com/karmada-io/karmada/pkg/util"
|
||||
"github.com/karmada-io/karmada/pkg/util/names"
|
||||
"github.com/karmada-io/karmada/pkg/util/validation"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
|
||||
clusterapis "github.com/karmada-io/karmada/pkg/apis/cluster"
|
||||
"github.com/karmada-io/karmada/pkg/apis/cluster/validation"
|
||||
)
|
||||
|
||||
// NewStrategy creates and returns a ClusterStrategy instance.
|
||||
|
|
@ -76,8 +77,8 @@ func (Strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
|||
|
||||
// Validate returns an ErrorList with validation errors or nil.
|
||||
func (Strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
// TODO: add validation for Cluster
|
||||
return field.ErrorList{}
|
||||
cluster := obj.(*clusterapis.Cluster)
|
||||
return validation.ValidateCluster(cluster)
|
||||
}
|
||||
|
||||
// WarningsOnCreate returns warnings for the creation of the given object.
|
||||
|
|
@ -102,7 +103,9 @@ func (Strategy) Canonicalize(obj runtime.Object) {
|
|||
// ValidateUpdate is invoked after default fields in the object have been
|
||||
// filled in before the object is persisted.
|
||||
func (Strategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
return field.ErrorList{}
|
||||
newCluster := obj.(*clusterapis.Cluster)
|
||||
oldCluster := old.(*clusterapis.Cluster)
|
||||
return validation.ValidateClusterUpdate(newCluster, oldCluster)
|
||||
}
|
||||
|
||||
// WarningsOnUpdate returns warnings for the given update.
|
||||
|
|
|
|||
|
|
@ -2,56 +2,16 @@ package validation
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kubevalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
|
||||
"github.com/karmada-io/karmada/pkg/util"
|
||||
)
|
||||
|
||||
const clusterNameMaxLength int = 48
|
||||
|
||||
// LabelValueMaxLength is a label's max length
|
||||
const LabelValueMaxLength int = 63
|
||||
|
||||
// ValidateClusterName tests whether the cluster name passed is valid.
|
||||
// If the cluster name is not valid, a list of error strings is returned. Otherwise an empty list (or nil) is returned.
|
||||
// Rules of a valid cluster name:
|
||||
// - Must be a valid label value as per RFC1123.
|
||||
// * An alphanumeric (a-z, and 0-9) string, with a maximum length of 63 characters,
|
||||
// with the '-' character allowed anywhere except the first or last character.
|
||||
// - Length must be less than 48 characters.
|
||||
// * Since cluster name used to generate execution namespace by adding a prefix, so reserve 15 characters for the prefix.
|
||||
func ValidateClusterName(name string) []string {
|
||||
if len(name) == 0 {
|
||||
return []string{"must be not empty"}
|
||||
}
|
||||
if len(name) > clusterNameMaxLength {
|
||||
return []string{fmt.Sprintf("must be no more than %d characters", clusterNameMaxLength)}
|
||||
}
|
||||
|
||||
return kubevalidation.IsDNS1123Label(name)
|
||||
}
|
||||
|
||||
// ValidateClusterProxyURL tests whether the proxyURL is valid.
|
||||
// If not valid, a list of error string is returned. Otherwise an empty list (or nil) is returned.
|
||||
func ValidateClusterProxyURL(proxyURL string) []string {
|
||||
u, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return []string{fmt.Sprintf("cloud not parse: %s, %v", proxyURL, err)}
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "http", "https", "socks5":
|
||||
default:
|
||||
return []string{fmt.Sprintf("unsupported scheme %q, must be http, https, or socks5", u.Scheme)}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePolicyFieldSelector tests if the fieldSelector of propagation policy is valid.
|
||||
func ValidatePolicyFieldSelector(fieldSelector *policyv1alpha1.FieldSelector) error {
|
||||
if fieldSelector == nil {
|
||||
|
|
|
|||
|
|
@ -1,101 +1,11 @@
|
|||
package validation
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
|
||||
)
|
||||
|
||||
func TestValidateClusterName(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
cluster string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "valid cluster",
|
||||
cluster: "valid-cluster",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "contains invalid character is not allowed",
|
||||
cluster: "invalid.cluster",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "empty name is not allowed",
|
||||
cluster: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "too long name is not allowed",
|
||||
cluster: "abcdefghijklmnopqrstuvwxyz01234567890123456789012", // 49 characters
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tc := test
|
||||
errs := ValidateClusterName(tc.cluster)
|
||||
if len(errs) > 0 && tc.expectError != true {
|
||||
t.Fatalf("expect no error but got: %s", strings.Join(errs, ";"))
|
||||
}
|
||||
if len(errs) == 0 && tc.expectError == true {
|
||||
t.Fatalf("expect an error but got none")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClusterProxyURL(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
proxy string
|
||||
expectError bool
|
||||
expectErrMsg string
|
||||
}{
|
||||
{
|
||||
name: "valid http",
|
||||
proxy: "http://example.com",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid https",
|
||||
proxy: "https://example.com",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "valid socks5",
|
||||
proxy: "socks5://example.com",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "no schema is not allowed",
|
||||
proxy: "example",
|
||||
expectError: true,
|
||||
expectErrMsg: `unsupported scheme "", must be http, https, or socks5`,
|
||||
},
|
||||
{
|
||||
name: "schema out of range is not allowed",
|
||||
proxy: "socks4://example.com",
|
||||
expectError: true,
|
||||
expectErrMsg: `unsupported scheme "socks4", must be http, https, or socks5`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
tc := test
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
errs := ValidateClusterProxyURL(tc.proxy)
|
||||
if !tc.expectError && len(errs) != 0 {
|
||||
t.Errorf("not expect errors but got: %v", errs)
|
||||
} else if tc.expectError && tc.expectErrMsg != strings.Join(errs, ",") {
|
||||
t.Errorf("expected error: %v, but got: %v", tc.expectErrMsg, strings.Join(errs, ","))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOverrideSpec(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
|
||||
clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1"
|
||||
"github.com/karmada-io/karmada/pkg/util/validation"
|
||||
)
|
||||
|
||||
// ValidatingAdmission validates cluster object when creating/updating/deleting.
|
||||
type ValidatingAdmission struct {
|
||||
decoder *admission.Decoder
|
||||
}
|
||||
|
||||
// Check if our ValidatingAdmission implements necessary interface
|
||||
var _ admission.Handler = &ValidatingAdmission{}
|
||||
var _ admission.DecoderInjector = &ValidatingAdmission{}
|
||||
|
||||
// Handle implements admission.Handler interface.
|
||||
// It yields a response to an AdmissionRequest.
|
||||
func (v *ValidatingAdmission) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
cluster := &clusterv1alpha1.Cluster{}
|
||||
|
||||
err := v.decoder.Decode(req, cluster)
|
||||
if err != nil {
|
||||
return admission.Errored(http.StatusBadRequest, err)
|
||||
}
|
||||
klog.V(2).Infof("Validating cluster(%s) for request: %s", cluster.Name, req.Operation)
|
||||
|
||||
if errs := validation.ValidateClusterName(cluster.Name); len(errs) != 0 {
|
||||
errMsg := fmt.Sprintf("invalid cluster name(%s): %s", cluster.Name, strings.Join(errs, ";"))
|
||||
klog.Error(errMsg)
|
||||
return admission.Denied(errMsg)
|
||||
}
|
||||
|
||||
if len(cluster.Spec.ProxyURL) > 0 {
|
||||
if errs := validation.ValidateClusterProxyURL(cluster.Spec.ProxyURL); len(errs) != 0 {
|
||||
errMsg := fmt.Sprintf("invalid proxy URL(%s): %s", cluster.Spec.ProxyURL, strings.Join(errs, ";"))
|
||||
klog.Error(errMsg)
|
||||
return admission.Denied(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
return admission.Allowed("")
|
||||
}
|
||||
|
||||
// InjectDecoder implements admission.DecoderInjector interface.
|
||||
// A decoder will be automatically injected.
|
||||
func (v *ValidatingAdmission) InjectDecoder(d *admission.Decoder) error {
|
||||
v.decoder = d
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in New Issue