mirror of
https://github.com/ceph/ceph-csi.git
synced 2025-06-14 18:53:35 +00:00
vendor files
This commit is contained in:
45
vendor/k8s.io/kubernetes/pkg/apis/admissionregistration/validation/BUILD
generated
vendored
Normal file
45
vendor/k8s.io/kubernetes/pkg/apis/admissionregistration/validation/BUILD
generated
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["validation_test.go"],
|
||||
importpath = "k8s.io/kubernetes/pkg/apis/admissionregistration/validation",
|
||||
library = ":go_default_library",
|
||||
deps = [
|
||||
"//pkg/apis/admissionregistration:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["validation.go"],
|
||||
importpath = "k8s.io/kubernetes/pkg/apis/admissionregistration/validation",
|
||||
deps = [
|
||||
"//pkg/apis/admissionregistration:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/validation:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
335
vendor/k8s.io/kubernetes/pkg/apis/admissionregistration/validation/validation.go
generated
vendored
Normal file
335
vendor/k8s.io/kubernetes/pkg/apis/admissionregistration/validation/validation.go
generated
vendored
Normal file
@ -0,0 +1,335 @@
|
||||
/*
|
||||
Copyright 2017 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 (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
metav1validation "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"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
func ValidateInitializerConfiguration(ic *admissionregistration.InitializerConfiguration) field.ErrorList {
|
||||
allErrors := genericvalidation.ValidateObjectMeta(&ic.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
||||
for i, initializer := range ic.Initializers {
|
||||
allErrors = append(allErrors, validateInitializer(&initializer, field.NewPath("initializers").Index(i))...)
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateInitializer(initializer *admissionregistration.Initializer, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
// initlializer.Name must be fully qualified
|
||||
allErrors = append(allErrors, validation.IsFullyQualifiedName(fldPath.Child("name"), initializer.Name)...)
|
||||
|
||||
for i, rule := range initializer.Rules {
|
||||
notAllowSubresources := false
|
||||
allErrors = append(allErrors, validateRule(&rule, fldPath.Child("rules").Index(i), notAllowSubresources)...)
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func hasWildcard(slice []string) bool {
|
||||
for _, s := range slice {
|
||||
if s == "*" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validateResources(resources []string, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(resources) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath, ""))
|
||||
}
|
||||
|
||||
// */x
|
||||
resourcesWithWildcardSubresoures := sets.String{}
|
||||
// x/*
|
||||
subResoucesWithWildcardResource := sets.String{}
|
||||
// */*
|
||||
hasDoubleWildcard := false
|
||||
// *
|
||||
hasSingleWildcard := false
|
||||
// x
|
||||
hasResourceWithoutSubresource := false
|
||||
|
||||
for i, resSub := range resources {
|
||||
if resSub == "" {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Index(i), ""))
|
||||
continue
|
||||
}
|
||||
if resSub == "*/*" {
|
||||
hasDoubleWildcard = true
|
||||
}
|
||||
if resSub == "*" {
|
||||
hasSingleWildcard = true
|
||||
}
|
||||
parts := strings.SplitN(resSub, "/", 2)
|
||||
if len(parts) == 1 {
|
||||
hasResourceWithoutSubresource = resSub != "*"
|
||||
continue
|
||||
}
|
||||
res, sub := parts[0], parts[1]
|
||||
if _, ok := resourcesWithWildcardSubresoures[res]; ok {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resSub, fmt.Sprintf("if '%s/*' is present, must not specify %s", res, resSub)))
|
||||
}
|
||||
if _, ok := subResoucesWithWildcardResource[sub]; ok {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resSub, fmt.Sprintf("if '*/%s' is present, must not specify %s", sub, resSub)))
|
||||
}
|
||||
if sub == "*" {
|
||||
resourcesWithWildcardSubresoures[res] = struct{}{}
|
||||
}
|
||||
if res == "*" {
|
||||
subResoucesWithWildcardResource[sub] = struct{}{}
|
||||
}
|
||||
}
|
||||
if len(resources) > 1 && hasDoubleWildcard {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*/*' is present, must not specify other resources"))
|
||||
}
|
||||
if hasSingleWildcard && hasResourceWithoutSubresource {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*' is present, must not specify other resources without subresources"))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateResourcesNoSubResources(resources []string, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(resources) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath, ""))
|
||||
}
|
||||
for i, resource := range resources {
|
||||
if resource == "" {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Index(i), ""))
|
||||
}
|
||||
if strings.Contains(resource, "/") {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resource, "must not specify subresources"))
|
||||
}
|
||||
}
|
||||
if len(resources) > 1 && hasWildcard(resources) {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*' is present, must not specify other resources"))
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateRule(rule *admissionregistration.Rule, fldPath *field.Path, allowSubResource bool) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(rule.APIGroups) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("apiGroups"), ""))
|
||||
}
|
||||
if len(rule.APIGroups) > 1 && hasWildcard(rule.APIGroups) {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("apiGroups"), rule.APIGroups, "if '*' is present, must not specify other API groups"))
|
||||
}
|
||||
// Note: group could be empty, e.g., the legacy "v1" API
|
||||
if len(rule.APIVersions) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("apiVersions"), ""))
|
||||
}
|
||||
if len(rule.APIVersions) > 1 && hasWildcard(rule.APIVersions) {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersions"), rule.APIVersions, "if '*' is present, must not specify other API versions"))
|
||||
}
|
||||
for i, version := range rule.APIVersions {
|
||||
if version == "" {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("apiVersions").Index(i), ""))
|
||||
}
|
||||
}
|
||||
if allowSubResource {
|
||||
allErrors = append(allErrors, validateResources(rule.Resources, fldPath.Child("resources"))...)
|
||||
} else {
|
||||
allErrors = append(allErrors, validateResourcesNoSubResources(rule.Resources, fldPath.Child("resources"))...)
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func ValidateInitializerConfigurationUpdate(newIC, oldIC *admissionregistration.InitializerConfiguration) field.ErrorList {
|
||||
return ValidateInitializerConfiguration(newIC)
|
||||
}
|
||||
|
||||
func ValidateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList {
|
||||
allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
||||
for i, hook := range e.Webhooks {
|
||||
allErrors = append(allErrors, validateWebhook(&hook, field.NewPath("webhooks").Index(i))...)
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration) field.ErrorList {
|
||||
allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
||||
for i, hook := range e.Webhooks {
|
||||
allErrors = append(allErrors, validateWebhook(&hook, field.NewPath("webhooks").Index(i))...)
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateWebhook(hook *admissionregistration.Webhook, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
// hook.Name must be fully qualified
|
||||
allErrors = append(allErrors, validation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...)
|
||||
|
||||
for i, rule := range hook.Rules {
|
||||
allErrors = append(allErrors, validateRuleWithOperations(&rule, fldPath.Child("rules").Index(i))...)
|
||||
}
|
||||
if hook.FailurePolicy != nil && !supportedFailurePolicies.Has(string(*hook.FailurePolicy)) {
|
||||
allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List()))
|
||||
}
|
||||
|
||||
if hook.NamespaceSelector != nil {
|
||||
allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
|
||||
}
|
||||
|
||||
allErrors = append(allErrors, validateWebhookClientConfig(fldPath.Child("clientConfig"), &hook.ClientConfig)...)
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateWebhookClientConfig(fldPath *field.Path, cc *admissionregistration.WebhookClientConfig) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if (cc.URL == nil) == (cc.Service == nil) {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("url"), "exactly one of url or service is required"))
|
||||
}
|
||||
|
||||
if cc.URL != nil {
|
||||
const form = "; desired format: https://host[/path]"
|
||||
if u, err := url.Parse(*cc.URL); err != nil {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("url"), "url must be a valid URL: "+err.Error()+form))
|
||||
} else {
|
||||
if u.Scheme != "https" {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.Scheme, "'https' is the only allowed URL scheme"+form))
|
||||
}
|
||||
if len(u.Host) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.Host, "host must be provided"+form))
|
||||
}
|
||||
if u.User != nil {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.User.String(), "user information is not permitted in the URL"))
|
||||
}
|
||||
if len(u.Fragment) != 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.Fragment, "fragments are not permitted in the URL"))
|
||||
}
|
||||
if len(u.RawQuery) != 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("url"), u.RawQuery, "query parameters are not permitted in the URL"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cc.Service != nil {
|
||||
allErrors = append(allErrors, validateWebhookService(fldPath.Child("service"), cc.Service)...)
|
||||
}
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateWebhookService(fldPath *field.Path, svc *admissionregistration.ServiceReference) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
|
||||
if len(svc.Name) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("name"), "service name is required"))
|
||||
}
|
||||
|
||||
if len(svc.Namespace) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("namespace"), "service namespace is required"))
|
||||
}
|
||||
|
||||
if svc.Path == nil {
|
||||
return allErrors
|
||||
}
|
||||
|
||||
// TODO: replace below with url.Parse + verifying that host is empty?
|
||||
|
||||
urlPath := *svc.Path
|
||||
if urlPath == "/" || len(urlPath) == 0 {
|
||||
return allErrors
|
||||
}
|
||||
if urlPath == "//" {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "segment[0] may not be empty"))
|
||||
return allErrors
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(urlPath, "/") {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "must start with a '/'"))
|
||||
}
|
||||
|
||||
urlPathToCheck := urlPath[1:]
|
||||
if strings.HasSuffix(urlPathToCheck, "/") {
|
||||
urlPathToCheck = urlPathToCheck[:len(urlPathToCheck)-1]
|
||||
}
|
||||
steps := strings.Split(urlPathToCheck, "/")
|
||||
for i, step := range steps {
|
||||
if len(step) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d] may not be empty", i)))
|
||||
continue
|
||||
}
|
||||
failures := validation.IsDNS1123Subdomain(step)
|
||||
for _, failure := range failures {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d]: %v", i, failure)))
|
||||
}
|
||||
}
|
||||
|
||||
return allErrors
|
||||
}
|
||||
|
||||
var supportedFailurePolicies = sets.NewString(
|
||||
string(admissionregistration.Ignore),
|
||||
string(admissionregistration.Fail),
|
||||
)
|
||||
|
||||
var supportedOperations = sets.NewString(
|
||||
string(admissionregistration.OperationAll),
|
||||
string(admissionregistration.Create),
|
||||
string(admissionregistration.Update),
|
||||
string(admissionregistration.Delete),
|
||||
string(admissionregistration.Connect),
|
||||
)
|
||||
|
||||
func hasWildcardOperation(operations []admissionregistration.OperationType) bool {
|
||||
for _, o := range operations {
|
||||
if o == admissionregistration.OperationAll {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validateRuleWithOperations(ruleWithOperations *admissionregistration.RuleWithOperations, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(ruleWithOperations.Operations) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("operations"), ""))
|
||||
}
|
||||
if len(ruleWithOperations.Operations) > 1 && hasWildcardOperation(ruleWithOperations.Operations) {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("operations"), ruleWithOperations.Operations, "if '*' is present, must not specify other operations"))
|
||||
}
|
||||
for i, operation := range ruleWithOperations.Operations {
|
||||
if !supportedOperations.Has(string(operation)) {
|
||||
allErrors = append(allErrors, field.NotSupported(fldPath.Child("operations").Index(i), operation, supportedOperations.List()))
|
||||
}
|
||||
}
|
||||
allowSubResource := true
|
||||
allErrors = append(allErrors, validateRule(&ruleWithOperations.Rule, fldPath, allowSubResource)...)
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList {
|
||||
return ValidateValidatingWebhookConfiguration(newC)
|
||||
}
|
||||
|
||||
func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.MutatingWebhookConfiguration) field.ErrorList {
|
||||
return ValidateMutatingWebhookConfiguration(newC)
|
||||
}
|
756
vendor/k8s.io/kubernetes/pkg/apis/admissionregistration/validation/validation_test.go
generated
vendored
Normal file
756
vendor/k8s.io/kubernetes/pkg/apis/admissionregistration/validation/validation_test.go
generated
vendored
Normal file
@ -0,0 +1,756 @@
|
||||
/*
|
||||
Copyright 2017 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"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
func getInitializerConfiguration(initializers []admissionregistration.Initializer) *admissionregistration.InitializerConfiguration {
|
||||
return &admissionregistration.InitializerConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "config",
|
||||
},
|
||||
Initializers: initializers,
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateInitializerConfiguration(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *admissionregistration.InitializerConfiguration
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "0 rule is valid",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "all initializers must have a fully qualified name",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
},
|
||||
{
|
||||
Name: "k8s.io",
|
||||
},
|
||||
{
|
||||
Name: "",
|
||||
},
|
||||
}),
|
||||
expectedError: `initializers[1].name: Invalid value: "k8s.io": should be a domain with at least three segments separated by dots, initializers[2].name: Required value`,
|
||||
},
|
||||
{
|
||||
name: "APIGroups must not be empty or nil",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
Rules: []admissionregistration.Rule{
|
||||
{
|
||||
APIGroups: []string{},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
{
|
||||
APIGroups: nil,
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `initializers[0].rules[0].apiGroups: Required value, initializers[0].rules[1].apiGroups: Required value`,
|
||||
},
|
||||
{
|
||||
name: "APIVersions must not be empty or nil",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
Rules: []admissionregistration.Rule{
|
||||
{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: nil,
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `initializers[0].rules[0].apiVersions: Required value, initializers[0].rules[1].apiVersions: Required value`,
|
||||
},
|
||||
{
|
||||
name: "Resources must not be empty or nil",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
Rules: []admissionregistration.Rule{
|
||||
{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `initializers[0].rules[0].resources: Required value, initializers[0].rules[1].resources: Required value`,
|
||||
},
|
||||
{
|
||||
name: "\"\" is a valid APIGroup",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
Rules: []admissionregistration.Rule{
|
||||
{
|
||||
APIGroups: []string{"a", ""},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "\"\" is NOT a valid APIVersion",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
Rules: []admissionregistration.Rule{
|
||||
{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a", ""},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: "apiVersions[1]: Required value",
|
||||
},
|
||||
{
|
||||
name: "\"\" is NOT a valid Resource",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
Rules: []admissionregistration.Rule{
|
||||
{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a", ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: "resources[1]: Required value",
|
||||
},
|
||||
{
|
||||
name: "wildcard cannot be mixed with other strings for APIGroups or APIVersions or Resources",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
Rules: []admissionregistration.Rule{
|
||||
{
|
||||
APIGroups: []string{"a", "*"},
|
||||
APIVersions: []string{"a", "*"},
|
||||
Resources: []string{"a", "*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `[initializers[0].rules[0].apiGroups: Invalid value: []string{"a", "*"}: if '*' is present, must not specify other API groups, initializers[0].rules[0].apiVersions: Invalid value: []string{"a", "*"}: if '*' is present, must not specify other API versions, initializers[0].rules[0].resources: Invalid value: []string{"a", "*"}: if '*' is present, must not specify other resources]`,
|
||||
},
|
||||
{
|
||||
name: "Subresource not allowed",
|
||||
config: getInitializerConfiguration(
|
||||
[]admissionregistration.Initializer{
|
||||
{
|
||||
Name: "initializer.k8s.io",
|
||||
Rules: []admissionregistration.Rule{
|
||||
{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a/b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: ` "a/b": must not specify subresources`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
errs := ValidateInitializerConfiguration(test.config)
|
||||
err := errs.ToAggregate()
|
||||
if err != nil {
|
||||
if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" {
|
||||
t.Errorf("test case %s, expected to contain %s, got %s", test.name, e, a)
|
||||
}
|
||||
} else {
|
||||
if test.expectedError != "" {
|
||||
t.Errorf("test case %s, unexpected no error, expected to contain %s", test.name, test.expectedError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
|
||||
func newValidatingWebhookConfiguration(hooks []admissionregistration.Webhook) *admissionregistration.ValidatingWebhookConfiguration {
|
||||
return &admissionregistration.ValidatingWebhookConfiguration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "config",
|
||||
},
|
||||
Webhooks: hooks,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add TestValidateMutatingWebhookConfiguration to test validation for mutating webhooks.
|
||||
|
||||
func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||
validClientConfig := admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr("https://example.com"),
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
config *admissionregistration.ValidatingWebhookConfiguration
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "all Webhooks must have a fully qualified name",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
},
|
||||
{
|
||||
Name: "k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
},
|
||||
{
|
||||
Name: "",
|
||||
ClientConfig: validClientConfig,
|
||||
},
|
||||
}),
|
||||
expectedError: `webhooks[1].name: Invalid value: "k8s.io": should be a domain with at least three segments separated by dots, webhooks[2].name: Required value`,
|
||||
},
|
||||
{
|
||||
name: "Operations must not be empty or nil",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Operations: nil,
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `webhooks[0].rules[0].operations: Required value, webhooks[0].rules[1].operations: Required value`,
|
||||
},
|
||||
{
|
||||
name: "\"\" is NOT a valid operation",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"CREATE", ""},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `Unsupported value: ""`,
|
||||
},
|
||||
{
|
||||
name: "operation must be either create/update/delete/connect",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"PATCH"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `Unsupported value: "PATCH"`,
|
||||
},
|
||||
{
|
||||
name: "wildcard operation cannot be mixed with other strings",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"CREATE", "*"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `if '*' is present, must not specify other operations`,
|
||||
},
|
||||
{
|
||||
name: `resource "*" can co-exist with resources that have subresources`,
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"*", "a/b", "a/*", "*/b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: `resource "*" cannot mix with resources that don't have subresources`,
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"*", "a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `if '*' is present, must not specify other resources without subresources`,
|
||||
},
|
||||
{
|
||||
name: "resource a/* cannot mix with a/x",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a/*", "a/x"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `webhooks[0].rules[0].resources[1]: Invalid value: "a/x": if 'a/*' is present, must not specify a/x`,
|
||||
},
|
||||
{
|
||||
name: "resource a/* can mix with a",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a/*", "a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "resource */a cannot mix with x/a",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"*/a", "x/a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `webhooks[0].rules[0].resources[1]: Invalid value: "x/a": if '*/a' is present, must not specify x/a`,
|
||||
},
|
||||
{
|
||||
name: "resource */* cannot mix with other resources",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
Rules: []admissionregistration.RuleWithOperations{
|
||||
{
|
||||
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"*/*", "a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `webhooks[0].rules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`,
|
||||
},
|
||||
{
|
||||
name: "FailurePolicy can only be \"Ignore\" or \"Fail\"",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
FailurePolicy: func() *admissionregistration.FailurePolicyType {
|
||||
r := admissionregistration.FailurePolicyType("other")
|
||||
return &r
|
||||
}(),
|
||||
},
|
||||
}),
|
||||
expectedError: `webhooks[0].failurePolicy: Unsupported value: "other": supported values: "Fail", "Ignore"`,
|
||||
},
|
||||
{
|
||||
name: "both service and URL missing",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{},
|
||||
},
|
||||
}),
|
||||
expectedError: `exactly one of`,
|
||||
},
|
||||
{
|
||||
name: "both service and URL provided",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
Service: &admissionregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
},
|
||||
URL: strPtr("example.com/k8s/webhook"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `[0].clientConfig.url: Required value: exactly one of url or service is required`,
|
||||
},
|
||||
{
|
||||
name: "blank URL",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr(""),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `[0].clientConfig.url: Invalid value: "": host must be provided`,
|
||||
},
|
||||
{
|
||||
name: "wrong scheme",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr("http://example.com"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `https`,
|
||||
},
|
||||
{
|
||||
name: "missing host",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr("https:///fancy/webhook"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `host must be provided`,
|
||||
},
|
||||
{
|
||||
name: "fragment",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr("https://example.com/#bookmark"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `"bookmark": fragments are not permitted`,
|
||||
},
|
||||
{
|
||||
name: "query",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr("https://example.com?arg=value"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `"arg=value": query parameters are not permitted`,
|
||||
},
|
||||
{
|
||||
name: "user",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr("https://harry.potter@example.com/"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `"harry.potter": user information is not permitted`,
|
||||
},
|
||||
{
|
||||
name: "just totally wrong",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr("arg#backwards=thisis?html.index/port:host//:https"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `host must be provided`,
|
||||
},
|
||||
{
|
||||
name: "path must start with slash",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
Service: &admissionregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("foo/"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `clientConfig.service.path: Invalid value: "foo/": must start with a '/'`,
|
||||
},
|
||||
{
|
||||
name: "path accepts slash",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
Service: &admissionregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: ``,
|
||||
},
|
||||
{
|
||||
name: "path accepts no trailing slash",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
Service: &admissionregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: ``,
|
||||
},
|
||||
{
|
||||
name: "path fails //",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
Service: &admissionregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("//"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `clientConfig.service.path: Invalid value: "//": segment[0] may not be empty`,
|
||||
},
|
||||
{
|
||||
name: "path no empty step",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
Service: &admissionregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/foo//bar/"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `clientConfig.service.path: Invalid value: "/foo//bar/": segment[1] may not be empty`,
|
||||
}, {
|
||||
name: "path no empty step 2",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
Service: &admissionregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/foo/bar//"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `clientConfig.service.path: Invalid value: "/foo/bar//": segment[2] may not be empty`,
|
||||
},
|
||||
{
|
||||
name: "path no non-subdomain",
|
||||
config: newValidatingWebhookConfiguration(
|
||||
[]admissionregistration.Webhook{
|
||||
{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: admissionregistration.WebhookClientConfig{
|
||||
Service: &admissionregistration.ServiceReference{
|
||||
Namespace: "ns",
|
||||
Name: "n",
|
||||
Path: strPtr("/apis/foo.bar/v1alpha1/--bad"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expectedError: `clientConfig.service.path: Invalid value: "/apis/foo.bar/v1alpha1/--bad": segment[3]: a DNS-1123 subdomain`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
errs := ValidateValidatingWebhookConfiguration(test.config)
|
||||
err := errs.ToAggregate()
|
||||
if err != nil {
|
||||
if e, a := test.expectedError, err.Error(); !strings.Contains(a, e) || e == "" {
|
||||
t.Errorf("expected to contain %s, got %s", e, a)
|
||||
}
|
||||
} else {
|
||||
if test.expectedError != "" {
|
||||
t.Errorf("unexpected no error, expected to contain %s", test.expectedError)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user