add prune and remove unused packages

This commit is contained in:
Michelle Au
2019-03-08 14:54:43 -08:00
parent f59b58d164
commit 8c0accad66
17240 changed files with 27 additions and 4750030 deletions

View File

@@ -1,9 +0,0 @@
# Disable inheritance as this is an api owners file
options:
no_parent_owners: true
approvers:
- api-approvers
reviewers:
- api-reviewers
labels:
- kind/api-change

View File

@@ -1,147 +0,0 @@
/*
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 fuzzer
import (
"reflect"
"strings"
"github.com/google/gofuzz"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
)
var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()
// Funcs returns the fuzzer functions for the apiextensions apis.
func Funcs(codecs runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
func(obj *apiextensions.CustomResourceDefinitionSpec, c fuzz.Continue) {
c.FuzzNoCustom(obj)
// match our defaulter
if len(obj.Scope) == 0 {
obj.Scope = apiextensions.NamespaceScoped
}
if len(obj.Names.Singular) == 0 {
obj.Names.Singular = strings.ToLower(obj.Names.Kind)
}
if len(obj.Names.ListKind) == 0 && len(obj.Names.Kind) > 0 {
obj.Names.ListKind = obj.Names.Kind + "List"
}
if len(obj.Versions) == 0 && len(obj.Version) != 0 {
obj.Versions = []apiextensions.CustomResourceDefinitionVersion{
{
Name: obj.Version,
Served: true,
Storage: true,
},
}
} else if len(obj.Versions) != 0 {
obj.Version = obj.Versions[0].Name
}
if len(obj.AdditionalPrinterColumns) == 0 {
obj.AdditionalPrinterColumns = []apiextensions.CustomResourceColumnDefinition{
{Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"},
}
}
if obj.Conversion == nil {
obj.Conversion = &apiextensions.CustomResourceConversion{
Strategy: apiextensions.NoneConverter,
}
}
},
func(obj *apiextensions.CustomResourceDefinition, c fuzz.Continue) {
c.FuzzNoCustom(obj)
if len(obj.Status.StoredVersions) == 0 {
for _, v := range obj.Spec.Versions {
if v.Storage && !apiextensions.IsStoredVersion(obj, v.Name) {
obj.Status.StoredVersions = append(obj.Status.StoredVersions, v.Name)
}
}
}
},
func(obj *apiextensions.JSONSchemaProps, c fuzz.Continue) {
// we cannot use c.FuzzNoCustom because of the interface{} fields. So let's loop with reflection.
vobj := reflect.ValueOf(obj).Elem()
tobj := reflect.TypeOf(obj).Elem()
for i := 0; i < tobj.NumField(); i++ {
field := tobj.Field(i)
switch field.Name {
case "Default", "Enum", "Example", "Ref":
continue
default:
isValue := true
switch field.Type.Kind() {
case reflect.Interface, reflect.Map, reflect.Slice, reflect.Ptr:
isValue = false
}
if isValue || c.Intn(10) == 0 {
c.Fuzz(vobj.Field(i).Addr().Interface())
}
}
}
if c.RandBool() {
validJSON := apiextensions.JSON(`{"some": {"json": "test"}, "string": 42}`)
obj.Default = &validJSON
}
if c.RandBool() {
obj.Enum = []apiextensions.JSON{c.Float64(), c.RandString(), c.RandBool()}
}
if c.RandBool() {
validJSON := apiextensions.JSON(`"foobarbaz"`)
obj.Example = &validJSON
}
if c.RandBool() {
validRef := "validRef"
obj.Ref = &validRef
}
},
func(obj *apiextensions.JSONSchemaPropsOrBool, c fuzz.Continue) {
if c.RandBool() {
obj.Allows = true
obj.Schema = &apiextensions.JSONSchemaProps{}
c.Fuzz(obj.Schema)
} else {
obj.Allows = c.RandBool()
}
},
func(obj *apiextensions.JSONSchemaPropsOrArray, c fuzz.Continue) {
// disallow both Schema and JSONSchemas to be nil.
if c.RandBool() {
obj.Schema = &apiextensions.JSONSchemaProps{}
c.Fuzz(obj.Schema)
} else {
obj.JSONSchemas = make([]apiextensions.JSONSchemaProps, c.Intn(3)+1)
for i := range obj.JSONSchemas {
c.Fuzz(&obj.JSONSchemas[i])
}
}
},
func(obj *apiextensions.JSONSchemaPropsOrStringArray, c fuzz.Continue) {
if c.RandBool() {
obj.Schema = &apiextensions.JSONSchemaProps{}
c.Fuzz(obj.Schema)
} else {
c.Fuzz(&obj.Property)
}
},
}
}

View File

@@ -1,421 +0,0 @@
/*
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 apiextensions
import (
"reflect"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestCRDHasFinalizer(t *testing.T) {
tests := []struct {
name string
crd *CustomResourceDefinition
finalizerToCheck string
expected bool
}{
{
name: "missing",
crd: &CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"not-it"}},
},
finalizerToCheck: "it",
expected: false,
},
{
name: "present",
crd: &CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"not-it", "it"}},
},
finalizerToCheck: "it",
expected: true,
},
}
for _, tc := range tests {
actual := CRDHasFinalizer(tc.crd, tc.finalizerToCheck)
if tc.expected != actual {
t.Errorf("%v expected %v, got %v", tc.name, tc.expected, actual)
}
}
}
func TestCRDRemoveFinalizer(t *testing.T) {
tests := []struct {
name string
crd *CustomResourceDefinition
finalizerToCheck string
expected []string
}{
{
name: "missing",
crd: &CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"not-it"}},
},
finalizerToCheck: "it",
expected: []string{"not-it"},
},
{
name: "present",
crd: &CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"not-it", "it"}},
},
finalizerToCheck: "it",
expected: []string{"not-it"},
},
}
for _, tc := range tests {
CRDRemoveFinalizer(tc.crd, tc.finalizerToCheck)
if !reflect.DeepEqual(tc.expected, tc.crd.Finalizers) {
t.Errorf("%v expected %v, got %v", tc.name, tc.expected, tc.crd.Finalizers)
}
}
}
func TestSetCRDCondition(t *testing.T) {
tests := []struct {
name string
crdCondition []CustomResourceDefinitionCondition
newCondition CustomResourceDefinitionCondition
expectedcrdCondition []CustomResourceDefinitionCondition
}{
{
name: "test setCRDcondition when one condition",
crdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
newCondition: CustomResourceDefinitionCondition{
Type: Established,
Status: ConditionFalse,
Reason: "NotAccepted",
Message: "Not accepted",
LastTransitionTime: metav1.Date(2018, 1, 2, 0, 0, 0, 0, time.UTC),
},
expectedcrdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionFalse,
Reason: "NotAccepted",
Message: "Not accepted",
LastTransitionTime: metav1.Date(2018, 1, 2, 0, 0, 0, 0, time.UTC),
},
},
},
{
name: "test setCRDcondition when two condition",
crdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: NamesAccepted,
Status: ConditionTrue,
Reason: "NoConflicts",
Message: "no conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
newCondition: CustomResourceDefinitionCondition{
Type: NamesAccepted,
Status: ConditionFalse,
Reason: "Conflicts",
Message: "conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 2, 0, 0, 0, 0, time.UTC),
},
expectedcrdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: NamesAccepted,
Status: ConditionFalse,
Reason: "Conflicts",
Message: "conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 2, 0, 0, 0, 0, time.UTC),
},
},
},
{
name: "test setCRDcondition when condition needs to be appended",
crdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
newCondition: CustomResourceDefinitionCondition{
Type: Terminating,
Status: ConditionFalse,
Reason: "NeverEstablished",
Message: "resource was never established",
LastTransitionTime: metav1.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC),
},
expectedcrdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: Terminating,
Status: ConditionFalse,
Reason: "NeverEstablished",
Message: "resource was never established",
LastTransitionTime: metav1.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC),
},
},
},
}
for _, tc := range tests {
crd := generateCRDwithCondition(tc.crdCondition)
SetCRDCondition(crd, tc.newCondition)
if len(tc.expectedcrdCondition) != len(crd.Status.Conditions) {
t.Errorf("%v expected %v, got %v", tc.name, tc.expectedcrdCondition, crd.Status.Conditions)
}
for i := range tc.expectedcrdCondition {
if !IsCRDConditionEquivalent(&tc.expectedcrdCondition[i], &crd.Status.Conditions[i]) {
t.Errorf("%v expected %v, got %v", tc.name, tc.expectedcrdCondition, crd.Status.Conditions)
}
}
}
}
func TestRemoveCRDCondition(t *testing.T) {
tests := []struct {
name string
crdCondition []CustomResourceDefinitionCondition
conditionType CustomResourceDefinitionConditionType
expectedcrdCondition []CustomResourceDefinitionCondition
}{
{
name: "test remove CRDCondition when the conditionType meets",
crdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: NamesAccepted,
Status: ConditionTrue,
Reason: "NoConflicts",
Message: "no conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
conditionType: NamesAccepted,
expectedcrdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2011, 1, 2, 0, 0, 0, 0, time.UTC),
},
},
},
{
name: "test remove CRDCondition when the conditionType not meets",
crdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: NamesAccepted,
Status: ConditionTrue,
Reason: "NoConflicts",
Message: "no conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
conditionType: Terminating,
expectedcrdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: NamesAccepted,
Status: ConditionTrue,
Reason: "NoConflicts",
Message: "no conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
},
}
for _, tc := range tests {
crd := generateCRDwithCondition(tc.crdCondition)
RemoveCRDCondition(crd, tc.conditionType)
if len(tc.expectedcrdCondition) != len(crd.Status.Conditions) {
t.Errorf("%v expected %v, got %v", tc.name, tc.expectedcrdCondition, crd.Status.Conditions)
}
for i := range tc.expectedcrdCondition {
if !IsCRDConditionEquivalent(&tc.expectedcrdCondition[i], &crd.Status.Conditions[i]) {
t.Errorf("%v expected %v, got %v", tc.name, tc.expectedcrdCondition, crd.Status.Conditions)
}
}
}
}
func TestIsCRDConditionPresentAndEqual(t *testing.T) {
tests := []struct {
name string
crdCondition []CustomResourceDefinitionCondition
conditionType CustomResourceDefinitionConditionType
status ConditionStatus
expectresult bool
}{
{
name: "test CRDCondition is not Present",
crdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: NamesAccepted,
Status: ConditionTrue,
Reason: "NoConflicts",
Message: "no conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
conditionType: Terminating,
status: ConditionTrue,
expectresult: false,
},
{
name: "test CRDCondition is Present but not Equal",
crdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: NamesAccepted,
Status: ConditionTrue,
Reason: "NoConflicts",
Message: "no conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
conditionType: Established,
status: ConditionFalse,
expectresult: false,
},
{
name: "test CRDCondition is Present and Equal",
crdCondition: []CustomResourceDefinitionCondition{
{
Type: Established,
Status: ConditionTrue,
Reason: "Accepted",
Message: "the initial names have been accepted",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: NamesAccepted,
Status: ConditionTrue,
Reason: "NoConflicts",
Message: "no conflicts found",
LastTransitionTime: metav1.Date(2018, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
conditionType: NamesAccepted,
status: ConditionTrue,
expectresult: true,
},
}
for _, tc := range tests {
crd := generateCRDwithCondition(tc.crdCondition)
res := IsCRDConditionPresentAndEqual(crd, tc.conditionType, tc.status)
if res != tc.expectresult {
t.Errorf("%v expected %t, got %t", tc.name, tc.expectresult, res)
}
}
}
func generateCRDwithCondition(conditions []CustomResourceDefinitionCondition) *CustomResourceDefinition {
testCRDObjectMeta := metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "12",
}
testCRDSpec := CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Scope: ResourceScope("Cluster"),
Names: CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
},
}
testCRDAcceptedNames := CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "kind",
ListKind: "listkind",
}
return &CustomResourceDefinition{
ObjectMeta: testCRDObjectMeta,
Spec: testCRDSpec,
Status: CustomResourceDefinitionStatus{
AcceptedNames: testCRDAcceptedNames,
Conditions: conditions,
},
}
}

View File

@@ -1,31 +0,0 @@
/*
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 install
import (
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
utilruntime.Must(apiextensions.AddToScheme(scheme))
utilruntime.Must(v1beta1.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(v1beta1.SchemeGroupVersion))
}

View File

@@ -1,28 +0,0 @@
/*
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 install
import (
"testing"
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
"k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
)
func TestRoundTrip(t *testing.T) {
roundtrip.RoundTripTestForAPIGroup(t, Install, apiextensionsfuzzer.Funcs)
}

View File

@@ -1,113 +0,0 @@
/*
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 v1beta1
import (
"reflect"
"testing"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)
func TestJSONConversion(t *testing.T) {
nilJSON := apiextensions.JSON(nil)
nullJSON := apiextensions.JSON("null")
stringJSON := apiextensions.JSON("foo")
boolJSON := apiextensions.JSON(true)
sliceJSON := apiextensions.JSON([]string{"foo", "bar", "baz"})
testCases := map[string]struct {
input *apiextensions.JSONSchemaProps
expected *JSONSchemaProps
}{
"nil": {
input: &apiextensions.JSONSchemaProps{
Default: nil,
},
expected: &JSONSchemaProps{},
},
"aliased nil": {
input: &apiextensions.JSONSchemaProps{
Default: &nilJSON,
},
expected: &JSONSchemaProps{},
},
"null": {
input: &apiextensions.JSONSchemaProps{
Default: &nullJSON,
},
expected: &JSONSchemaProps{
Default: &JSON{
Raw: []byte(`"null"`),
},
},
},
"string": {
input: &apiextensions.JSONSchemaProps{
Default: &stringJSON,
},
expected: &JSONSchemaProps{
Default: &JSON{
Raw: []byte(`"foo"`),
},
},
},
"bool": {
input: &apiextensions.JSONSchemaProps{
Default: &boolJSON,
},
expected: &JSONSchemaProps{
Default: &JSON{
Raw: []byte(`true`),
},
},
},
"slice": {
input: &apiextensions.JSONSchemaProps{
Default: &sliceJSON,
},
expected: &JSONSchemaProps{
Default: &JSON{
Raw: []byte(`["foo","bar","baz"]`),
},
},
},
}
scheme := runtime.NewScheme()
// add internal and external types
if err := apiextensions.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := AddToScheme(scheme); err != nil {
t.Fatal(err)
}
for k, tc := range testCases {
external := &JSONSchemaProps{}
if err := scheme.Convert(tc.input, external, nil); err != nil {
t.Errorf("%s: unexpected error: %v", k, err)
}
if !reflect.DeepEqual(external, tc.expected) {
t.Errorf("%s: expected\n\t%#v, got \n\t%#v", k, tc.expected, external)
}
}
}

View File

@@ -1,522 +0,0 @@
/*
Copyright 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.
*/
// This file was autogenerated by go-to-protobuf. Do not edit it manually!
syntax = 'proto2';
package k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1;
import "k8s.io/apimachinery/pkg/apis/meta/v1/generated.proto";
import "k8s.io/apimachinery/pkg/runtime/generated.proto";
import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto";
// Package-wide variables from generator "generated".
option go_package = "v1beta1";
// ConversionRequest describes the conversion request parameters.
message ConversionRequest {
// `uid` is an identifier for the individual request/response. It allows us to distinguish instances of requests which are
// otherwise identical (parallel requests, requests when earlier requests did not modify etc)
// The UID is meant to track the round trip (request/response) between the KAS and the WebHook, not the user request.
// It is suitable for correlating log entries between the webhook and apiserver, for either auditing or debugging.
optional string uid = 1;
// `desiredAPIVersion` is the version to convert given objects to. e.g. "myapi.example.com/v1"
optional string desiredAPIVersion = 2;
// `objects` is the list of CR objects to be converted.
repeated k8s.io.apimachinery.pkg.runtime.RawExtension objects = 3;
}
// ConversionResponse describes a conversion response.
message ConversionResponse {
// `uid` is an identifier for the individual request/response.
// This should be copied over from the corresponding AdmissionRequest.
optional string uid = 1;
// `convertedObjects` is the list of converted version of `request.objects` if the `result` is successful otherwise empty.
// The webhook is expected to set apiVersion of these objects to the ConversionRequest.desiredAPIVersion. The list
// must also has the same size as input list with the same objects in the same order(i.e. equal UIDs and object meta)
repeated k8s.io.apimachinery.pkg.runtime.RawExtension convertedObjects = 2;
// `result` contains the result of conversion with extra details if the conversion failed. `result.status` determines if
// the conversion failed or succeeded. The `result.status` field is required and represent the success or failure of the
// conversion. A successful conversion must set `result.status` to `Success`. A failed conversion must set
// `result.status` to `Failure` and provide more details in `result.message` and return http status 200. The `result.message`
// will be used to construct an error message for the end user.
optional k8s.io.apimachinery.pkg.apis.meta.v1.Status result = 3;
}
// ConversionReview describes a conversion request/response.
message ConversionReview {
// `request` describes the attributes for the conversion request.
// +optional
optional ConversionRequest request = 1;
// `response` describes the attributes for the conversion response.
// +optional
optional ConversionResponse response = 2;
}
// CustomResourceColumnDefinition specifies a column for server side printing.
message CustomResourceColumnDefinition {
// name is a human readable name for the column.
optional string name = 1;
// type is an OpenAPI type definition for this column.
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.
optional string type = 2;
// format is an optional OpenAPI type definition for this column. The 'name' format is applied
// to the primary identifier column to assist in clients identifying column is the resource name.
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.
// +optional
optional string format = 3;
// description is a human readable description of this column.
// +optional
optional string description = 4;
// priority is an integer defining the relative importance of this column compared to others. Lower
// numbers are considered higher priority. Columns that may be omitted in limited space scenarios
// should be given a higher priority.
// +optional
optional int32 priority = 5;
// JSONPath is a simple JSON path, i.e. with array notation.
optional string JSONPath = 6;
}
// CustomResourceConversion describes how to convert different versions of a CR.
message CustomResourceConversion {
// `strategy` specifies the conversion strategy. Allowed values are:
// - `None`: The converter only change the apiVersion and would not touch any other field in the CR.
// - `Webhook`: API Server will call to an external webhook to do the conversion. Additional information is needed for this option.
optional string strategy = 1;
// `webhookClientConfig` is the instructions for how to call the webhook if strategy is `Webhook`. This field is
// alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
optional WebhookClientConfig webhookClientConfig = 2;
}
// CustomResourceDefinition represents a resource that should be exposed on the API server. Its name MUST be in the format
// <.spec.name>.<.spec.group>.
message CustomResourceDefinition {
optional k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
// Spec describes how the user wants the resources to appear
optional CustomResourceDefinitionSpec spec = 2;
// Status indicates the actual state of the CustomResourceDefinition
// +optional
optional CustomResourceDefinitionStatus status = 3;
}
// CustomResourceDefinitionCondition contains details for the current condition of this pod.
message CustomResourceDefinitionCondition {
// Type is the type of the condition.
optional string type = 1;
// Status is the status of the condition.
// Can be True, False, Unknown.
optional string status = 2;
// Last time the condition transitioned from one status to another.
// +optional
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time lastTransitionTime = 3;
// Unique, one-word, CamelCase reason for the condition's last transition.
// +optional
optional string reason = 4;
// Human-readable message indicating details about last transition.
// +optional
optional string message = 5;
}
// CustomResourceDefinitionList is a list of CustomResourceDefinition objects.
message CustomResourceDefinitionList {
optional k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
// Items individual CustomResourceDefinitions
repeated CustomResourceDefinition items = 2;
}
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
message CustomResourceDefinitionNames {
// Plural is the plural name of the resource to serve. It must match the name of the CustomResourceDefinition-registration
// too: plural.group and it must be all lowercase.
optional string plural = 1;
// Singular is the singular name of the resource. It must be all lowercase Defaults to lowercased <kind>
// +optional
optional string singular = 2;
// ShortNames are short names for the resource. It must be all lowercase.
// +optional
repeated string shortNames = 3;
// Kind is the serialized kind of the resource. It is normally CamelCase and singular.
optional string kind = 4;
// ListKind is the serialized kind of the list for this resource. Defaults to <kind>List.
// +optional
optional string listKind = 5;
// Categories is a list of grouped resources custom resources belong to (e.g. 'all')
// +optional
repeated string categories = 6;
}
// CustomResourceDefinitionSpec describes how a user wants their resource to appear
message CustomResourceDefinitionSpec {
// Group is the group this resource belongs in
optional string group = 1;
// Version is the version this resource belongs in
// Should be always first item in Versions field if provided.
// Optional, but at least one of Version or Versions must be set.
// Deprecated: Please use `Versions`.
// +optional
optional string version = 2;
// Names are the names used to describe this custom resource
optional CustomResourceDefinitionNames names = 3;
// Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced
optional string scope = 4;
// Validation describes the validation methods for CustomResources
// Optional, the global validation schema for all versions.
// Top-level and per-version schemas are mutually exclusive.
// +optional
optional CustomResourceValidation validation = 5;
// Subresources describes the subresources for CustomResource
// Optional, the global subresources for all versions.
// Top-level and per-version subresources are mutually exclusive.
// +optional
optional CustomResourceSubresources subresources = 6;
// Versions is the list of all supported versions for this resource.
// If Version field is provided, this field is optional.
// Validation: All versions must use the same validation schema for now. i.e., top
// level Validation field is applied to all of these versions.
// Order: The version name will be used to compute the order.
// If the version string is "kube-like", it will sort above non "kube-like" version strings, which are ordered
// lexicographically. "Kube-like" versions start with a "v", then are followed by a number (the major version),
// then optionally the string "alpha" or "beta" and another number (the minor version). These are sorted first
// by GA > beta > alpha (where GA is a version with no suffix such as beta or alpha), and then by comparing
// major version, then minor version. An example sorted list of versions:
// v10, v2, v1, v11beta2, v10beta3, v3beta1, v12alpha1, v11alpha2, foo1, foo10.
// +optional
repeated CustomResourceDefinitionVersion versions = 7;
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
// Optional, the global columns for all versions.
// Top-level and per-version columns are mutually exclusive.
// +optional
repeated CustomResourceColumnDefinition additionalPrinterColumns = 8;
// `conversion` defines conversion settings for the CRD.
// +optional
optional CustomResourceConversion conversion = 9;
}
// CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition
message CustomResourceDefinitionStatus {
// Conditions indicate state for particular aspects of a CustomResourceDefinition
repeated CustomResourceDefinitionCondition conditions = 1;
// AcceptedNames are the names that are actually being used to serve discovery
// They may be different than the names in spec.
optional CustomResourceDefinitionNames acceptedNames = 2;
// StoredVersions are all versions of CustomResources that were ever persisted. Tracking these
// versions allows a migration path for stored versions in etcd. The field is mutable
// so the migration controller can first finish a migration to another version (i.e.
// that no old objects are left in the storage), and then remove the rest of the
// versions from this list.
// None of the versions in this list can be removed from the spec.Versions field.
repeated string storedVersions = 3;
}
// CustomResourceDefinitionVersion describes a version for CRD.
message CustomResourceDefinitionVersion {
// Name is the version name, e.g. “v1”, “v2beta1”, etc.
optional string name = 1;
// Served is a flag enabling/disabling this version from being served via REST APIs
optional bool served = 2;
// Storage flags the version as storage version. There must be exactly one
// flagged as storage version.
optional bool storage = 3;
// Schema describes the schema for CustomResource used in validation, pruning, and defaulting.
// Top-level and per-version schemas are mutually exclusive.
// Per-version schemas must not all be set to identical values (top-level validation schema should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
optional CustomResourceValidation schema = 4;
// Subresources describes the subresources for CustomResource
// Top-level and per-version subresources are mutually exclusive.
// Per-version subresources must not all be set to identical values (top-level subresources should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// +optional
optional CustomResourceSubresources subresources = 5;
// AdditionalPrinterColumns are additional columns shown e.g. in kubectl next to the name. Defaults to a created-at column.
// Top-level and per-version columns are mutually exclusive.
// Per-version columns must not all be set to identical values (top-level columns should be used instead)
// This field is alpha-level and is only honored by servers that enable the CustomResourceWebhookConversion feature.
// NOTE: CRDs created prior to 1.13 populated the top-level additionalPrinterColumns field by default. To apply an
// update that changes to per-version additionalPrinterColumns, the top-level additionalPrinterColumns field must
// be explicitly set to null
// +optional
repeated CustomResourceColumnDefinition additionalPrinterColumns = 6;
}
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
message CustomResourceSubresourceScale {
// SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .spec.
// If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.
optional string specReplicasPath = 1;
// StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource
// will default to 0.
optional string statusReplicasPath = 2;
// LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// Must be set to work with HPA.
// If there is no value under the given path in the CustomResource, the status label selector value in the /scale
// subresource will default to the empty string.
// +optional
optional string labelSelectorPath = 3;
}
// CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources.
// Status is represented by the `.status` JSON path inside of a CustomResource. When set,
// * exposes a /status subresource for the custom resource
// * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza
// * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza
message CustomResourceSubresourceStatus {
}
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
message CustomResourceSubresources {
// Status denotes the status subresource for CustomResources
// +optional
optional CustomResourceSubresourceStatus status = 1;
// Scale denotes the scale subresource for CustomResources
// +optional
optional CustomResourceSubresourceScale scale = 2;
}
// CustomResourceValidation is a list of validation methods for CustomResources.
message CustomResourceValidation {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
// +optional
optional JSONSchemaProps openAPIV3Schema = 1;
}
// ExternalDocumentation allows referencing an external resource for extended documentation.
message ExternalDocumentation {
optional string description = 1;
optional string url = 2;
}
// JSON represents any valid JSON value.
// These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil.
message JSON {
optional bytes raw = 1;
}
// JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).
message JSONSchemaProps {
optional string id = 1;
optional string schema = 2;
optional string ref = 3;
optional string description = 4;
optional string type = 5;
optional string format = 6;
optional string title = 7;
optional JSON default = 8;
optional double maximum = 9;
optional bool exclusiveMaximum = 10;
optional double minimum = 11;
optional bool exclusiveMinimum = 12;
optional int64 maxLength = 13;
optional int64 minLength = 14;
optional string pattern = 15;
optional int64 maxItems = 16;
optional int64 minItems = 17;
optional bool uniqueItems = 18;
optional double multipleOf = 19;
repeated JSON enum = 20;
optional int64 maxProperties = 21;
optional int64 minProperties = 22;
repeated string required = 23;
optional JSONSchemaPropsOrArray items = 24;
repeated JSONSchemaProps allOf = 25;
repeated JSONSchemaProps oneOf = 26;
repeated JSONSchemaProps anyOf = 27;
optional JSONSchemaProps not = 28;
map<string, JSONSchemaProps> properties = 29;
optional JSONSchemaPropsOrBool additionalProperties = 30;
map<string, JSONSchemaProps> patternProperties = 31;
map<string, JSONSchemaPropsOrStringArray> dependencies = 32;
optional JSONSchemaPropsOrBool additionalItems = 33;
map<string, JSONSchemaProps> definitions = 34;
optional ExternalDocumentation externalDocs = 35;
optional JSON example = 36;
}
// JSONSchemaPropsOrArray represents a value that can either be a JSONSchemaProps
// or an array of JSONSchemaProps. Mainly here for serialization purposes.
message JSONSchemaPropsOrArray {
optional JSONSchemaProps schema = 1;
repeated JSONSchemaProps jSONSchemas = 2;
}
// JSONSchemaPropsOrBool represents JSONSchemaProps or a boolean value.
// Defaults to true for the boolean property.
message JSONSchemaPropsOrBool {
optional bool allows = 1;
optional JSONSchemaProps schema = 2;
}
// JSONSchemaPropsOrStringArray represents a JSONSchemaProps or a string array.
message JSONSchemaPropsOrStringArray {
optional JSONSchemaProps schema = 1;
repeated string property = 2;
}
// ServiceReference holds a reference to Service.legacy.k8s.io
message ServiceReference {
// `namespace` is the namespace of the service.
// Required
optional string namespace = 1;
// `name` is the name of the service.
// Required
optional string name = 2;
// `path` is an optional URL path which will be sent in any request to
// this service.
// +optional
optional string path = 3;
}
// WebhookClientConfig contains the information to make a TLS
// connection with the webhook. It has the same field as admissionregistration.v1beta1.WebhookClientConfig.
message WebhookClientConfig {
// `url` gives the location of the webhook, in standard URL form
// (`scheme://host:port/path`). Exactly one of `url` or `service`
// must be specified.
//
// The `host` should not refer to a service running in the cluster; use
// the `service` field instead. The host might be resolved via external
// DNS in some apiservers (e.g., `kube-apiserver` cannot resolve
// in-cluster DNS as that would be a layering violation). `host` may
// also be an IP address.
//
// Please note that using `localhost` or `127.0.0.1` as a `host` is
// risky unless you take great care to run this webhook on all hosts
// which run an apiserver which might need to make calls to this
// webhook. Such installs are likely to be non-portable, i.e., not easy
// to turn up in a new cluster.
//
// The scheme must be "https"; the URL must begin with "https://".
//
// A path is optional, and if present may be any string permissible in
// a URL. You may use the path to pass an arbitrary string to the
// webhook, for example, a cluster identifier.
//
// Attempting to use a user or basic auth e.g. "user:password@" is not
// allowed. Fragments ("#...") and query parameters ("?...") are not
// allowed, either.
//
// +optional
optional string url = 3;
// `service` is a reference to the service for this webhook. Either
// `service` or `url` must be specified.
//
// If the webhook is running within the cluster, then you should use `service`.
//
// Port 443 will be used if it is open, otherwise it is an error.
//
// +optional
optional ServiceReference service = 1;
// `caBundle` is a PEM encoded CA bundle which will be used to validate the webhook's server certificate.
// If unspecified, system trust roots on the apiserver are used.
// +optional
optional bytes caBundle = 2;
}

View File

@@ -1,150 +0,0 @@
/*
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 v1beta1
import (
"encoding/json"
"reflect"
"testing"
)
type JSONSchemaPropsOrBoolHolder struct {
JSPoB JSONSchemaPropsOrBool `json:"val1"`
JSPoBOmitEmpty *JSONSchemaPropsOrBool `json:"val2,omitempty"`
}
func TestJSONSchemaPropsOrBoolUnmarshalJSON(t *testing.T) {
cases := []struct {
input string
result JSONSchemaPropsOrBoolHolder
}{
{`{}`, JSONSchemaPropsOrBoolHolder{}},
{`{"val1": {}}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{}}}},
{`{"val1": {"type":"string"}}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val1": false}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{}}},
{`{"val1": true}`, JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true}}},
{`{"val2": {}}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{}}}},
{`{"val2": {"type":"string"}}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true, Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val2": false}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{}}},
{`{"val2": true}`, JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true}}},
}
for _, c := range cases {
var result JSONSchemaPropsOrBoolHolder
if err := json.Unmarshal([]byte(c.input), &result); err != nil {
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
}
if !reflect.DeepEqual(result, c.result) {
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result)
}
}
}
func TestStringArrayOrStringMarshalJSON(t *testing.T) {
cases := []struct {
input JSONSchemaPropsOrBoolHolder
result string
}{
{JSONSchemaPropsOrBoolHolder{}, `{"val1":false}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{}}}, `{"val1":{}}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":{"type":"string"}}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{}}, `{"val1":false}`},
{JSONSchemaPropsOrBoolHolder{JSPoB: JSONSchemaPropsOrBool{Allows: true}}, `{"val1":true}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{}}}, `{"val1":false,"val2":{}}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":false,"val2":{"type":"string"}}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{}}, `{"val1":false,"val2":false}`},
{JSONSchemaPropsOrBoolHolder{JSPoBOmitEmpty: &JSONSchemaPropsOrBool{Allows: true}}, `{"val1":false,"val2":true}`},
}
for _, c := range cases {
result, err := json.Marshal(&c.input)
if err != nil {
t.Errorf("Unexpected error marshaling input '%v': %v", c.input, err)
}
if string(result) != c.result {
t.Errorf("Failed to marshal input '%v': expected: %q, got %q", c.input, c.result, string(result))
}
}
}
type JSONSchemaPropsOrArrayHolder struct {
JSPoA JSONSchemaPropsOrArray `json:"val1"`
JSPoAOmitEmpty *JSONSchemaPropsOrArray `json:"val2,omitempty"`
}
func TestJSONSchemaPropsOrArrayUnmarshalJSON(t *testing.T) {
cases := []struct {
input string
result JSONSchemaPropsOrArrayHolder
}{
{`{}`, JSONSchemaPropsOrArrayHolder{}},
{`{"val1": {}}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}},
{`{"val1": {"type":"string"}}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val1": [{}]}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}},
{`{"val1": [{},{"type":"string"}]}`, JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}},
{`{"val2": {}}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}},
{`{"val2": {"type":"string"}}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}},
{`{"val2": [{}]}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}},
{`{"val2": [{},{"type":"string"}]}`, JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}},
}
for _, c := range cases {
var result JSONSchemaPropsOrArrayHolder
if err := json.Unmarshal([]byte(c.input), &result); err != nil {
t.Errorf("Failed to unmarshal input '%v': %v", c.input, err)
}
if !reflect.DeepEqual(result, c.result) {
t.Errorf("Failed to unmarshal input '%v': expected %+v, got %+v", c.input, c.result, result)
}
}
}
func TestJSONSchemaPropsOrArrayMarshalJSON(t *testing.T) {
cases := []struct {
input JSONSchemaPropsOrArrayHolder
result string
}{
{JSONSchemaPropsOrArrayHolder{}, `{"val1":null}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}, `{"val1":{}}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":{"type":"string"}}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}, `{"val1":[{}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoA: JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}, `{"val1":[{},{"type":"string"}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{}}, `{"val1":null,"val2":null}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{}}}, `{"val1":null,"val2":{}}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{Schema: &JSONSchemaProps{Type: "string"}}}, `{"val1":null,"val2":{"type":"string"}}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}}}}, `{"val1":null,"val2":[{}]}`},
{JSONSchemaPropsOrArrayHolder{JSPoAOmitEmpty: &JSONSchemaPropsOrArray{JSONSchemas: []JSONSchemaProps{{}, {Type: "string"}}}}, `{"val1":null,"val2":[{},{"type":"string"}]}`},
}
for i, c := range cases {
result, err := json.Marshal(&c.input)
if err != nil {
t.Errorf("%d: Unexpected error marshaling input '%v': %v", i, c.input, err)
}
if string(result) != c.result {
t.Errorf("%d: Failed to marshal input '%v': expected: %q, got %q", i, c.input, c.result, string(result))
}
}
}

View File

@@ -1,734 +0,0 @@
/*
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"
"reflect"
"strings"
apiequality "k8s.io/apimachinery/pkg/api/equality"
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/sets"
validationutil "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
)
var (
printerColumnDatatypes = sets.NewString("integer", "number", "string", "boolean", "date")
customResourceColumnDefinitionFormats = sets.NewString("int32", "int64", "float", "double", "byte", "date", "date-time", "password")
)
// ValidateCustomResourceDefinition statically validates
func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinition) field.ErrorList {
nameValidationFn := func(name string, prefix bool) []string {
ret := genericvalidation.NameIsDNSSubdomain(name, prefix)
requiredName := obj.Spec.Names.Plural + "." + obj.Spec.Group
if name != requiredName {
ret = append(ret, fmt.Sprintf(`must be spec.names.plural+"."+spec.group`))
}
return ret
}
allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateCustomResourceDefinitionSpec(&obj.Spec, field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...)
return allErrs
}
// ValidateCustomResourceDefinitionUpdate statically validates
func ValidateCustomResourceDefinitionUpdate(obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList {
allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateCustomResourceDefinitionSpecUpdate(&obj.Spec, &oldObj.Spec, apiextensions.IsCRDConditionTrue(oldObj, apiextensions.Established), field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionStoredVersions(obj.Status.StoredVersions, obj.Spec.Versions, field.NewPath("status").Child("storedVersions"))...)
return allErrs
}
// ValidateCustomResourceDefinitionStoredVersions statically validates
func ValidateCustomResourceDefinitionStoredVersions(storedVersions []string, versions []apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path) field.ErrorList {
if len(storedVersions) == 0 {
return field.ErrorList{field.Invalid(fldPath, storedVersions, "must have at least one stored version")}
}
allErrs := field.ErrorList{}
storedVersionsMap := map[string]int{}
for i, v := range storedVersions {
storedVersionsMap[v] = i
}
for _, v := range versions {
_, ok := storedVersionsMap[v.Name]
if v.Storage && !ok {
allErrs = append(allErrs, field.Invalid(fldPath, v, "must have the storage version "+v.Name))
}
if ok {
delete(storedVersionsMap, v.Name)
}
}
for v, i := range storedVersionsMap {
allErrs = append(allErrs, field.Invalid(fldPath.Index(i), v, "must appear in spec.versions"))
}
return allErrs
}
// ValidateUpdateCustomResourceDefinitionStatus statically validates
func ValidateUpdateCustomResourceDefinitionStatus(obj, oldObj *apiextensions.CustomResourceDefinition) field.ErrorList {
allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
return allErrs
}
// ValidateCustomResourceDefinitionVersion statically validates.
func ValidateCustomResourceDefinitionVersion(version *apiextensions.CustomResourceDefinitionVersion, fldPath *field.Path, statusEnabled bool) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(version.Schema, statusEnabled, fldPath.Child("schema"))...)
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(version.Subresources, fldPath.Child("subresources"))...)
for i := range version.AdditionalPrinterColumns {
allErrs = append(allErrs, ValidateCustomResourceColumnDefinition(&version.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i))...)
}
return allErrs
}
// ValidateCustomResourceDefinitionSpec statically validates
func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(spec.Group) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("group"), ""))
} else if errs := validationutil.IsDNS1123Subdomain(spec.Group); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, strings.Join(errs, ",")))
} else if len(strings.Split(spec.Group, ".")) < 2 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, "should be a domain with at least one dot"))
}
allErrs = append(allErrs, validateEnumStrings(fldPath.Child("scope"), string(spec.Scope), []string{string(apiextensions.ClusterScoped), string(apiextensions.NamespaceScoped)}, true)...)
storageFlagCount := 0
versionsMap := map[string]bool{}
uniqueNames := true
for i, version := range spec.Versions {
if version.Storage {
storageFlagCount++
}
if versionsMap[version.Name] {
uniqueNames = false
} else {
versionsMap[version.Name] = true
}
if errs := validationutil.IsDNS1035Label(version.Name); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions").Index(i).Child("name"), spec.Versions[i].Name, strings.Join(errs, ",")))
}
subresources := getSubresourcesForVersion(spec, version.Name)
allErrs = append(allErrs, ValidateCustomResourceDefinitionVersion(&version, fldPath.Child("versions").Index(i), hasStatusEnabled(subresources))...)
}
// The top-level and per-version fields are mutual exclusive
if spec.Validation != nil && hasPerVersionSchema(spec.Versions) {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "top-level and per-version schemas are mutually exclusive"))
}
if spec.Subresources != nil && hasPerVersionSubresources(spec.Versions) {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("subresources"), "top-level and per-version subresources are mutually exclusive"))
}
if len(spec.AdditionalPrinterColumns) > 0 && hasPerVersionColumns(spec.Versions) {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalPrinterColumns"), "top-level and per-version additionalPrinterColumns are mutually exclusive"))
}
// Per-version fields may not all be set to identical values (top-level field should be used instead)
if hasIdenticalPerVersionSchema(spec.Versions) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version schemas may not all be set to identical values (top-level validation should be used instead)"))
}
if hasIdenticalPerVersionSubresources(spec.Versions) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version subresources may not all be set to identical values (top-level subresources should be used instead)"))
}
if hasIdenticalPerVersionColumns(spec.Versions) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "per-version additionalPrinterColumns may not all be set to identical values (top-level additionalPrinterColumns should be used instead)"))
}
if !uniqueNames {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must contain unique version names"))
}
if storageFlagCount != 1 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("versions"), spec.Versions, "must have exactly one version marked as storage version"))
}
if len(spec.Version) != 0 {
if errs := validationutil.IsDNS1035Label(spec.Version); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, strings.Join(errs, ",")))
}
if len(spec.Versions) >= 1 && spec.Versions[0].Name != spec.Version {
allErrs = append(allErrs, field.Invalid(fldPath.Child("version"), spec.Version, "must match the first version in spec.versions"))
}
}
// in addition to the basic name restrictions, some names are required for spec, but not for status
if len(spec.Names.Plural) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("names", "plural"), ""))
}
if len(spec.Names.Singular) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("names", "singular"), ""))
}
if len(spec.Names.Kind) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("names", "kind"), ""))
}
if len(spec.Names.ListKind) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("names", "listKind"), ""))
}
allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...)
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, hasAnyStatusEnabled(spec), fldPath.Child("validation"))...)
} else if spec.Validation != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate CustomResourceValidation"))
}
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...)
} else if spec.Subresources != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("subresources"), "disabled by feature-gate CustomResourceSubresources"))
}
for i := range spec.AdditionalPrinterColumns {
if errs := ValidateCustomResourceColumnDefinition(&spec.AdditionalPrinterColumns[i], fldPath.Child("additionalPrinterColumns").Index(i)); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
}
allErrs = append(allErrs, ValidateCustomResourceConversion(spec.Conversion, fldPath.Child("conversion"))...)
return allErrs
}
func validateEnumStrings(fldPath *field.Path, value string, accepted []string, required bool) field.ErrorList {
if value == "" {
if required {
return field.ErrorList{field.Required(fldPath, "")}
}
return field.ErrorList{}
}
for _, a := range accepted {
if a == value {
return field.ErrorList{}
}
}
return field.ErrorList{field.NotSupported(fldPath, value, accepted)}
}
// ValidateCustomResourceConversion statically validates
func ValidateCustomResourceConversion(conversion *apiextensions.CustomResourceConversion, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if conversion == nil {
return allErrs
}
allErrs = append(allErrs, validateEnumStrings(fldPath.Child("strategy"), string(conversion.Strategy), []string{string(apiextensions.NoneConverter), string(apiextensions.WebhookConverter)}, true)...)
if conversion.Strategy == apiextensions.WebhookConverter {
if conversion.WebhookClientConfig == nil {
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) {
allErrs = append(allErrs, field.Required(fldPath.Child("webhookClientConfig"), "required when strategy is set to Webhook"))
} else {
allErrs = append(allErrs, field.Required(fldPath.Child("webhookClientConfig"), "required when strategy is set to Webhook, but not allowed because the CustomResourceWebhookConversion feature is disabled"))
}
} else {
cc := conversion.WebhookClientConfig
switch {
case (cc.URL == nil) == (cc.Service == nil):
allErrs = append(allErrs, field.Required(fldPath.Child("webhookClientConfig"), "exactly one of url or service is required"))
case cc.URL != nil:
allErrs = append(allErrs, webhook.ValidateWebhookURL(fldPath.Child("webhookClientConfig").Child("url"), *cc.URL, true)...)
case cc.Service != nil:
allErrs = append(allErrs, webhook.ValidateWebhookService(fldPath.Child("webhookClientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path)...)
}
}
} else if conversion.WebhookClientConfig != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("webhookClientConfig"), "should not be set when strategy is not set to Webhook"))
}
return allErrs
}
// ValidateCustomResourceDefinitionSpecUpdate statically validates
func ValidateCustomResourceDefinitionSpecUpdate(spec, oldSpec *apiextensions.CustomResourceDefinitionSpec, established bool, fldPath *field.Path) field.ErrorList {
allErrs := ValidateCustomResourceDefinitionSpec(spec, fldPath)
if established {
// these effect the storage and cannot be changed therefore
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Scope, oldSpec.Scope, fldPath.Child("scope"))...)
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Kind, oldSpec.Names.Kind, fldPath.Child("names", "kind"))...)
}
// these affects the resource name, which is always immutable, so this can't be updated.
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Group, oldSpec.Group, fldPath.Child("group"))...)
allErrs = append(allErrs, genericvalidation.ValidateImmutableField(spec.Names.Plural, oldSpec.Names.Plural, fldPath.Child("names", "plural"))...)
return allErrs
}
// getSubresourcesForVersion returns the subresources for given version in given CRD spec.
// NOTE That this function assumes version always exist since it's used by the validation process
// that iterates through the existing versions.
func getSubresourcesForVersion(crd *apiextensions.CustomResourceDefinitionSpec, version string) *apiextensions.CustomResourceSubresources {
if !hasPerVersionSubresources(crd.Versions) {
return crd.Subresources
}
for _, v := range crd.Versions {
if version == v.Name {
return v.Subresources
}
}
return nil
}
// hasAnyStatusEnabled returns true if given CRD spec has at least one Status Subresource set
// among the top-level and per-version Subresources.
func hasAnyStatusEnabled(crd *apiextensions.CustomResourceDefinitionSpec) bool {
if hasStatusEnabled(crd.Subresources) {
return true
}
for _, v := range crd.Versions {
if hasStatusEnabled(v.Subresources) {
return true
}
}
return false
}
// hasStatusEnabled returns true if given CRD Subresources has non-nil Status set.
func hasStatusEnabled(subresources *apiextensions.CustomResourceSubresources) bool {
if subresources != nil && subresources.Status != nil {
return true
}
return false
}
// hasPerVersionSchema returns true if a CRD uses per-version schema.
func hasPerVersionSchema(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Schema != nil {
return true
}
}
return false
}
// hasPerVersionSubresources returns true if a CRD uses per-version subresources.
func hasPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Subresources != nil {
return true
}
}
return false
}
// hasPerVersionColumns returns true if a CRD uses per-version columns.
func hasPerVersionColumns(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if len(v.AdditionalPrinterColumns) > 0 {
return true
}
}
return false
}
// hasIdenticalPerVersionSchema returns true if a CRD sets identical non-nil values
// to all per-version schemas
func hasIdenticalPerVersionSchema(versions []apiextensions.CustomResourceDefinitionVersion) bool {
if len(versions) == 0 {
return false
}
value := versions[0].Schema
for _, v := range versions {
if v.Schema == nil || !apiequality.Semantic.DeepEqual(v.Schema, value) {
return false
}
}
return true
}
// hasIdenticalPerVersionSubresources returns true if a CRD sets identical non-nil values
// to all per-version subresources
func hasIdenticalPerVersionSubresources(versions []apiextensions.CustomResourceDefinitionVersion) bool {
if len(versions) == 0 {
return false
}
value := versions[0].Subresources
for _, v := range versions {
if v.Subresources == nil || !apiequality.Semantic.DeepEqual(v.Subresources, value) {
return false
}
}
return true
}
// hasIdenticalPerVersionColumns returns true if a CRD sets identical non-nil values
// to all per-version columns
func hasIdenticalPerVersionColumns(versions []apiextensions.CustomResourceDefinitionVersion) bool {
if len(versions) == 0 {
return false
}
value := versions[0].AdditionalPrinterColumns
for _, v := range versions {
if len(v.AdditionalPrinterColumns) == 0 || !apiequality.Semantic.DeepEqual(v.AdditionalPrinterColumns, value) {
return false
}
}
return true
}
// ValidateCustomResourceDefinitionStatus statically validates
func ValidateCustomResourceDefinitionStatus(status *apiextensions.CustomResourceDefinitionStatus, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&status.AcceptedNames, fldPath.Child("acceptedNames"))...)
return allErrs
}
// ValidateCustomResourceDefinitionNames statically validates
func ValidateCustomResourceDefinitionNames(names *apiextensions.CustomResourceDefinitionNames, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if errs := validationutil.IsDNS1035Label(names.Plural); len(names.Plural) > 0 && len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("plural"), names.Plural, strings.Join(errs, ",")))
}
if errs := validationutil.IsDNS1035Label(names.Singular); len(names.Singular) > 0 && len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("singular"), names.Singular, strings.Join(errs, ",")))
}
if errs := validationutil.IsDNS1035Label(strings.ToLower(names.Kind)); len(names.Kind) > 0 && len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), names.Kind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ",")))
}
if errs := validationutil.IsDNS1035Label(strings.ToLower(names.ListKind)); len(names.ListKind) > 0 && len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("listKind"), names.ListKind, "may have mixed case, but should otherwise match: "+strings.Join(errs, ",")))
}
for i, shortName := range names.ShortNames {
if errs := validationutil.IsDNS1035Label(shortName); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("shortNames").Index(i), shortName, strings.Join(errs, ",")))
}
}
// kind and listKind may not be the same or parsing become ambiguous
if len(names.Kind) > 0 && names.Kind == names.ListKind {
allErrs = append(allErrs, field.Invalid(fldPath.Child("listKind"), names.ListKind, "kind and listKind may not be the same"))
}
for i, category := range names.Categories {
if errs := validationutil.IsDNS1035Label(category); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("categories").Index(i), category, strings.Join(errs, ",")))
}
}
return allErrs
}
// ValidateCustomResourceColumnDefinition statically validates a printer column.
func ValidateCustomResourceColumnDefinition(col *apiextensions.CustomResourceColumnDefinition, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(col.Name) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("name"), ""))
}
if len(col.Type) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("type"), fmt.Sprintf("must be one of %s", strings.Join(printerColumnDatatypes.List(), ","))))
} else if !printerColumnDatatypes.Has(col.Type) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), col.Type, fmt.Sprintf("must be one of %s", strings.Join(printerColumnDatatypes.List(), ","))))
}
if len(col.Format) > 0 && !customResourceColumnDefinitionFormats.Has(col.Format) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("format"), col.Format, fmt.Sprintf("must be one of %s", strings.Join(customResourceColumnDefinitionFormats.List(), ","))))
}
if len(col.JSONPath) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("JSONPath"), ""))
} else if errs := validateSimpleJSONPath(col.JSONPath, fldPath.Child("JSONPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
}
return allErrs
}
// specStandardValidator applies validations for different OpenAPI specification versions.
type specStandardValidator interface {
validate(spec *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList
}
// ValidateCustomResourceDefinitionValidation statically validates
func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, statusSubresourceEnabled bool, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if customResourceValidation == nil {
return allErrs
}
if schema := customResourceValidation.OpenAPIV3Schema; schema != nil {
// if the status subresource is enabled, only certain fields are allowed inside the root schema.
// these fields are chosen such that, if status is extracted as properties["status"], it's validation is not lost.
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && statusSubresourceEnabled {
v := reflect.ValueOf(schema).Elem()
for i := 0; i < v.NumField(); i++ {
// skip zero values
if value := v.Field(i).Interface(); reflect.DeepEqual(value, reflect.Zero(reflect.TypeOf(value)).Interface()) {
continue
}
fieldName := v.Type().Field(i).Name
// only "object" type is valid at root of the schema since validation schema for status is extracted as properties["status"]
if fieldName == "Type" {
if schema.Type != "object" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema.type"), schema.Type, fmt.Sprintf(`only "object" is allowed as the type at the root of the schema if the status subresource is enabled`)))
break
}
continue
}
if !allowedAtRootSchema(fieldName) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), *schema, fmt.Sprintf(`only %v fields are allowed at the root of the schema if the status subresource is enabled`, allowedFieldsAtRootSchema)))
break
}
}
}
openAPIV3Schema := &specStandardValidatorV3{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
}
// if validation passed otherwise, make sure we can actually construct a schema validator from this custom resource validation.
if len(allErrs) == 0 {
if _, _, err := apiservervalidation.NewSchemaValidator(customResourceValidation); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath, "", fmt.Sprintf("error building validator: %v", err)))
}
}
return allErrs
}
// ValidateCustomResourceDefinitionOpenAPISchema statically validates
func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSchemaProps, fldPath *field.Path, ssv specStandardValidator) field.ErrorList {
allErrs := field.ErrorList{}
if schema == nil {
return allErrs
}
allErrs = append(allErrs, ssv.validate(schema, fldPath)...)
if schema.UniqueItems == true {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("uniqueItems"), "uniqueItems cannot be set to true since the runtime complexity becomes quadratic"))
}
// additionalProperties and properties are mutual exclusive because otherwise they
// contradict Kubernetes' API convention to ignore unknown fields.
//
// In other words:
// - properties are for structs,
// - additionalProperties are for map[string]interface{}
//
// Note: when patternProperties is added to OpenAPI some day, this will have to be
// restricted like additionalProperties.
if schema.AdditionalProperties != nil {
if len(schema.Properties) != 0 {
if schema.AdditionalProperties.Allows == false || schema.AdditionalProperties.Schema != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "additionalProperties and properties are mutual exclusive"))
}
}
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalProperties.Schema, fldPath.Child("additionalProperties"), ssv)...)
}
if len(schema.Properties) != 0 {
for property, jsonSchema := range schema.Properties {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("properties").Key(property), ssv)...)
}
}
if len(schema.PatternProperties) != 0 {
for property, jsonSchema := range schema.PatternProperties {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("patternProperties").Key(property), ssv)...)
}
}
if schema.AdditionalItems != nil {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.AdditionalItems.Schema, fldPath.Child("additionalItems"), ssv)...)
}
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Not, fldPath.Child("not"), ssv)...)
if len(schema.AllOf) != 0 {
for i, jsonSchema := range schema.AllOf {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("allOf").Index(i), ssv)...)
}
}
if len(schema.OneOf) != 0 {
for i, jsonSchema := range schema.OneOf {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("oneOf").Index(i), ssv)...)
}
}
if len(schema.AnyOf) != 0 {
for i, jsonSchema := range schema.AnyOf {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("anyOf").Index(i), ssv)...)
}
}
if len(schema.Definitions) != 0 {
for definition, jsonSchema := range schema.Definitions {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("definitions").Key(definition), ssv)...)
}
}
if schema.Items != nil {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema.Items.Schema, fldPath.Child("items"), ssv)...)
if len(schema.Items.JSONSchemas) != 0 {
for i, jsonSchema := range schema.Items.JSONSchemas {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(&jsonSchema, fldPath.Child("items").Index(i), ssv)...)
}
}
}
if schema.Dependencies != nil {
for dependency, jsonSchemaPropsOrStringArray := range schema.Dependencies {
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(jsonSchemaPropsOrStringArray.Schema, fldPath.Child("dependencies").Key(dependency), ssv)...)
}
}
return allErrs
}
type specStandardValidatorV3 struct{}
// validate validates against OpenAPI Schema v3.
func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if schema == nil {
return allErrs
}
if schema.Default != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "default is not supported"))
}
if schema.ID != "" {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("id"), "id is not supported"))
}
if schema.AdditionalItems != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalItems"), "additionalItems is not supported"))
}
if len(schema.PatternProperties) != 0 {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("patternProperties"), "patternProperties is not supported"))
}
if len(schema.Definitions) != 0 {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("definitions"), "definitions is not supported"))
}
if schema.Dependencies != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("dependencies"), "dependencies is not supported"))
}
if schema.Ref != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("$ref"), "$ref is not supported"))
}
if schema.Type == "null" {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("type"), "type cannot be set to null"))
}
if schema.Items != nil && len(schema.Items.JSONSchemas) != 0 {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("items"), "items must be a schema object and not an array"))
}
return allErrs
}
// ValidateCustomResourceDefinitionSubresources statically validates
func ValidateCustomResourceDefinitionSubresources(subresources *apiextensions.CustomResourceSubresources, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if subresources == nil {
return allErrs
}
if subresources.Scale != nil {
if len(subresources.Scale.SpecReplicasPath) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("scale.specReplicasPath"), ""))
} else {
// should be constrained json path under .spec
if errs := validateSimpleJSONPath(subresources.Scale.SpecReplicasPath, fldPath.Child("scale.specReplicasPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(subresources.Scale.SpecReplicasPath, ".spec.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.specReplicasPath"), subresources.Scale.SpecReplicasPath, "should be a json path under .spec"))
}
}
if len(subresources.Scale.StatusReplicasPath) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("scale.statusReplicasPath"), ""))
} else {
// should be constrained json path under .status
if errs := validateSimpleJSONPath(subresources.Scale.StatusReplicasPath, fldPath.Child("scale.statusReplicasPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(subresources.Scale.StatusReplicasPath, ".status.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.statusReplicasPath"), subresources.Scale.StatusReplicasPath, "should be a json path under .status"))
}
}
// if labelSelectorPath is present, it should be a constrained json path under .status
if subresources.Scale.LabelSelectorPath != nil && len(*subresources.Scale.LabelSelectorPath) > 0 {
if errs := validateSimpleJSONPath(*subresources.Scale.LabelSelectorPath, fldPath.Child("scale.labelSelectorPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(*subresources.Scale.LabelSelectorPath, ".status.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.labelSelectorPath"), subresources.Scale.LabelSelectorPath, "should be a json path under .status"))
}
}
}
return allErrs
}
func validateSimpleJSONPath(s string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
switch {
case len(s) == 0:
allErrs = append(allErrs, field.Invalid(fldPath, s, "must not be empty"))
case s[0] != '.':
allErrs = append(allErrs, field.Invalid(fldPath, s, "must be a simple json path starting with ."))
case s != ".":
if cs := strings.Split(s[1:], "."); len(cs) < 1 {
allErrs = append(allErrs, field.Invalid(fldPath, s, "must be a json path in the dot notation"))
}
}
return allErrs
}
var allowedFieldsAtRootSchema = []string{"Description", "Type", "Format", "Title", "Maximum", "ExclusiveMaximum", "Minimum", "ExclusiveMinimum", "MaxLength", "MinLength", "Pattern", "MaxItems", "MinItems", "UniqueItems", "MultipleOf", "Required", "Items", "Properties", "ExternalDocs", "Example"}
func allowedAtRootSchema(field string) bool {
for _, v := range allowedFieldsAtRootSchema {
if field == v {
return true
}
}
return false
}

File diff suppressed because it is too large Load Diff