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

View File

@@ -1,225 +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 apiserver
import (
"fmt"
"net/http"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/endpoints/discovery"
genericregistry "k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
serverstorage "k8s.io/apiserver/pkg/server/storage"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
"k8s.io/apiextensions-apiserver/pkg/controller/status"
"k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition"
_ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
_ "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion"
)
var (
Scheme = runtime.NewScheme()
Codecs = serializer.NewCodecFactory(Scheme)
// if you modify this, make sure you update the crEncoder
unversionedVersion = schema.GroupVersion{Group: "", Version: "v1"}
unversionedTypes = []runtime.Object{
&metav1.Status{},
&metav1.WatchEvent{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
}
)
func init() {
install.Install(Scheme)
// we need to add the options to empty v1
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Group: "", Version: "v1"})
Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...)
}
type ExtraConfig struct {
CRDRESTOptionsGetter genericregistry.RESTOptionsGetter
// MasterCount is used to detect whether cluster is HA, and if it is
// the CRD Establishing will be hold by 5 seconds.
MasterCount int
// ServiceResolver is used in CR webhook converters to resolve webhook's service names
ServiceResolver webhook.ServiceResolver
// AuthResolverWrapper is used in CR webhook converters
AuthResolverWrapper webhook.AuthenticationInfoResolverWrapper
}
type Config struct {
GenericConfig *genericapiserver.RecommendedConfig
ExtraConfig ExtraConfig
}
type completedConfig struct {
GenericConfig genericapiserver.CompletedConfig
ExtraConfig *ExtraConfig
}
type CompletedConfig struct {
// Embed a private pointer that cannot be instantiated outside of this package.
*completedConfig
}
type CustomResourceDefinitions struct {
GenericAPIServer *genericapiserver.GenericAPIServer
// provided for easier embedding
Informers internalinformers.SharedInformerFactory
}
// Complete fills in any fields not set that are required to have valid data. It's mutating the receiver.
func (cfg *Config) Complete() CompletedConfig {
c := completedConfig{
cfg.GenericConfig.Complete(),
&cfg.ExtraConfig,
}
c.GenericConfig.EnableDiscovery = false
c.GenericConfig.Version = &version.Info{
Major: "0",
Minor: "1",
}
return CompletedConfig{&c}
}
// New returns a new instance of CustomResourceDefinitions from the given config.
func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) (*CustomResourceDefinitions, error) {
genericServer, err := c.GenericConfig.New("apiextensions-apiserver", delegationTarget)
if err != nil {
return nil, err
}
s := &CustomResourceDefinitions{
GenericAPIServer: genericServer,
}
apiResourceConfig := c.GenericConfig.MergedResourceConfig
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(apiextensions.GroupName, Scheme, metav1.ParameterCodec, Codecs)
if apiResourceConfig.VersionEnabled(v1beta1.SchemeGroupVersion) {
storage := map[string]rest.Storage{}
// customresourcedefinitions
customResourceDefintionStorage := customresourcedefinition.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
storage["customresourcedefinitions"] = customResourceDefintionStorage
storage["customresourcedefinitions/status"] = customresourcedefinition.NewStatusREST(Scheme, customResourceDefintionStorage)
apiGroupInfo.VersionedResourcesStorageMap["v1beta1"] = storage
}
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
crdClient, err := internalclientset.NewForConfig(s.GenericAPIServer.LoopbackClientConfig)
if err != nil {
// it's really bad that this is leaking here, but until we can fix the test (which I'm pretty sure isn't even testing what it wants to test),
// we need to be able to move forward
return nil, fmt.Errorf("failed to create clientset: %v", err)
}
s.Informers = internalinformers.NewSharedInformerFactory(crdClient, 5*time.Minute)
delegateHandler := delegationTarget.UnprotectedHandler()
if delegateHandler == nil {
delegateHandler = http.NotFoundHandler()
}
versionDiscoveryHandler := &versionDiscoveryHandler{
discovery: map[schema.GroupVersion]*discovery.APIVersionHandler{},
delegate: delegateHandler,
}
groupDiscoveryHandler := &groupDiscoveryHandler{
discovery: map[string]*discovery.APIGroupHandler{},
delegate: delegateHandler,
}
establishingController := establish.NewEstablishingController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions())
crdHandler, err := NewCustomResourceDefinitionHandler(
versionDiscoveryHandler,
groupDiscoveryHandler,
s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(),
delegateHandler,
c.ExtraConfig.CRDRESTOptionsGetter,
c.GenericConfig.AdmissionControl,
establishingController,
c.ExtraConfig.ServiceResolver,
c.ExtraConfig.AuthResolverWrapper,
c.ExtraConfig.MasterCount,
)
if err != nil {
return nil, err
}
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)
crdController := NewDiscoveryController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler)
namingController := status.NewNamingConditionController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions())
finalizingController := finalizer.NewCRDFinalizer(
s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(),
crdClient.Apiextensions(),
crdHandler,
)
s.GenericAPIServer.AddPostStartHook("start-apiextensions-informers", func(context genericapiserver.PostStartHookContext) error {
s.Informers.Start(context.StopCh)
return nil
})
s.GenericAPIServer.AddPostStartHook("start-apiextensions-controllers", func(context genericapiserver.PostStartHookContext) error {
go crdController.Run(context.StopCh)
go namingController.Run(context.StopCh)
go establishingController.Run(context.StopCh)
go finalizingController.Run(5, context.StopCh)
return nil
})
return s, nil
}
func DefaultAPIResourceConfigSource() *serverstorage.ResourceConfig {
ret := serverstorage.NewResourceConfig()
// NOTE: GroupVersions listed here will be enabled by default. Don't put alpha versions in the list.
ret.EnableVersions(
v1beta1.SchemeGroupVersion,
)
return ret
}

View File

@@ -1,152 +0,0 @@
/*
Copyright 2018 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 conversion
import (
"fmt"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiserver/pkg/util/webhook"
)
// CRConverterFactory is the factory for all CR converters.
type CRConverterFactory struct {
// webhookConverterFactory is the factory for webhook converters.
// This field should not be used if CustomResourceWebhookConversion feature is disabled.
webhookConverterFactory *webhookConverterFactory
}
// NewCRConverterFactory creates a new CRConverterFactory
func NewCRConverterFactory(serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper) (*CRConverterFactory, error) {
converterFactory := &CRConverterFactory{}
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) {
webhookConverterFactory, err := newWebhookConverterFactory(serviceResolver, authResolverWrapper)
if err != nil {
return nil, err
}
converterFactory.webhookConverterFactory = webhookConverterFactory
}
return converterFactory, nil
}
// NewConverter returns a new CR converter based on the conversion settings in crd object.
func (m *CRConverterFactory) NewConverter(crd *apiextensions.CustomResourceDefinition) (safe, unsafe runtime.ObjectConvertor, err error) {
validVersions := map[schema.GroupVersion]bool{}
for _, version := range crd.Spec.Versions {
validVersions[schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}] = true
}
switch crd.Spec.Conversion.Strategy {
case apiextensions.NoneConverter:
unsafe = &crConverter{
clusterScoped: crd.Spec.Scope == apiextensions.ClusterScoped,
delegate: &nopConverter{
validVersions: validVersions,
},
}
return &safeConverterWrapper{unsafe}, unsafe, nil
case apiextensions.WebhookConverter:
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) {
return nil, nil, fmt.Errorf("webhook conversion is disabled on this cluster")
}
unsafe, err := m.webhookConverterFactory.NewWebhookConverter(validVersions, crd)
if err != nil {
return nil, nil, err
}
return &safeConverterWrapper{unsafe}, unsafe, nil
}
return nil, nil, fmt.Errorf("unknown conversion strategy %q for CRD %s", crd.Spec.Conversion.Strategy, crd.Name)
}
var _ runtime.ObjectConvertor = &crConverter{}
// crConverter extends the delegate with generic CR conversion behaviour. The delegate will implement the
// user defined conversion strategy given in the CustomResourceDefinition.
type crConverter struct {
delegate runtime.ObjectConvertor
clusterScoped bool
}
func (c *crConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
// We currently only support metadata.namespace and metadata.name.
switch {
case label == "metadata.name":
return label, value, nil
case !c.clusterScoped && label == "metadata.namespace":
return label, value, nil
default:
return "", "", fmt.Errorf("field label not supported: %s", label)
}
}
func (c *crConverter) Convert(in, out, context interface{}) error {
return c.delegate.Convert(in, out, context)
}
// ConvertToVersion converts in object to the given gvk in place and returns the same `in` object.
func (c *crConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
// Run the converter on the list items instead of list itself
if list, ok := in.(*unstructured.UnstructuredList); ok {
for i := range list.Items {
obj, err := c.delegate.ConvertToVersion(&list.Items[i], target)
if err != nil {
return nil, err
}
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("output type %T in not valid for unstructured conversion", obj)
}
list.Items[i] = *u
}
return list, nil
}
return c.delegate.ConvertToVersion(in, target)
}
// safeConverterWrapper is a wrapper over an unsafe object converter that makes copy of the input and then delegate to the unsafe converter.
type safeConverterWrapper struct {
unsafe runtime.ObjectConvertor
}
var _ runtime.ObjectConvertor = &nopConverter{}
// ConvertFieldLabel delegate the call to the unsafe converter.
func (c *safeConverterWrapper) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
return c.unsafe.ConvertFieldLabel(gvk, label, value)
}
// Convert makes a copy of in object and then delegate the call to the unsafe converter.
func (c *safeConverterWrapper) Convert(in, out, context interface{}) error {
inObject, ok := in.(runtime.Object)
if !ok {
return fmt.Errorf("input type %T in not valid for object conversion", in)
}
return c.unsafe.Convert(inObject.DeepCopyObject(), out, context)
}
// ConvertToVersion makes a copy of in object and then delegate the call to the unsafe converter.
func (c *safeConverterWrapper) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
return c.unsafe.ConvertToVersion(in.DeepCopyObject(), target)
}

View File

@@ -1,79 +0,0 @@
/*
Copyright 2018 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 conversion
import (
"errors"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// nopConverter is a converter that only sets the apiVersion fields, but does not real conversion.
type nopConverter struct {
validVersions map[schema.GroupVersion]bool
}
var _ runtime.ObjectConvertor = &nopConverter{}
func (nopConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
return "", "", errors.New("unstructured cannot convert field labels")
}
func (c *nopConverter) Convert(in, out, context interface{}) error {
unstructIn, ok := in.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("input type %T in not valid for unstructured conversion", in)
}
unstructOut, ok := out.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("output type %T in not valid for unstructured conversion", out)
}
outGVK := unstructOut.GroupVersionKind()
if !c.validVersions[outGVK.GroupVersion()] {
return fmt.Errorf("request to convert CR from an invalid group/version: %s", outGVK.String())
}
inGVK := unstructIn.GroupVersionKind()
if !c.validVersions[inGVK.GroupVersion()] {
return fmt.Errorf("request to convert CR to an invalid group/version: %s", inGVK.String())
}
unstructOut.SetUnstructuredContent(unstructIn.UnstructuredContent())
_, err := c.ConvertToVersion(unstructOut, outGVK.GroupVersion())
if err != nil {
return err
}
return nil
}
func (c *nopConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
kind := in.GetObjectKind().GroupVersionKind()
gvk, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{kind})
if !ok {
// TODO: should this be a typed error?
return nil, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", kind, target)
}
if !c.validVersions[gvk.GroupVersion()] {
return nil, fmt.Errorf("request to convert CR to an invalid group/version: %s", gvk.String())
}
in.GetObjectKind().SetGroupVersionKind(gvk)
return in, nil
}

View File

@@ -1,350 +0,0 @@
/*
Copyright 2018 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 conversion
import (
"context"
"errors"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/rest"
internal "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
)
type webhookConverterFactory struct {
clientManager webhook.ClientManager
}
func newWebhookConverterFactory(serviceResolver webhook.ServiceResolver, authResolverWrapper webhook.AuthenticationInfoResolverWrapper) (*webhookConverterFactory, error) {
clientManager, err := webhook.NewClientManager(v1beta1.SchemeGroupVersion, v1beta1.AddToScheme)
if err != nil {
return nil, err
}
authInfoResolver, err := webhook.NewDefaultAuthenticationInfoResolver("")
if err != nil {
return nil, err
}
// Set defaults which may be overridden later.
clientManager.SetAuthenticationInfoResolver(authInfoResolver)
clientManager.SetAuthenticationInfoResolverWrapper(authResolverWrapper)
clientManager.SetServiceResolver(serviceResolver)
return &webhookConverterFactory{clientManager}, nil
}
// webhookConverter is a converter that calls an external webhook to do the CR conversion.
type webhookConverter struct {
validVersions map[schema.GroupVersion]bool
clientManager webhook.ClientManager
restClient *rest.RESTClient
name string
nopConverter nopConverter
}
func webhookClientConfigForCRD(crd *internal.CustomResourceDefinition) *webhook.ClientConfig {
apiConfig := crd.Spec.Conversion.WebhookClientConfig
ret := webhook.ClientConfig{
Name: fmt.Sprintf("conversion_webhook_for_%s", crd.Name),
CABundle: apiConfig.CABundle,
}
if apiConfig.URL != nil {
ret.URL = *apiConfig.URL
}
if apiConfig.Service != nil {
ret.Service = &webhook.ClientConfigService{
Name: apiConfig.Service.Name,
Namespace: apiConfig.Service.Namespace,
}
if apiConfig.Service.Path != nil {
ret.Service.Path = *apiConfig.Service.Path
}
}
return &ret
}
var _ runtime.ObjectConvertor = &webhookConverter{}
func (f *webhookConverterFactory) NewWebhookConverter(validVersions map[schema.GroupVersion]bool, crd *internal.CustomResourceDefinition) (*webhookConverter, error) {
restClient, err := f.clientManager.HookClient(*webhookClientConfigForCRD(crd))
if err != nil {
return nil, err
}
return &webhookConverter{
clientManager: f.clientManager,
validVersions: validVersions,
restClient: restClient,
name: crd.Name,
nopConverter: nopConverter{validVersions: validVersions},
}, nil
}
func (webhookConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
return "", "", errors.New("unstructured cannot convert field labels")
}
func (c *webhookConverter) Convert(in, out, context interface{}) error {
unstructIn, ok := in.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("input type %T in not valid for unstructured conversion", in)
}
unstructOut, ok := out.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("output type %T in not valid for unstructured conversion", out)
}
outGVK := unstructOut.GroupVersionKind()
if !c.validVersions[outGVK.GroupVersion()] {
return fmt.Errorf("request to convert CR from an invalid group/version: %s", outGVK.String())
}
inGVK := unstructIn.GroupVersionKind()
if !c.validVersions[inGVK.GroupVersion()] {
return fmt.Errorf("request to convert CR to an invalid group/version: %s", inGVK.String())
}
converted, err := c.ConvertToVersion(unstructIn, outGVK.GroupVersion())
if err != nil {
return err
}
unstructuredConverted, ok := converted.(runtime.Unstructured)
if !ok {
// this should not happened
return fmt.Errorf("CR conversion failed")
}
unstructOut.SetUnstructuredContent(unstructuredConverted.UnstructuredContent())
return nil
}
func createConversionReview(obj runtime.Object, apiVersion string) *v1beta1.ConversionReview {
listObj, isList := obj.(*unstructured.UnstructuredList)
var objects []runtime.RawExtension
if isList {
for i := 0; i < len(listObj.Items); i++ {
// Only sent item for conversion, if the apiVersion is different
if listObj.Items[i].GetAPIVersion() != apiVersion {
objects = append(objects, runtime.RawExtension{Object: &listObj.Items[i]})
}
}
} else {
if obj.GetObjectKind().GroupVersionKind().GroupVersion().String() != apiVersion {
objects = []runtime.RawExtension{{Object: obj}}
}
}
return &v1beta1.ConversionReview{
Request: &v1beta1.ConversionRequest{
Objects: objects,
DesiredAPIVersion: apiVersion,
UID: uuid.NewUUID(),
},
Response: &v1beta1.ConversionResponse{},
}
}
func getRawExtensionObject(rx runtime.RawExtension) (runtime.Object, error) {
if rx.Object != nil {
return rx.Object, nil
}
u := unstructured.Unstructured{}
err := u.UnmarshalJSON(rx.Raw)
if err != nil {
return nil, err
}
return &u, nil
}
// getTargetGroupVersion returns group/version which should be used to convert in objects to.
// String version of the return item is APIVersion.
func getTargetGroupVersion(in runtime.Object, target runtime.GroupVersioner) (schema.GroupVersion, error) {
fromGVK := in.GetObjectKind().GroupVersionKind()
toGVK, ok := target.KindForGroupVersionKinds([]schema.GroupVersionKind{fromGVK})
if !ok {
// TODO: should this be a typed error?
return schema.GroupVersion{}, fmt.Errorf("%v is unstructured and is not suitable for converting to %q", fromGVK.String(), target)
}
return toGVK.GroupVersion(), nil
}
func (c *webhookConverter) ConvertToVersion(in runtime.Object, target runtime.GroupVersioner) (runtime.Object, error) {
// In general, the webhook should not do any defaulting or validation. A special case of that is an empty object
// conversion that must result an empty object and practically is the same as nopConverter.
// A smoke test in API machinery calls the converter on empty objects. As this case happens consistently
// it special cased here not to call webhook converter. The test initiated here:
// https://github.com/kubernetes/kubernetes/blob/dbb448bbdcb9e440eee57024ffa5f1698956a054/staging/src/k8s.io/apiserver/pkg/storage/cacher/cacher.go#L201
if isEmptyUnstructuredObject(in) {
return c.nopConverter.ConvertToVersion(in, target)
}
toGV, err := getTargetGroupVersion(in, target)
if err != nil {
return nil, err
}
if !c.validVersions[toGV] {
return nil, fmt.Errorf("request to convert CR to an invalid group/version: %s", toGV.String())
}
fromGV := in.GetObjectKind().GroupVersionKind().GroupVersion()
if !c.validVersions[fromGV] {
return nil, fmt.Errorf("request to convert CR from an invalid group/version: %s", fromGV.String())
}
listObj, isList := in.(*unstructured.UnstructuredList)
if isList {
for i, item := range listObj.Items {
fromGV := item.GroupVersionKind().GroupVersion()
if !c.validVersions[fromGV] {
return nil, fmt.Errorf("input list has invalid group/version `%v` at `%v` index", fromGV, i)
}
}
}
request := createConversionReview(in, toGV.String())
if len(request.Request.Objects) == 0 {
if !isList {
return in, nil
}
out := listObj.DeepCopy()
out.SetAPIVersion(toGV.String())
return out, nil
}
response := &v1beta1.ConversionReview{}
// TODO: Figure out if adding one second timeout make sense here.
ctx := context.TODO()
r := c.restClient.Post().Context(ctx).Body(request).Do()
if err := r.Into(response); err != nil {
// TODO: Return a webhook specific error to be able to convert it to meta.Status
return nil, fmt.Errorf("calling to conversion webhook failed for %s: %v", c.name, err)
}
if response.Response == nil {
// TODO: Return a webhook specific error to be able to convert it to meta.Status
return nil, fmt.Errorf("conversion webhook response was absent for %s", c.name)
}
if response.Response.Result.Status != v1.StatusSuccess {
// TODO return status message as error
return nil, fmt.Errorf("conversion request failed for %v, Response: %v", in.GetObjectKind(), response)
}
if len(response.Response.ConvertedObjects) != len(request.Request.Objects) {
return nil, fmt.Errorf("expected %v converted objects, got %v", len(request.Request.Objects), len(response.Response.ConvertedObjects))
}
if isList {
convertedList := listObj.DeepCopy()
// Collection of items sent for conversion is different than list items
// because only items that needed conversion has been sent.
convertedIndex := 0
for i := 0; i < len(listObj.Items); i++ {
if listObj.Items[i].GetAPIVersion() == toGV.String() {
// This item has not been sent for conversion, skip it.
continue
}
converted, err := getRawExtensionObject(response.Response.ConvertedObjects[convertedIndex])
convertedIndex++
original := listObj.Items[i]
if err != nil {
return nil, fmt.Errorf("invalid converted object at index %v: %v", convertedIndex, err)
}
if e, a := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); e != a {
return nil, fmt.Errorf("invalid converted object at index %v: invalid groupVersion, e=%v, a=%v", convertedIndex, e, a)
}
if e, a := original.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; e != a {
return nil, fmt.Errorf("invalid converted object at index %v: invalid kind, e=%v, a=%v", convertedIndex, e, a)
}
unstructConverted, ok := converted.(*unstructured.Unstructured)
if !ok {
// this should not happened
return nil, fmt.Errorf("CR conversion failed")
}
if err := validateConvertedObject(&listObj.Items[i], unstructConverted); err != nil {
return nil, fmt.Errorf("invalid converted object at index %v: %v", convertedIndex, err)
}
convertedList.Items[i] = *unstructConverted
}
convertedList.SetAPIVersion(toGV.String())
return convertedList, nil
}
if len(response.Response.ConvertedObjects) != 1 {
// This should not happened
return nil, fmt.Errorf("CR conversion failed")
}
converted, err := getRawExtensionObject(response.Response.ConvertedObjects[0])
if err != nil {
return nil, err
}
if e, a := toGV, converted.GetObjectKind().GroupVersionKind().GroupVersion(); e != a {
return nil, fmt.Errorf("invalid converted object: invalid groupVersion, e=%v, a=%v", e, a)
}
if e, a := in.GetObjectKind().GroupVersionKind().Kind, converted.GetObjectKind().GroupVersionKind().Kind; e != a {
return nil, fmt.Errorf("invalid converted object: invalid kind, e=%v, a=%v", e, a)
}
unstructConverted, ok := converted.(*unstructured.Unstructured)
if !ok {
// this should not happened
return nil, fmt.Errorf("CR conversion failed")
}
unstructIn, ok := in.(*unstructured.Unstructured)
if !ok {
// this should not happened
return nil, fmt.Errorf("CR conversion failed")
}
if err := validateConvertedObject(unstructIn, unstructConverted); err != nil {
return nil, fmt.Errorf("invalid converted object: %v", err)
}
return converted, nil
}
func validateConvertedObject(unstructIn, unstructOut *unstructured.Unstructured) error {
if e, a := unstructIn.GetKind(), unstructOut.GetKind(); e != a {
return fmt.Errorf("must have the same kind: %v != %v", e, a)
}
if e, a := unstructIn.GetName(), unstructOut.GetName(); e != a {
return fmt.Errorf("must have the same name: %v != %v", e, a)
}
if e, a := unstructIn.GetNamespace(), unstructOut.GetNamespace(); e != a {
return fmt.Errorf("must have the same namespace: %v != %v", e, a)
}
if e, a := unstructIn.GetUID(), unstructOut.GetUID(); e != a {
return fmt.Errorf("must have the same UID: %v != %v", e, a)
}
return nil
}
// isEmptyUnstructuredObject returns true if in is an empty unstructured object, i.e. an unstructured object that does
// not have any field except apiVersion and kind.
func isEmptyUnstructuredObject(in runtime.Object) bool {
u, ok := in.(*unstructured.Unstructured)
if !ok {
return false
}
if len(u.Object) != 2 {
return false
}
if _, ok := u.Object["kind"]; !ok {
return false
}
if _, ok := u.Object["apiVersion"]; !ok {
return false
}
return true
}

View File

@@ -1,127 +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 apiserver
import (
"net/http"
"strings"
"sync"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/discovery"
)
type versionDiscoveryHandler struct {
// TODO, writing is infrequent, optimize this
discoveryLock sync.RWMutex
discovery map[schema.GroupVersion]*discovery.APIVersionHandler
delegate http.Handler
}
func (r *versionDiscoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
pathParts := splitPath(req.URL.Path)
// only match /apis/<group>/<version>
if len(pathParts) != 3 || pathParts[0] != "apis" {
r.delegate.ServeHTTP(w, req)
return
}
discovery, ok := r.getDiscovery(schema.GroupVersion{Group: pathParts[1], Version: pathParts[2]})
if !ok {
r.delegate.ServeHTTP(w, req)
return
}
discovery.ServeHTTP(w, req)
}
func (r *versionDiscoveryHandler) getDiscovery(gv schema.GroupVersion) (*discovery.APIVersionHandler, bool) {
r.discoveryLock.RLock()
defer r.discoveryLock.RUnlock()
ret, ok := r.discovery[gv]
return ret, ok
}
func (r *versionDiscoveryHandler) setDiscovery(gv schema.GroupVersion, discovery *discovery.APIVersionHandler) {
r.discoveryLock.Lock()
defer r.discoveryLock.Unlock()
r.discovery[gv] = discovery
}
func (r *versionDiscoveryHandler) unsetDiscovery(gv schema.GroupVersion) {
r.discoveryLock.Lock()
defer r.discoveryLock.Unlock()
delete(r.discovery, gv)
}
type groupDiscoveryHandler struct {
// TODO, writing is infrequent, optimize this
discoveryLock sync.RWMutex
discovery map[string]*discovery.APIGroupHandler
delegate http.Handler
}
func (r *groupDiscoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
pathParts := splitPath(req.URL.Path)
// only match /apis/<group>
if len(pathParts) != 2 || pathParts[0] != "apis" {
r.delegate.ServeHTTP(w, req)
return
}
discovery, ok := r.getDiscovery(pathParts[1])
if !ok {
r.delegate.ServeHTTP(w, req)
return
}
discovery.ServeHTTP(w, req)
}
func (r *groupDiscoveryHandler) getDiscovery(group string) (*discovery.APIGroupHandler, bool) {
r.discoveryLock.RLock()
defer r.discoveryLock.RUnlock()
ret, ok := r.discovery[group]
return ret, ok
}
func (r *groupDiscoveryHandler) setDiscovery(group string, discovery *discovery.APIGroupHandler) {
r.discoveryLock.Lock()
defer r.discoveryLock.Unlock()
r.discovery[group] = discovery
}
func (r *groupDiscoveryHandler) unsetDiscovery(group string) {
r.discoveryLock.Lock()
defer r.discoveryLock.Unlock()
delete(r.discovery, group)
}
// splitPath returns the segments for a URL path.
func splitPath(path string) []string {
path = strings.Trim(path, "/")
if path == "" {
return []string{}
}
return strings.Split(path, "/")
}

View File

@@ -1,279 +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 apiserver
import (
"fmt"
"sort"
"time"
"k8s.io/klog"
autoscaling "k8s.io/api/autoscaling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/endpoints/discovery"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
)
type DiscoveryController struct {
versionHandler *versionDiscoveryHandler
groupHandler *groupDiscoveryHandler
crdLister listers.CustomResourceDefinitionLister
crdsSynced cache.InformerSynced
// To allow injection for testing.
syncFn func(version schema.GroupVersion) error
queue workqueue.RateLimitingInterface
}
func NewDiscoveryController(crdInformer informers.CustomResourceDefinitionInformer, versionHandler *versionDiscoveryHandler, groupHandler *groupDiscoveryHandler) *DiscoveryController {
c := &DiscoveryController{
versionHandler: versionHandler,
groupHandler: groupHandler,
crdLister: crdInformer.Lister(),
crdsSynced: crdInformer.Informer().HasSynced,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "DiscoveryController"),
}
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addCustomResourceDefinition,
UpdateFunc: c.updateCustomResourceDefinition,
DeleteFunc: c.deleteCustomResourceDefinition,
})
c.syncFn = c.sync
return c
}
func (c *DiscoveryController) sync(version schema.GroupVersion) error {
apiVersionsForDiscovery := []metav1.GroupVersionForDiscovery{}
apiResourcesForDiscovery := []metav1.APIResource{}
versionsForDiscoveryMap := map[metav1.GroupVersion]bool{}
crds, err := c.crdLister.List(labels.Everything())
if err != nil {
return err
}
foundVersion := false
foundGroup := false
for _, crd := range crds {
if !apiextensions.IsCRDConditionTrue(crd, apiextensions.Established) {
continue
}
if crd.Spec.Group != version.Group {
continue
}
foundThisVersion := false
for _, v := range crd.Spec.Versions {
if !v.Served {
continue
}
// If there is any Served version, that means the group should show up in discovery
foundGroup = true
gv := metav1.GroupVersion{Group: crd.Spec.Group, Version: v.Name}
if !versionsForDiscoveryMap[gv] {
versionsForDiscoveryMap[gv] = true
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
GroupVersion: crd.Spec.Group + "/" + v.Name,
Version: v.Name,
})
}
if v.Name == version.Version {
foundThisVersion = true
}
}
if !foundThisVersion {
continue
}
foundVersion = true
verbs := metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"})
// if we're terminating we don't allow some verbs
if apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating) {
verbs = metav1.Verbs([]string{"delete", "deletecollection", "get", "list", "watch"})
}
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Name: crd.Status.AcceptedNames.Plural,
SingularName: crd.Status.AcceptedNames.Singular,
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
Kind: crd.Status.AcceptedNames.Kind,
Verbs: verbs,
ShortNames: crd.Status.AcceptedNames.ShortNames,
Categories: crd.Status.AcceptedNames.Categories,
})
subresources, err := getSubresourcesForVersion(crd, version.Version)
if err != nil {
return err
}
if subresources != nil && subresources.Status != nil {
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Name: crd.Status.AcceptedNames.Plural + "/status",
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
Kind: crd.Status.AcceptedNames.Kind,
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
})
}
if subresources != nil && subresources.Scale != nil {
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Group: autoscaling.GroupName,
Version: "v1",
Kind: "Scale",
Name: crd.Status.AcceptedNames.Plural + "/scale",
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
})
}
}
if !foundGroup {
c.groupHandler.unsetDiscovery(version.Group)
c.versionHandler.unsetDiscovery(version)
return nil
}
sortGroupDiscoveryByKubeAwareVersion(apiVersionsForDiscovery)
apiGroup := metav1.APIGroup{
Name: version.Group,
Versions: apiVersionsForDiscovery,
// the preferred versions for a group is the first item in
// apiVersionsForDiscovery after it put in the right ordered
PreferredVersion: apiVersionsForDiscovery[0],
}
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))
if !foundVersion {
c.versionHandler.unsetDiscovery(version)
return nil
}
c.versionHandler.setDiscovery(version, discovery.NewAPIVersionHandler(Codecs, version, discovery.APIResourceListerFunc(func() []metav1.APIResource {
return apiResourcesForDiscovery
})))
return nil
}
func sortGroupDiscoveryByKubeAwareVersion(gd []metav1.GroupVersionForDiscovery) {
sort.Slice(gd, func(i, j int) bool {
return version.CompareKubeAwareVersionStrings(gd[i].Version, gd[j].Version) > 0
})
}
func (c *DiscoveryController) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
defer klog.Infof("Shutting down DiscoveryController")
klog.Infof("Starting DiscoveryController")
if !cache.WaitForCacheSync(stopCh, c.crdsSynced) {
utilruntime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
return
}
// only start one worker thread since its a slow moving API
go wait.Until(c.runWorker, time.Second, stopCh)
<-stopCh
}
func (c *DiscoveryController) runWorker() {
for c.processNextWorkItem() {
}
}
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
func (c *DiscoveryController) processNextWorkItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.syncFn(key.(schema.GroupVersion))
if err == nil {
c.queue.Forget(key)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
c.queue.AddRateLimited(key)
return true
}
func (c *DiscoveryController) enqueue(obj *apiextensions.CustomResourceDefinition) {
for _, v := range obj.Spec.Versions {
c.queue.Add(schema.GroupVersion{Group: obj.Spec.Group, Version: v.Name})
}
}
func (c *DiscoveryController) addCustomResourceDefinition(obj interface{}) {
castObj := obj.(*apiextensions.CustomResourceDefinition)
klog.V(4).Infof("Adding customresourcedefinition %s", castObj.Name)
c.enqueue(castObj)
}
func (c *DiscoveryController) updateCustomResourceDefinition(oldObj, newObj interface{}) {
castNewObj := newObj.(*apiextensions.CustomResourceDefinition)
castOldObj := oldObj.(*apiextensions.CustomResourceDefinition)
klog.V(4).Infof("Updating customresourcedefinition %s", castOldObj.Name)
// Enqueue both old and new object to make sure we remove and add appropriate Versions.
// The working queue will resolve any duplicates and only changes will stay in the queue.
c.enqueue(castNewObj)
c.enqueue(castOldObj)
}
func (c *DiscoveryController) deleteCustomResourceDefinition(obj interface{}) {
castObj, ok := obj.(*apiextensions.CustomResourceDefinition)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
klog.Errorf("Couldn't get object from tombstone %#v", obj)
return
}
castObj, ok = tombstone.Obj.(*apiextensions.CustomResourceDefinition)
if !ok {
klog.Errorf("Tombstone contained object that is not expected %#v", obj)
return
}
}
klog.V(4).Infof("Deleting customresourcedefinition %q", castObj.Name)
c.enqueue(castObj)
}

View File

@@ -1,932 +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 apiserver
import (
"fmt"
"net/http"
"path"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"k8s.io/klog"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
"k8s.io/apimachinery/pkg/types"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/endpoints/handlers"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/metrics"
apirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/storage/storagebackend"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/scale"
"k8s.io/client-go/scale/scheme/autoscalingv1"
"k8s.io/client-go/tools/cache"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
"k8s.io/apiserver/pkg/util/webhook"
)
// crdHandler serves the `/apis` endpoint.
// This is registered as a filter so that it never collides with any explicitly registered endpoints
type crdHandler struct {
versionDiscoveryHandler *versionDiscoveryHandler
groupDiscoveryHandler *groupDiscoveryHandler
customStorageLock sync.Mutex
// customStorage contains a crdStorageMap
// atomic.Value has a very good read performance compared to sync.RWMutex
// see https://gist.github.com/dim/152e6bf80e1384ea72e17ac717a5000a
// which is suited for most read and rarely write cases
customStorage atomic.Value
crdLister listers.CustomResourceDefinitionLister
delegate http.Handler
restOptionsGetter generic.RESTOptionsGetter
admission admission.Interface
establishingController *establish.EstablishingController
// MasterCount is used to implement sleep to improve
// CRD establishing process for HA clusters.
masterCount int
converterFactory *conversion.CRConverterFactory
}
// crdInfo stores enough information to serve the storage for the custom resource
type crdInfo struct {
// spec and acceptedNames are used to compare against if a change is made on a CRD. We only update
// the storage if one of these changes.
spec *apiextensions.CustomResourceDefinitionSpec
acceptedNames *apiextensions.CustomResourceDefinitionNames
// Storage per version
storages map[string]customresource.CustomResourceStorage
// Request scope per version
requestScopes map[string]handlers.RequestScope
// Scale scope per version
scaleRequestScopes map[string]handlers.RequestScope
// Status scope per version
statusRequestScopes map[string]handlers.RequestScope
// storageVersion is the CRD version used when storing the object in etcd.
storageVersion string
}
// crdStorageMap goes from customresourcedefinition to its storage
type crdStorageMap map[types.UID]*crdInfo
func NewCustomResourceDefinitionHandler(
versionDiscoveryHandler *versionDiscoveryHandler,
groupDiscoveryHandler *groupDiscoveryHandler,
crdInformer informers.CustomResourceDefinitionInformer,
delegate http.Handler,
restOptionsGetter generic.RESTOptionsGetter,
admission admission.Interface,
establishingController *establish.EstablishingController,
serviceResolver webhook.ServiceResolver,
authResolverWrapper webhook.AuthenticationInfoResolverWrapper,
masterCount int) (*crdHandler, error) {
ret := &crdHandler{
versionDiscoveryHandler: versionDiscoveryHandler,
groupDiscoveryHandler: groupDiscoveryHandler,
customStorage: atomic.Value{},
crdLister: crdInformer.Lister(),
delegate: delegate,
restOptionsGetter: restOptionsGetter,
admission: admission,
establishingController: establishingController,
masterCount: masterCount,
}
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
UpdateFunc: ret.updateCustomResourceDefinition,
DeleteFunc: func(obj interface{}) {
ret.removeDeadStorage()
},
})
crConverterFactory, err := conversion.NewCRConverterFactory(serviceResolver, authResolverWrapper)
if err != nil {
return nil, err
}
ret.converterFactory = crConverterFactory
ret.customStorage.Store(crdStorageMap{})
return ret, nil
}
func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
requestInfo, ok := apirequest.RequestInfoFrom(ctx)
if !ok {
responsewriters.InternalError(w, req, fmt.Errorf("no RequestInfo found in the context"))
return
}
if !requestInfo.IsResourceRequest {
pathParts := splitPath(requestInfo.Path)
// only match /apis/<group>/<version>
// only registered under /apis
if len(pathParts) == 3 {
r.versionDiscoveryHandler.ServeHTTP(w, req)
return
}
// only match /apis/<group>
if len(pathParts) == 2 {
r.groupDiscoveryHandler.ServeHTTP(w, req)
return
}
r.delegate.ServeHTTP(w, req)
return
}
crdName := requestInfo.Resource + "." + requestInfo.APIGroup
crd, err := r.crdLister.Get(crdName)
if apierrors.IsNotFound(err) {
r.delegate.ServeHTTP(w, req)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if !apiextensions.HasServedCRDVersion(crd, requestInfo.APIVersion) {
r.delegate.ServeHTTP(w, req)
return
}
// There is a small chance that a CRD is being served because NamesAccepted condition is true,
// but it becomes "unserved" because another names update leads to a conflict
// and EstablishingController wasn't fast enough to put the CRD into the Established condition.
// We accept this as the problem is small and self-healing.
if !apiextensions.IsCRDConditionTrue(crd, apiextensions.NamesAccepted) &&
!apiextensions.IsCRDConditionTrue(crd, apiextensions.Established) {
r.delegate.ServeHTTP(w, req)
return
}
terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating)
crdInfo, err := r.getOrCreateServingInfoFor(crd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
verb := strings.ToUpper(requestInfo.Verb)
resource := requestInfo.Resource
subresource := requestInfo.Subresource
scope := metrics.CleanScope(requestInfo)
supportedTypes := []string{
string(types.JSONPatchType),
string(types.MergePatchType),
}
var handler http.HandlerFunc
subresources, err := getSubresourcesForVersion(crd, requestInfo.APIVersion)
if err != nil {
utilruntime.HandleError(err)
http.Error(w, "the server could not properly serve the CR subresources", http.StatusInternalServerError)
return
}
switch {
case subresource == "status" && subresources != nil && subresources.Status != nil:
handler = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case subresource == "scale" && subresources != nil && subresources.Scale != nil:
handler = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case len(subresource) == 0:
handler = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
default:
http.Error(w, "the server could not find the requested resource", http.StatusNotFound)
}
if handler != nil {
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
handler(w, req)
return
}
}
func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.requestScopes[requestInfo.APIVersion]
storage := crdInfo.storages[requestInfo.APIVersion].CustomResource
minRequestTimeout := 1 * time.Minute
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, storage, requestScope)
case "list":
forceWatch := false
return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
case "watch":
forceWatch := true
return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
case "create":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.CreateResource(storage, requestScope, r.admission)
case "update":
return handlers.UpdateResource(storage, requestScope, r.admission)
case "patch":
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
case "delete":
allowsOptions := true
return handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
case "deletecollection":
checkBody := true
return handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
}
func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.statusRequestScopes[requestInfo.APIVersion]
storage := crdInfo.storages[requestInfo.APIVersion].Status
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, nil, requestScope)
case "update":
return handlers.UpdateResource(storage, requestScope, r.admission)
case "patch":
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
}
func (r *crdHandler) serveScale(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.scaleRequestScopes[requestInfo.APIVersion]
storage := crdInfo.storages[requestInfo.APIVersion].Scale
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, nil, requestScope)
case "update":
return handlers.UpdateResource(storage, requestScope, r.admission)
case "patch":
return handlers.PatchResource(storage, requestScope, r.admission, supportedTypes)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
}
func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) {
oldCRD := oldObj.(*apiextensions.CustomResourceDefinition)
newCRD := newObj.(*apiextensions.CustomResourceDefinition)
r.customStorageLock.Lock()
defer r.customStorageLock.Unlock()
// Add CRD to the establishing controller queue.
// For HA clusters, we want to prevent race conditions when changing status to Established,
// so we want to be sure that CRD is Installing at least for 5 seconds before Establishing it.
// TODO: find a real HA safe checkpointing mechanism instead of an arbitrary wait.
if !apiextensions.IsCRDConditionTrue(newCRD, apiextensions.Established) &&
apiextensions.IsCRDConditionTrue(newCRD, apiextensions.NamesAccepted) {
if r.masterCount > 1 {
r.establishingController.QueueCRD(newCRD.Name, 5*time.Second)
} else {
r.establishingController.QueueCRD(newCRD.Name, 0)
}
}
storageMap := r.customStorage.Load().(crdStorageMap)
oldInfo, found := storageMap[newCRD.UID]
if !found {
return
}
if apiequality.Semantic.DeepEqual(&newCRD.Spec, oldInfo.spec) && apiequality.Semantic.DeepEqual(&newCRD.Status.AcceptedNames, oldInfo.acceptedNames) {
klog.V(6).Infof("Ignoring customresourcedefinition %s update because neither spec, nor accepted names changed", oldCRD.Name)
return
}
klog.V(4).Infof("Updating customresourcedefinition %s", oldCRD.Name)
// Copy because we cannot write to storageMap without a race
// as it is used without locking elsewhere.
storageMap2 := storageMap.clone()
if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok {
for _, storage := range oldInfo.storages {
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
storage.CustomResource.DestroyFunc()
}
delete(storageMap2, types.UID(oldCRD.UID))
}
r.customStorage.Store(storageMap2)
}
// removeDeadStorage removes REST storage that isn't being used
func (r *crdHandler) removeDeadStorage() {
allCustomResourceDefinitions, err := r.crdLister.List(labels.Everything())
if err != nil {
utilruntime.HandleError(err)
return
}
r.customStorageLock.Lock()
defer r.customStorageLock.Unlock()
storageMap := r.customStorage.Load().(crdStorageMap)
// Copy because we cannot write to storageMap without a race
// as it is used without locking elsewhere
storageMap2 := storageMap.clone()
for uid, s := range storageMap2 {
found := false
for _, crd := range allCustomResourceDefinitions {
if crd.UID == uid {
found = true
break
}
}
if !found {
klog.V(4).Infof("Removing dead CRD storage for %s/%s", s.spec.Group, s.spec.Names.Kind)
for _, storage := range s.storages {
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
storage.CustomResource.DestroyFunc()
}
delete(storageMap2, uid)
}
}
r.customStorage.Store(storageMap2)
}
// GetCustomResourceListerCollectionDeleter returns the ListerCollectionDeleter of
// the given crd.
func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions.CustomResourceDefinition) (finalizer.ListerCollectionDeleter, error) {
info, err := r.getOrCreateServingInfoFor(crd)
if err != nil {
return nil, err
}
return info.storages[info.storageVersion].CustomResource, nil
}
var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()
func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) {
storageMap := r.customStorage.Load().(crdStorageMap)
if ret, ok := storageMap[crd.UID]; ok {
return ret, nil
}
r.customStorageLock.Lock()
defer r.customStorageLock.Unlock()
storageMap = r.customStorage.Load().(crdStorageMap)
if ret, ok := storageMap[crd.UID]; ok {
return ret, nil
}
storageVersion, err := apiextensions.GetCRDStorageVersion(crd)
if err != nil {
return nil, err
}
// Scope/Storages per version.
requestScopes := map[string]handlers.RequestScope{}
storages := map[string]customresource.CustomResourceStorage{}
statusScopes := map[string]handlers.RequestScope{}
scaleScopes := map[string]handlers.RequestScope{}
for _, v := range crd.Spec.Versions {
safeConverter, unsafeConverter, err := r.converterFactory.NewConverter(crd)
if err != nil {
return nil, err
}
// In addition to Unstructured objects (Custom Resources), we also may sometimes need to
// decode unversioned Options objects, so we delegate to parameterScheme for such types.
parameterScheme := runtime.NewScheme()
parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name},
&metav1.ListOptions{},
&metav1.ExportOptions{},
&metav1.GetOptions{},
&metav1.DeleteOptions{},
)
parameterCodec := runtime.NewParameterCodec(parameterScheme)
kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.Kind}
typer := newUnstructuredObjectTyper(parameterScheme)
creator := unstructuredCreator{}
validationSchema, err := getSchemaForVersion(crd, v.Name)
if err != nil {
utilruntime.HandleError(err)
return nil, fmt.Errorf("the server could not properly serve the CR schema")
}
validator, _, err := apiservervalidation.NewSchemaValidator(validationSchema)
if err != nil {
return nil, err
}
var statusSpec *apiextensions.CustomResourceSubresourceStatus
var statusValidator *validate.SchemaValidator
subresources, err := getSubresourcesForVersion(crd, v.Name)
if err != nil {
utilruntime.HandleError(err)
return nil, fmt.Errorf("the server could not properly serve the CR subresources")
}
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && subresources != nil && subresources.Status != nil {
statusSpec = subresources.Status
// for the status subresource, validate only against the status schema
if validationSchema != nil && validationSchema.OpenAPIV3Schema != nil && validationSchema.OpenAPIV3Schema.Properties != nil {
if statusSchema, ok := validationSchema.OpenAPIV3Schema.Properties["status"]; ok {
openapiSchema := &spec.Schema{}
if err := apiservervalidation.ConvertJSONSchemaProps(&statusSchema, openapiSchema); err != nil {
return nil, err
}
statusValidator = validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)
}
}
}
var scaleSpec *apiextensions.CustomResourceSubresourceScale
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && subresources != nil && subresources.Scale != nil {
scaleSpec = subresources.Scale
}
columns, err := getColumnsForVersion(crd, v.Name)
if err != nil {
utilruntime.HandleError(err)
return nil, fmt.Errorf("the server could not properly serve the CR columns")
}
table, err := tableconvertor.New(columns)
if err != nil {
klog.V(2).Infof("The CRD for %v has an invalid printer specification, falling back to default printing: %v", kind, err)
}
storages[v.Name] = customresource.NewStorage(
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural},
schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.Kind},
schema.GroupVersionKind{Group: crd.Spec.Group, Version: v.Name, Kind: crd.Status.AcceptedNames.ListKind},
customresource.NewStrategy(
typer,
crd.Spec.Scope == apiextensions.NamespaceScoped,
kind,
validator,
statusValidator,
statusSpec,
scaleSpec,
),
crdConversionRESTOptionsGetter{
RESTOptionsGetter: r.restOptionsGetter,
converter: safeConverter,
decoderVersion: schema.GroupVersion{Group: crd.Spec.Group, Version: v.Name},
encoderVersion: schema.GroupVersion{Group: crd.Spec.Group, Version: storageVersion},
},
crd.Status.AcceptedNames.Categories,
table,
)
selfLinkPrefix := ""
switch crd.Spec.Scope {
case apiextensions.ClusterScoped:
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, v.Name) + "/" + crd.Status.AcceptedNames.Plural + "/"
case apiextensions.NamespaceScoped:
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, v.Name, "namespaces") + "/"
}
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
requestScopes[v.Name] = handlers.RequestScope{
Namer: handlers.ContextBasedNaming{
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
},
Serializer: unstructuredNegotiatedSerializer{typer: typer, creator: creator, converter: safeConverter},
ParameterCodec: parameterCodec,
Creater: creator,
Convertor: safeConverter,
Defaulter: unstructuredDefaulter{parameterScheme},
Typer: typer,
UnsafeConvertor: unsafeConverter,
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: v.Name, Resource: crd.Status.AcceptedNames.Plural},
Kind: kind,
// a handler for a specific group-version of a custom resource uses that version as the in-memory representation
HubGroupVersion: kind.GroupVersion(),
MetaGroupVersion: metav1.SchemeGroupVersion,
TableConvertor: storages[v.Name].CustomResource,
}
// override scaleSpec subresource values
// shallow copy
scaleScope := requestScopes[v.Name]
scaleConverter := scale.NewScaleConverter()
scaleScope.Subresource = "scale"
scaleScope.Serializer = serializer.NewCodecFactory(scaleConverter.Scheme())
scaleScope.Kind = autoscalingv1.SchemeGroupVersion.WithKind("Scale")
scaleScope.Namer = handlers.ContextBasedNaming{
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
SelfLinkPathSuffix: "/scale",
}
scaleScopes[v.Name] = scaleScope
// override status subresource values
// shallow copy
statusScope := requestScopes[v.Name]
statusScope.Subresource = "status"
statusScope.Namer = handlers.ContextBasedNaming{
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
SelfLinkPathSuffix: "/status",
}
statusScopes[v.Name] = statusScope
}
ret := &crdInfo{
spec: &crd.Spec,
acceptedNames: &crd.Status.AcceptedNames,
storages: storages,
requestScopes: requestScopes,
scaleRequestScopes: scaleScopes,
statusRequestScopes: statusScopes,
storageVersion: storageVersion,
}
// Copy because we cannot write to storageMap without a race
// as it is used without locking elsewhere.
storageMap2 := storageMap.clone()
storageMap2[crd.UID] = ret
r.customStorage.Store(storageMap2)
return ret, nil
}
type unstructuredNegotiatedSerializer struct {
typer runtime.ObjectTyper
creator runtime.ObjectCreater
converter runtime.ObjectConvertor
}
func (s unstructuredNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
return []runtime.SerializerInfo{
{
MediaType: "application/json",
EncodesAsText: true,
Serializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, false),
PrettySerializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, true),
StreamSerializer: &runtime.StreamSerializerInfo{
EncodesAsText: true,
Serializer: json.NewSerializer(json.DefaultMetaFactory, s.creator, s.typer, false),
Framer: json.Framer,
},
},
{
MediaType: "application/yaml",
EncodesAsText: true,
Serializer: json.NewYAMLSerializer(json.DefaultMetaFactory, s.creator, s.typer),
},
}
}
func (s unstructuredNegotiatedSerializer) EncoderForVersion(encoder runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
return versioning.NewCodec(encoder, nil, s.converter, Scheme, Scheme, Scheme, gv, nil, "crdNegotiatedSerializer")
}
func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
d := schemaCoercingDecoder{delegate: decoder, validator: unstructuredSchemaCoercer{}}
return versioning.NewDefaultingCodecForScheme(Scheme, nil, d, nil, gv)
}
type UnstructuredObjectTyper struct {
Delegate runtime.ObjectTyper
UnstructuredTyper runtime.ObjectTyper
}
func newUnstructuredObjectTyper(Delegate runtime.ObjectTyper) UnstructuredObjectTyper {
return UnstructuredObjectTyper{
Delegate: Delegate,
UnstructuredTyper: crdserverscheme.NewUnstructuredObjectTyper(),
}
}
func (t UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
// Delegate for things other than Unstructured.
if _, ok := obj.(runtime.Unstructured); !ok {
return t.Delegate.ObjectKinds(obj)
}
return t.UnstructuredTyper.ObjectKinds(obj)
}
func (t UnstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
return t.Delegate.Recognizes(gvk) || t.UnstructuredTyper.Recognizes(gvk)
}
type unstructuredCreator struct{}
func (c unstructuredCreator) New(kind schema.GroupVersionKind) (runtime.Object, error) {
ret := &unstructured.Unstructured{}
ret.SetGroupVersionKind(kind)
return ret, nil
}
type unstructuredDefaulter struct {
delegate runtime.ObjectDefaulter
}
func (d unstructuredDefaulter) Default(in runtime.Object) {
// Delegate for things other than Unstructured.
if _, ok := in.(runtime.Unstructured); !ok {
d.delegate.Default(in)
}
}
type CRDRESTOptionsGetter struct {
StorageConfig storagebackend.Config
StoragePrefix string
EnableWatchCache bool
DefaultWatchCacheSize int
EnableGarbageCollection bool
DeleteCollectionWorkers int
CountMetricPollPeriod time.Duration
}
func (t CRDRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
ret := generic.RESTOptions{
StorageConfig: &t.StorageConfig,
Decorator: generic.UndecoratedStorage,
EnableGarbageCollection: t.EnableGarbageCollection,
DeleteCollectionWorkers: t.DeleteCollectionWorkers,
ResourcePrefix: resource.Group + "/" + resource.Resource,
CountMetricPollPeriod: t.CountMetricPollPeriod,
}
if t.EnableWatchCache {
ret.Decorator = genericregistry.StorageWithCacher(t.DefaultWatchCacheSize)
}
return ret, nil
}
// clone returns a clone of the provided crdStorageMap.
// The clone is a shallow copy of the map.
func (in crdStorageMap) clone() crdStorageMap {
if in == nil {
return nil
}
out := make(crdStorageMap, len(in))
for key, value := range in {
out[key] = value
}
return out
}
// crdConversionRESTOptionsGetter overrides the codec with one using the
// provided custom converter and custom encoder and decoder version.
type crdConversionRESTOptionsGetter struct {
generic.RESTOptionsGetter
converter runtime.ObjectConvertor
encoderVersion schema.GroupVersion
decoderVersion schema.GroupVersion
}
func (t crdConversionRESTOptionsGetter) GetRESTOptions(resource schema.GroupResource) (generic.RESTOptions, error) {
ret, err := t.RESTOptionsGetter.GetRESTOptions(resource)
if err == nil {
d := schemaCoercingDecoder{delegate: ret.StorageConfig.Codec, validator: unstructuredSchemaCoercer{
// drop invalid fields while decoding old CRs (before we had any ObjectMeta validation)
dropInvalidMetadata: true,
}}
c := schemaCoercingConverter{delegate: t.converter, validator: unstructuredSchemaCoercer{}}
ret.StorageConfig.Codec = versioning.NewCodec(
ret.StorageConfig.Codec,
d,
c,
&unstructuredCreator{},
crdserverscheme.NewUnstructuredObjectTyper(),
&unstructuredDefaulter{delegate: Scheme},
t.encoderVersion,
t.decoderVersion,
"crdRESTOptions",
)
}
return ret, err
}
// schemaCoercingDecoder calls the delegate decoder, and then applies the Unstructured schema validator
// to coerce the schema.
type schemaCoercingDecoder struct {
delegate runtime.Decoder
validator unstructuredSchemaCoercer
}
var _ runtime.Decoder = schemaCoercingDecoder{}
func (d schemaCoercingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
obj, gvk, err := d.delegate.Decode(data, defaults, into)
if err != nil {
return nil, gvk, err
}
if u, ok := obj.(*unstructured.Unstructured); ok {
if err := d.validator.apply(u); err != nil {
return nil, gvk, err
}
}
return obj, gvk, nil
}
// schemaCoercingConverter calls the delegate converter and applies the Unstructured validator to
// coerce the schema.
type schemaCoercingConverter struct {
delegate runtime.ObjectConvertor
validator unstructuredSchemaCoercer
}
var _ runtime.ObjectConvertor = schemaCoercingConverter{}
func (v schemaCoercingConverter) Convert(in, out, context interface{}) error {
if err := v.delegate.Convert(in, out, context); err != nil {
return err
}
if u, ok := out.(*unstructured.Unstructured); ok {
if err := v.validator.apply(u); err != nil {
return err
}
}
return nil
}
func (v schemaCoercingConverter) ConvertToVersion(in runtime.Object, gv runtime.GroupVersioner) (runtime.Object, error) {
out, err := v.delegate.ConvertToVersion(in, gv)
if err != nil {
return nil, err
}
if u, ok := out.(*unstructured.Unstructured); ok {
if err := v.validator.apply(u); err != nil {
return nil, err
}
}
return out, nil
}
func (v schemaCoercingConverter) ConvertFieldLabel(gvk schema.GroupVersionKind, label, value string) (string, string, error) {
return v.delegate.ConvertFieldLabel(gvk, label, value)
}
// unstructuredSchemaCoercer does the validation for Unstructured that json.Unmarshal
// does for native types. This includes:
// - validating and pruning ObjectMeta (here with optional error instead of pruning)
// - TODO: application of an OpenAPI validator (against the whole object or a top-level field of it).
// - TODO: optionally application of post-validation algorithms like defaulting and/or OpenAPI based pruning.
type unstructuredSchemaCoercer struct {
dropInvalidMetadata bool
}
func (v *unstructuredSchemaCoercer) apply(u *unstructured.Unstructured) error {
// save implicit meta fields that don't have to be specified in the validation spec
kind, foundKind, err := unstructured.NestedString(u.UnstructuredContent(), "kind")
if err != nil {
return err
}
apiVersion, foundApiVersion, err := unstructured.NestedString(u.UnstructuredContent(), "apiVersion")
if err != nil {
return err
}
objectMeta, foundObjectMeta, err := getObjectMeta(u, v.dropInvalidMetadata)
if err != nil {
return err
}
// restore meta fields, starting clean
if foundKind {
u.SetKind(kind)
}
if foundApiVersion {
u.SetAPIVersion(apiVersion)
}
if foundObjectMeta {
if err := setObjectMeta(u, objectMeta); err != nil {
return err
}
}
return nil
}
var encodingjson = json.CaseSensitiveJsonIterator()
func getObjectMeta(u *unstructured.Unstructured, dropMalformedFields bool) (*metav1.ObjectMeta, bool, error) {
metadata, found := u.UnstructuredContent()["metadata"]
if !found {
return nil, false, nil
}
// round-trip through JSON first, hoping that unmarshaling just works
objectMeta := &metav1.ObjectMeta{}
metadataBytes, err := encodingjson.Marshal(metadata)
if err != nil {
return nil, false, err
}
if err = encodingjson.Unmarshal(metadataBytes, objectMeta); err == nil {
// if successful, return
return objectMeta, true, nil
}
if !dropMalformedFields {
// if we're not trying to drop malformed fields, return the error
return nil, true, err
}
metadataMap, ok := metadata.(map[string]interface{})
if !ok {
return nil, false, fmt.Errorf("invalid metadata: expected object, got %T", metadata)
}
// Go field by field accumulating into the metadata object.
// This takes advantage of the fact that you can repeatedly unmarshal individual fields into a single struct,
// each iteration preserving the old key-values.
accumulatedObjectMeta := &metav1.ObjectMeta{}
testObjectMeta := &metav1.ObjectMeta{}
for k, v := range metadataMap {
// serialize a single field
if singleFieldBytes, err := encodingjson.Marshal(map[string]interface{}{k: v}); err == nil {
// do a test unmarshal
if encodingjson.Unmarshal(singleFieldBytes, testObjectMeta) == nil {
// if that succeeds, unmarshal for real
encodingjson.Unmarshal(singleFieldBytes, accumulatedObjectMeta)
}
}
}
return accumulatedObjectMeta, true, nil
}
func setObjectMeta(u *unstructured.Unstructured, objectMeta *metav1.ObjectMeta) error {
if objectMeta == nil {
unstructured.RemoveNestedField(u.UnstructuredContent(), "metadata")
return nil
}
metadata, err := runtime.DefaultUnstructuredConverter.ToUnstructured(objectMeta)
if err != nil {
return err
}
u.UnstructuredContent()["metadata"] = metadata
return nil
}

View File

@@ -1,319 +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 apiserver
import (
"math/rand"
"reflect"
"testing"
corev1 "k8s.io/api/core/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
"k8s.io/apimachinery/pkg/api/equality"
metafuzzer "k8s.io/apimachinery/pkg/apis/meta/fuzzer"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/diff"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
)
func TestConvertFieldLabel(t *testing.T) {
tests := []struct {
name string
clusterScoped bool
label string
expectError bool
}{
{
name: "cluster scoped - name is ok",
clusterScoped: true,
label: "metadata.name",
},
{
name: "cluster scoped - namespace is not ok",
clusterScoped: true,
label: "metadata.namespace",
expectError: true,
},
{
name: "cluster scoped - other field is not ok",
clusterScoped: true,
label: "some.other.field",
expectError: true,
},
{
name: "namespace scoped - name is ok",
label: "metadata.name",
},
{
name: "namespace scoped - namespace is ok",
label: "metadata.namespace",
},
{
name: "namespace scoped - other field is not ok",
label: "some.other.field",
expectError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
crd := apiextensions.CustomResourceDefinition{
Spec: apiextensions.CustomResourceDefinitionSpec{
Conversion: &apiextensions.CustomResourceConversion{
Strategy: "None",
},
},
}
if test.clusterScoped {
crd.Spec.Scope = apiextensions.ClusterScoped
} else {
crd.Spec.Scope = apiextensions.NamespaceScoped
}
f, err := conversion.NewCRConverterFactory(nil, nil)
if err != nil {
t.Fatal(err)
}
_, c, err := f.NewConverter(&crd)
label, value, err := c.ConvertFieldLabel(schema.GroupVersionKind{}, test.label, "value")
if e, a := test.expectError, err != nil; e != a {
t.Fatalf("err: expected %t, got %t", e, a)
}
if test.expectError {
if e, a := "field label not supported: "+test.label, err.Error(); e != a {
t.Errorf("err: expected %s, got %s", e, a)
}
return
}
if e, a := test.label, label; e != a {
t.Errorf("label: expected %s, got %s", e, a)
}
if e, a := "value", value; e != a {
t.Errorf("value: expected %s, got %s", e, a)
}
})
}
}
func TestRoundtripObjectMeta(t *testing.T) {
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
codec := json.NewSerializer(json.DefaultMetaFactory, scheme, scheme, false)
seed := rand.Int63()
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(seed), codecs)
N := 1000
for i := 0; i < N; i++ {
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
original := &metav1.ObjectMeta{}
fuzzer.Fuzz(original)
if err := setObjectMeta(u, original); err != nil {
t.Fatalf("unexpected error setting ObjectMeta: %v", err)
}
o, _, err := getObjectMeta(u, false)
if err != nil {
t.Fatalf("unexpected error getting the Objectmeta: %v", err)
}
if !equality.Semantic.DeepEqual(original, o) {
t.Errorf("diff: %v\nCodec: %#v", diff.ObjectReflectDiff(original, o), codec)
}
}
}
// TestMalformedObjectMetaFields sets a number of different random values and types for all
// metadata fields. If json.Unmarshal accepts them, compare that getObjectMeta
// gives the same result. Otherwise, drop malformed fields.
func TestMalformedObjectMetaFields(t *testing.T) {
fuzzer := fuzzer.FuzzerFor(metafuzzer.Funcs, rand.NewSource(rand.Int63()), serializer.NewCodecFactory(runtime.NewScheme()))
spuriousValues := func() []interface{} {
return []interface{}{
// primitives
nil,
int64(1),
float64(1.5),
true,
"a",
// well-formed complex values
[]interface{}{"a", "b"},
map[string]interface{}{"a": "1", "b": "2"},
[]interface{}{int64(1), int64(2)},
[]interface{}{float64(1.5), float64(2.5)},
// known things json decoding tolerates
map[string]interface{}{"a": "1", "b": nil},
// malformed things
map[string]interface{}{"a": "1", "b": []interface{}{"nested"}},
[]interface{}{"a", int64(1), float64(1.5), true, []interface{}{"nested"}},
}
}
N := 100
for i := 0; i < N; i++ {
fuzzedObjectMeta := &metav1.ObjectMeta{}
fuzzer.Fuzz(fuzzedObjectMeta)
goodMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
for _, pth := range jsonPaths(nil, goodMetaMap) {
for _, v := range spuriousValues() {
// skip values of same type, because they can only cause decoding errors further insides
orig, err := JsonPathValue(goodMetaMap, pth, 0)
if err != nil {
t.Fatalf("unexpected to not find something at %v: %v", pth, err)
}
if reflect.TypeOf(v) == reflect.TypeOf(orig) {
continue
}
// make a spurious map
spuriousMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
if err := SetJsonPath(spuriousMetaMap, pth, 0, v); err != nil {
t.Fatal(err)
}
// See if it can unmarshal to object meta
spuriousJSON, err := encodingjson.Marshal(spuriousMetaMap)
if err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
expectedObjectMeta := &metav1.ObjectMeta{}
if err := encodingjson.Unmarshal(spuriousJSON, expectedObjectMeta); err != nil {
// if standard json unmarshal would fail decoding this field, drop the field entirely
truncatedMetaMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(fuzzedObjectMeta.DeepCopy())
if err != nil {
t.Fatal(err)
}
// we expect this logic for the different fields:
switch {
default:
// delete complete top-level field by default
DeleteJsonPath(truncatedMetaMap, pth[:1], 0)
}
truncatedJSON, err := encodingjson.Marshal(truncatedMetaMap)
if err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
expectedObjectMeta = &metav1.ObjectMeta{}
if err := encodingjson.Unmarshal(truncatedJSON, expectedObjectMeta); err != nil {
t.Fatalf("error on %v=%#v: %v", pth, v, err)
}
}
// make sure dropInvalidTypedFields+getObjectMeta matches what we expect
u := &unstructured.Unstructured{Object: map[string]interface{}{"metadata": spuriousMetaMap}}
actualObjectMeta, _, err := getObjectMeta(u, true)
if err != nil {
t.Errorf("got unexpected error after dropping invalid typed fields on %v=%#v: %v", pth, v, err)
continue
}
if !equality.Semantic.DeepEqual(expectedObjectMeta, actualObjectMeta) {
t.Errorf("%v=%#v, diff: %v\n", pth, v, diff.ObjectReflectDiff(expectedObjectMeta, actualObjectMeta))
t.Errorf("expectedObjectMeta %#v", expectedObjectMeta)
}
}
}
}
}
func TestGetObjectMetaNils(t *testing.T) {
u := &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "Pod",
"apiVersion": "v1",
"metadata": map[string]interface{}{
"generateName": nil,
"labels": map[string]interface{}{
"foo": nil,
},
},
},
}
o, _, err := getObjectMeta(u, true)
if err != nil {
t.Fatal(err)
}
if o.GenerateName != "" {
t.Errorf("expected null json value to be read as \"\" string, but got: %q", o.GenerateName)
}
if got, expected := o.Labels, map[string]string{"foo": ""}; !reflect.DeepEqual(got, expected) {
t.Errorf("unexpected labels, expected=%#v, got=%#v", expected, got)
}
// double check this what the kube JSON decode is doing
bs, _ := encodingjson.Marshal(u.UnstructuredContent())
kubeObj, _, err := clientgoscheme.Codecs.UniversalDecoder(corev1.SchemeGroupVersion).Decode(bs, nil, nil)
if err != nil {
t.Fatal(err)
}
pod, ok := kubeObj.(*corev1.Pod)
if !ok {
t.Fatalf("expected v1 Pod, got: %T", pod)
}
if got, expected := o.GenerateName, pod.ObjectMeta.GenerateName; got != expected {
t.Errorf("expected generatedName to be %q, got %q", expected, got)
}
if got, expected := o.Labels, pod.ObjectMeta.Labels; !reflect.DeepEqual(got, expected) {
t.Errorf("expected labels to be %v, got %v", expected, got)
}
}
func TestGetObjectMeta(t *testing.T) {
for i := 0; i < 100; i++ {
u := &unstructured.Unstructured{Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "good",
"Name": "bad1",
"nAme": "bad2",
"naMe": "bad3",
"namE": "bad4",
"namespace": "good",
"Namespace": "bad1",
"nAmespace": "bad2",
"naMespace": "bad3",
"namEspace": "bad4",
"creationTimestamp": "a",
},
}}
meta, _, err := getObjectMeta(u, true)
if err != nil {
t.Fatal(err)
}
if meta.Name != "good" || meta.Namespace != "good" {
t.Fatalf("got %#v", meta)
}
}
}

View File

@@ -1,117 +0,0 @@
/*
Copyright 2018 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 apiserver
import (
"fmt"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)
// getSchemaForVersion returns the validation schema for given version in given CRD.
func getSchemaForVersion(crd *apiextensions.CustomResourceDefinition, version string) (*apiextensions.CustomResourceValidation, error) {
if !hasPerVersionSchema(crd.Spec.Versions) {
return crd.Spec.Validation, nil
}
if crd.Spec.Validation != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version schemas must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return v.Schema, nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// getSubresourcesForVersion returns the subresources for given version in given CRD.
func getSubresourcesForVersion(crd *apiextensions.CustomResourceDefinition, version string) (*apiextensions.CustomResourceSubresources, error) {
if !hasPerVersionSubresources(crd.Spec.Versions) {
return crd.Spec.Subresources, nil
}
if crd.Spec.Subresources != nil {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version subresources must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return v.Subresources, nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// getColumnsForVersion returns the columns for given version in given CRD.
// NOTE: the newly logically-defaulted columns is not pointing to the original CRD object.
// One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through
// the original CRD object instead.
func getColumnsForVersion(crd *apiextensions.CustomResourceDefinition, version string) ([]apiextensions.CustomResourceColumnDefinition, error) {
if !hasPerVersionColumns(crd.Spec.Versions) {
return serveDefaultColumnsIfEmpty(crd.Spec.AdditionalPrinterColumns), nil
}
if len(crd.Spec.AdditionalPrinterColumns) > 0 {
return nil, fmt.Errorf("malformed CustomResourceDefinition %s version %s: top-level and per-version additionalPrinterColumns must be mutual exclusive", crd.Name, version)
}
for _, v := range crd.Spec.Versions {
if version == v.Name {
return serveDefaultColumnsIfEmpty(v.AdditionalPrinterColumns), nil
}
}
return nil, fmt.Errorf("version %s not found in CustomResourceDefinition: %v", version, crd.Name)
}
// serveDefaultColumnsIfEmpty applies logically defaulting to columns, if the input columns is empty.
// NOTE: in this way, the newly logically-defaulted columns is not pointing to the original CRD object.
// One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through
// the original CRD object instead.
func serveDefaultColumnsIfEmpty(columns []apiextensions.CustomResourceColumnDefinition) []apiextensions.CustomResourceColumnDefinition {
if len(columns) > 0 {
return columns
}
return []apiextensions.CustomResourceColumnDefinition{
{Name: "Age", Type: "date", Description: swaggerMetadataDescriptions["creationTimestamp"], JSONPath: ".metadata.creationTimestamp"},
}
}
// 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
}

View File

@@ -1,235 +0,0 @@
/*
Copyright 2018 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 apiserver
import (
"bytes"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
)
type (
jsonPathNode struct {
index *int
field string
}
JsonPath []jsonPathNode
)
func (p JsonPath) String() string {
var buf bytes.Buffer
for _, n := range p {
if n.index == nil {
buf.WriteString("." + n.field)
} else {
buf.WriteString(fmt.Sprintf("[%d]", *n.index))
}
}
return buf.String()
}
func jsonPaths(base JsonPath, j map[string]interface{}) []JsonPath {
res := make([]JsonPath, 0, len(j))
for k, old := range j {
kPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{field: k})
res = append(res, kPth)
switch old := old.(type) {
case map[string]interface{}:
res = append(res, jsonPaths(kPth, old)...)
case []interface{}:
res = append(res, jsonIterSlice(kPth, old)...)
}
}
return res
}
func jsonIterSlice(base JsonPath, j []interface{}) []JsonPath {
res := make([]JsonPath, 0, len(j))
for i, old := range j {
index := i
iPth := append(append([]jsonPathNode(nil), base...), jsonPathNode{index: &index})
res = append(res, iPth)
switch old := old.(type) {
case map[string]interface{}:
res = append(res, jsonPaths(iPth, old)...)
case []interface{}:
res = append(res, jsonIterSlice(iPth, old)...)
}
}
return res
}
func JsonPathValue(j map[string]interface{}, pth JsonPath, base int) (interface{}, error) {
if len(pth) == base {
return nil, fmt.Errorf("empty json path is invalid for object")
}
if pth[base].index != nil {
return nil, fmt.Errorf("index json path is invalid for object")
}
field, ok := j[pth[base].field]
if !ok || len(pth) == base+1 {
if len(pth) > base+1 {
return nil, fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
}
return j[pth[base].field], nil
}
switch field := field.(type) {
case map[string]interface{}:
return JsonPathValue(field, pth, base+1)
case []interface{}:
return jsonPathValueSlice(field, pth, base+1)
default:
return nil, fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
}
}
func jsonPathValueSlice(j []interface{}, pth JsonPath, base int) (interface{}, error) {
if len(pth) == base {
return nil, fmt.Errorf("empty json path %q is invalid for object", pth)
}
if pth[base].index == nil {
return nil, fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
}
if *pth[base].index >= len(j) {
return nil, fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
}
if len(pth) == base+1 {
return j[*pth[base].index], nil
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return JsonPathValue(item, pth, base+1)
case []interface{}:
return jsonPathValueSlice(item, pth, base+1)
default:
return nil, fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
}
}
func SetJsonPath(j map[string]interface{}, pth JsonPath, base int, value interface{}) error {
if len(pth) == base {
return fmt.Errorf("empty json path is invalid for object")
}
if pth[base].index != nil {
return fmt.Errorf("index json path is invalid for object")
}
field, ok := j[pth[base].field]
if !ok || len(pth) == base+1 {
if len(pth) > base+1 {
return fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
}
j[pth[base].field] = runtime.DeepCopyJSONValue(value)
return nil
}
switch field := field.(type) {
case map[string]interface{}:
return SetJsonPath(field, pth, base+1, value)
case []interface{}:
return setJsonPathSlice(field, pth, base+1, value)
default:
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
}
}
func setJsonPathSlice(j []interface{}, pth JsonPath, base int, value interface{}) error {
if len(pth) == base {
return fmt.Errorf("empty json path %q is invalid for object", pth)
}
if pth[base].index == nil {
return fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
}
if *pth[base].index >= len(j) {
return fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
}
if len(pth) == base+1 {
j[*pth[base].index] = runtime.DeepCopyJSONValue(value)
return nil
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return SetJsonPath(item, pth, base+1, value)
case []interface{}:
return setJsonPathSlice(item, pth, base+1, value)
default:
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
}
}
func DeleteJsonPath(j map[string]interface{}, pth JsonPath, base int) error {
if len(pth) == base {
return fmt.Errorf("empty json path is invalid for object")
}
if pth[base].index != nil {
return fmt.Errorf("index json path is invalid for object")
}
field, ok := j[pth[base].field]
if !ok || len(pth) == base+1 {
if len(pth) > base+1 {
return fmt.Errorf("invalid non-terminal json path %q for non-existing field", pth)
}
delete(j, pth[base].field)
return nil
}
switch field := field.(type) {
case map[string]interface{}:
return DeleteJsonPath(field, pth, base+1)
case []interface{}:
if len(pth) == base+2 {
if pth[base+1].index == nil {
return fmt.Errorf("field json path %q is invalid for object", pth)
}
j[pth[base].field] = append(field[:*pth[base+1].index], field[*pth[base+1].index+1:]...)
return nil
}
return deleteJsonPathSlice(field, pth, base+1)
default:
return fmt.Errorf("invalid non-terminal json path %q for field", pth[:base+1])
}
}
func deleteJsonPathSlice(j []interface{}, pth JsonPath, base int) error {
if len(pth) == base {
return fmt.Errorf("empty json path %q is invalid for object", pth)
}
if pth[base].index == nil {
return fmt.Errorf("field json path %q is invalid for object", pth[:base+1])
}
if *pth[base].index >= len(j) {
return fmt.Errorf("invalid index %q for array of size %d", pth[:base+1], len(j))
}
if len(pth) == base+1 {
return fmt.Errorf("cannot delete item at index %q in-place", pth[:base])
}
switch item := j[*pth[base].index].(type) {
case map[string]interface{}:
return DeleteJsonPath(item, pth, base+1)
case []interface{}:
if len(pth) == base+2 {
if pth[base+1].index == nil {
return fmt.Errorf("field json path %q is invalid for object", pth)
}
j[*pth[base].index] = append(item[:*pth[base+1].index], item[*pth[base+1].index+1:])
return nil
}
return deleteJsonPathSlice(item, pth, base+1)
default:
return fmt.Errorf("invalid non-terminal json path %q for index", pth[:base+1])
}
}

View File

@@ -1,249 +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 (
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)
// NewSchemaValidator creates an openapi schema validator for the given CRD validation.
func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceValidation) (*validate.SchemaValidator, *spec.Schema, error) {
// Convert CRD schema to openapi schema
openapiSchema := &spec.Schema{}
if customResourceValidation != nil {
if err := ConvertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
return nil, nil, err
}
}
return validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default), openapiSchema, nil
}
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
// CustomResource is a JSON data structure.
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
if validator == nil {
return nil
}
result := validator.Validate(customResource)
if result.AsError() != nil {
return result.AsError()
}
return nil
}
// ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema
func ConvertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
if in == nil {
return nil
}
out.ID = in.ID
out.Schema = spec.SchemaURL(in.Schema)
out.Description = in.Description
if in.Type != "" {
out.Type = spec.StringOrArray([]string{in.Type})
}
out.Format = in.Format
out.Title = in.Title
out.Maximum = in.Maximum
out.ExclusiveMaximum = in.ExclusiveMaximum
out.Minimum = in.Minimum
out.ExclusiveMinimum = in.ExclusiveMinimum
out.MaxLength = in.MaxLength
out.MinLength = in.MinLength
out.Pattern = in.Pattern
out.MaxItems = in.MaxItems
out.MinItems = in.MinItems
out.UniqueItems = in.UniqueItems
out.MultipleOf = in.MultipleOf
out.MaxProperties = in.MaxProperties
out.MinProperties = in.MinProperties
out.Required = in.Required
if in.Default != nil {
out.Default = *(in.Default)
}
if in.Example != nil {
out.Example = *(in.Example)
}
out.Enum = make([]interface{}, len(in.Enum))
for k, v := range in.Enum {
out.Enum[k] = v
}
if err := convertSliceOfJSONSchemaProps(&in.AllOf, &out.AllOf); err != nil {
return err
}
if err := convertSliceOfJSONSchemaProps(&in.OneOf, &out.OneOf); err != nil {
return err
}
if err := convertSliceOfJSONSchemaProps(&in.AnyOf, &out.AnyOf); err != nil {
return err
}
if in.Not != nil {
in, out := &in.Not, &out.Not
*out = new(spec.Schema)
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
var err error
out.Properties, err = convertMapOfJSONSchemaProps(in.Properties)
if err != nil {
return err
}
out.PatternProperties, err = convertMapOfJSONSchemaProps(in.PatternProperties)
if err != nil {
return err
}
out.Definitions, err = convertMapOfJSONSchemaProps(in.Definitions)
if err != nil {
return err
}
if in.Ref != nil {
out.Ref, err = spec.NewRef(*in.Ref)
if err != nil {
return err
}
}
if in.AdditionalProperties != nil {
in, out := &in.AdditionalProperties, &out.AdditionalProperties
*out = new(spec.SchemaOrBool)
if err := convertJSONSchemaPropsorBool(*in, *out); err != nil {
return err
}
}
if in.AdditionalItems != nil {
in, out := &in.AdditionalItems, &out.AdditionalItems
*out = new(spec.SchemaOrBool)
if err := convertJSONSchemaPropsorBool(*in, *out); err != nil {
return err
}
}
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = new(spec.SchemaOrArray)
if err := convertJSONSchemaPropsOrArray(*in, *out); err != nil {
return err
}
}
if in.Dependencies != nil {
in, out := &in.Dependencies, &out.Dependencies
*out = make(spec.Dependencies, len(*in))
for key, val := range *in {
newVal := new(spec.SchemaOrStringArray)
if err := convertJSONSchemaPropsOrStringArray(&val, newVal); err != nil {
return err
}
(*out)[key] = *newVal
}
}
if in.ExternalDocs != nil {
out.ExternalDocs = &spec.ExternalDocumentation{}
out.ExternalDocs.Description = in.ExternalDocs.Description
out.ExternalDocs.URL = in.ExternalDocs.URL
}
return nil
}
func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]spec.Schema) error {
if in != nil {
for _, jsonSchemaProps := range *in {
schema := spec.Schema{}
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return err
}
*out = append(*out, schema)
}
}
return nil
}
func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (map[string]spec.Schema, error) {
out := make(map[string]spec.Schema)
if len(in) != 0 {
for k, jsonSchemaProps := range in {
schema := spec.Schema{}
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return nil, err
}
out[k] = schema
}
}
return out, nil
}
func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out *spec.SchemaOrArray) error {
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
if in.JSONSchemas != nil {
in, out := &in.JSONSchemas, &out.Schemas
*out = make([]spec.Schema, len(*in))
for i := range *in {
if err := ConvertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
return err
}
}
}
return nil
}
func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *spec.SchemaOrBool) error {
out.Allows = in.Allows
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
return nil
}
func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStringArray, out *spec.SchemaOrStringArray) error {
out.Property = in.Property
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(spec.Schema)
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err
}
}
return nil
}

View File

@@ -1,87 +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 (
"math/rand"
"testing"
"github.com/go-openapi/spec"
"k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsfuzzer "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/fuzzer"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
)
// TestRoundTrip checks the conversion to go-openapi types.
// internal -> go-openapi -> JSON -> external -> internal
func TestRoundTrip(t *testing.T) {
scheme := runtime.NewScheme()
codecs := serializer.NewCodecFactory(scheme)
// add internal and external types to scheme
if err := apiextensions.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
if err := apiextensionsv1beta1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
seed := rand.Int63()
fuzzerFuncs := fuzzer.MergeFuzzerFuncs(apiextensionsfuzzer.Funcs)
f := fuzzer.FuzzerFor(fuzzerFuncs, rand.NewSource(seed), codecs)
for i := 0; i < 20; i++ {
// fuzz internal types
internal := &apiextensions.JSONSchemaProps{}
f.Fuzz(internal)
// internal -> go-openapi
openAPITypes := &spec.Schema{}
if err := ConvertJSONSchemaProps(internal, openAPITypes); err != nil {
t.Fatal(err)
}
// go-openapi -> JSON
openAPIJSON, err := json.Marshal(openAPITypes)
if err != nil {
t.Fatal(err)
}
// JSON -> external
external := &apiextensionsv1beta1.JSONSchemaProps{}
if err := json.Unmarshal(openAPIJSON, external); err != nil {
t.Fatal(err)
}
// external -> internal
internalRoundTripped := &apiextensions.JSONSchemaProps{}
if err := scheme.Convert(external, internalRoundTripped, nil); err != nil {
t.Fatal(err)
}
if !apiequality.Semantic.DeepEqual(internal, internalRoundTripped) {
t.Fatalf("expected\n\t%#v, got \n\t%#v", internal, internalRoundTripped)
}
}
}

View File

@@ -1,82 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
fakeapiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1/fake"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
fakediscovery "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/testing"
)
// NewSimpleClientset returns a clientset that will respond with the provided objects.
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {
if err := o.Add(obj); err != nil {
panic(err)
}
}
cs := &Clientset{}
cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake}
cs.AddReactor("*", "*", testing.ObjectReaction(o))
cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
gvr := action.GetResource()
ns := action.GetNamespace()
watch, err := o.Watch(gvr, ns)
if err != nil {
return false, nil, err
}
return true, watch, nil
})
return cs
}
// Clientset implements clientset.Interface. Meant to be embedded into a
// struct to get a default implementation. This makes faking out just the method
// you want to test easier.
type Clientset struct {
testing.Fake
discovery *fakediscovery.FakeDiscovery
}
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
return c.discovery
}
var _ clientset.Interface = &Clientset{}
// ApiextensionsV1beta1 retrieves the ApiextensionsV1beta1Client
func (c *Clientset) ApiextensionsV1beta1() apiextensionsv1beta1.ApiextensionsV1beta1Interface {
return &fakeapiextensionsv1beta1.FakeApiextensionsV1beta1{Fake: &c.Fake}
}
// Apiextensions retrieves the ApiextensionsV1beta1Client
func (c *Clientset) Apiextensions() apiextensionsv1beta1.ApiextensionsV1beta1Interface {
return &fakeapiextensionsv1beta1.FakeApiextensionsV1beta1{Fake: &c.Fake}
}

View File

@@ -1,20 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated fake clientset.
package fake

View File

@@ -1,56 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
var parameterCodec = runtime.NewParameterCodec(scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
apiextensionsv1beta1.AddToScheme,
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
var AddToScheme = localSchemeBuilder.AddToScheme
func init() {
v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
utilruntime.Must(AddToScheme(scheme))
}

View File

@@ -1,20 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
// Package fake has the automatically generated clients.
package fake

View File

@@ -1,40 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1beta1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeApiextensionsV1beta1 struct {
*testing.Fake
}
func (c *FakeApiextensionsV1beta1) CustomResourceDefinitions() v1beta1.CustomResourceDefinitionInterface {
return &FakeCustomResourceDefinitions{c}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeApiextensionsV1beta1) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}

View File

@@ -1,131 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
)
// FakeCustomResourceDefinitions implements CustomResourceDefinitionInterface
type FakeCustomResourceDefinitions struct {
Fake *FakeApiextensionsV1beta1
}
var customresourcedefinitionsResource = schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1beta1", Resource: "customresourcedefinitions"}
var customresourcedefinitionsKind = schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1beta1", Kind: "CustomResourceDefinition"}
// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any.
func (c *FakeCustomResourceDefinitions) Get(name string, options v1.GetOptions) (result *v1beta1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootGetAction(customresourcedefinitionsResource, name), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}
// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors.
func (c *FakeCustomResourceDefinitions) List(opts v1.ListOptions) (result *v1beta1.CustomResourceDefinitionList, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootListAction(customresourcedefinitionsResource, customresourcedefinitionsKind, opts), &v1beta1.CustomResourceDefinitionList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v1beta1.CustomResourceDefinitionList{ListMeta: obj.(*v1beta1.CustomResourceDefinitionList).ListMeta}
for _, item := range obj.(*v1beta1.CustomResourceDefinitionList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested customResourceDefinitions.
func (c *FakeCustomResourceDefinitions) Watch(opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewRootWatchAction(customresourcedefinitionsResource, opts))
}
// Create takes the representation of a customResourceDefinition and creates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *FakeCustomResourceDefinitions) Create(customResourceDefinition *v1beta1.CustomResourceDefinition) (result *v1beta1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootCreateAction(customresourcedefinitionsResource, customResourceDefinition), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}
// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *FakeCustomResourceDefinitions) Update(customResourceDefinition *v1beta1.CustomResourceDefinition) (result *v1beta1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateAction(customresourcedefinitionsResource, customResourceDefinition), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *FakeCustomResourceDefinitions) UpdateStatus(customResourceDefinition *v1beta1.CustomResourceDefinition) (*v1beta1.CustomResourceDefinition, error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateSubresourceAction(customresourcedefinitionsResource, "status", customResourceDefinition), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}
// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs.
func (c *FakeCustomResourceDefinitions) Delete(name string, options *v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewRootDeleteAction(customresourcedefinitionsResource, name), &v1beta1.CustomResourceDefinition{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeCustomResourceDefinitions) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
action := testing.NewRootDeleteCollectionAction(customresourcedefinitionsResource, listOptions)
_, err := c.Fake.Invokes(action, &v1beta1.CustomResourceDefinitionList{})
return err
}
// Patch applies the patch and returns the patched customResourceDefinition.
func (c *FakeCustomResourceDefinitions) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1beta1.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, name, pt, data, subresources...), &v1beta1.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*v1beta1.CustomResourceDefinition), err
}

View File

@@ -1,90 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package internalclientset
import (
apiextensionsinternalversion "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion"
discovery "k8s.io/client-go/discovery"
rest "k8s.io/client-go/rest"
flowcontrol "k8s.io/client-go/util/flowcontrol"
)
type Interface interface {
Discovery() discovery.DiscoveryInterface
Apiextensions() apiextensionsinternalversion.ApiextensionsInterface
}
// Clientset contains the clients for groups. Each group has exactly one
// version included in a Clientset.
type Clientset struct {
*discovery.DiscoveryClient
apiextensions *apiextensionsinternalversion.ApiextensionsClient
}
// Apiextensions retrieves the ApiextensionsClient
func (c *Clientset) Apiextensions() apiextensionsinternalversion.ApiextensionsInterface {
return c.apiextensions
}
// Discovery retrieves the DiscoveryClient
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
if c == nil {
return nil
}
return c.DiscoveryClient
}
// NewForConfig creates a new Clientset for the given config.
func NewForConfig(c *rest.Config) (*Clientset, error) {
configShallowCopy := *c
if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
}
var cs Clientset
var err error
cs.apiextensions, err = apiextensionsinternalversion.NewForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
return &cs, nil
}
// NewForConfigOrDie creates a new Clientset for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *Clientset {
var cs Clientset
cs.apiextensions = apiextensionsinternalversion.NewForConfigOrDie(c)
cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
return &cs
}
// New creates a new Clientset for the given RESTClient.
func New(c rest.Interface) *Clientset {
var cs Clientset
cs.apiextensions = apiextensionsinternalversion.New(c)
cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
return &cs
}

View File

@@ -1,20 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated clientset.
package internalclientset

View File

@@ -1,77 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
apiextensionsinternalversion "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion"
fakeapiextensionsinternalversion "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion/fake"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
fakediscovery "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/testing"
)
// NewSimpleClientset returns a clientset that will respond with the provided objects.
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {
if err := o.Add(obj); err != nil {
panic(err)
}
}
cs := &Clientset{}
cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake}
cs.AddReactor("*", "*", testing.ObjectReaction(o))
cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
gvr := action.GetResource()
ns := action.GetNamespace()
watch, err := o.Watch(gvr, ns)
if err != nil {
return false, nil, err
}
return true, watch, nil
})
return cs
}
// Clientset implements clientset.Interface. Meant to be embedded into a
// struct to get a default implementation. This makes faking out just the method
// you want to test easier.
type Clientset struct {
testing.Fake
discovery *fakediscovery.FakeDiscovery
}
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
return c.discovery
}
var _ clientset.Interface = &Clientset{}
// Apiextensions retrieves the ApiextensionsClient
func (c *Clientset) Apiextensions() apiextensionsinternalversion.ApiextensionsInterface {
return &fakeapiextensionsinternalversion.FakeApiextensions{Fake: &c.Fake}
}

View File

@@ -1,20 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated fake clientset.
package fake

View File

@@ -1,56 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
apiextensionsinternalversion "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
var parameterCodec = runtime.NewParameterCodec(scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
apiextensionsinternalversion.AddToScheme,
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
var AddToScheme = localSchemeBuilder.AddToScheme
func init() {
v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
utilruntime.Must(AddToScheme(scheme))
}

View File

@@ -1,20 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package contains the scheme of the automatically generated clientset.
package scheme

View File

@@ -1,41 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package scheme
import (
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
)
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
func init() {
v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
Install(Scheme)
}
// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
apiextensions.Install(scheme)
}

View File

@@ -1,96 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package internalversion
import (
"k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/scheme"
rest "k8s.io/client-go/rest"
)
type ApiextensionsInterface interface {
RESTClient() rest.Interface
CustomResourceDefinitionsGetter
}
// ApiextensionsClient is used to interact with features provided by the apiextensions.k8s.io group.
type ApiextensionsClient struct {
restClient rest.Interface
}
func (c *ApiextensionsClient) CustomResourceDefinitions() CustomResourceDefinitionInterface {
return newCustomResourceDefinitions(c)
}
// NewForConfig creates a new ApiextensionsClient for the given config.
func NewForConfig(c *rest.Config) (*ApiextensionsClient, error) {
config := *c
if err := setConfigDefaults(&config); err != nil {
return nil, err
}
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &ApiextensionsClient{client}, nil
}
// NewForConfigOrDie creates a new ApiextensionsClient for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *ApiextensionsClient {
client, err := NewForConfig(c)
if err != nil {
panic(err)
}
return client
}
// New creates a new ApiextensionsClient for the given RESTClient.
func New(c rest.Interface) *ApiextensionsClient {
return &ApiextensionsClient{c}
}
func setConfigDefaults(config *rest.Config) error {
config.APIPath = "/apis"
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
if config.GroupVersion == nil || config.GroupVersion.Group != scheme.Scheme.PrioritizedVersionsForGroup("apiextensions.k8s.io")[0].Group {
gv := scheme.Scheme.PrioritizedVersionsForGroup("apiextensions.k8s.io")[0]
config.GroupVersion = &gv
}
config.NegotiatedSerializer = scheme.Codecs
if config.QPS == 0 {
config.QPS = 5
}
if config.Burst == 0 {
config.Burst = 10
}
return nil
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *ApiextensionsClient) RESTClient() rest.Interface {
if c == nil {
return nil
}
return c.restClient
}

View File

@@ -1,180 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package internalversion
import (
"time"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
scheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/scheme"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
rest "k8s.io/client-go/rest"
)
// CustomResourceDefinitionsGetter has a method to return a CustomResourceDefinitionInterface.
// A group's client should implement this interface.
type CustomResourceDefinitionsGetter interface {
CustomResourceDefinitions() CustomResourceDefinitionInterface
}
// CustomResourceDefinitionInterface has methods to work with CustomResourceDefinition resources.
type CustomResourceDefinitionInterface interface {
Create(*apiextensions.CustomResourceDefinition) (*apiextensions.CustomResourceDefinition, error)
Update(*apiextensions.CustomResourceDefinition) (*apiextensions.CustomResourceDefinition, error)
UpdateStatus(*apiextensions.CustomResourceDefinition) (*apiextensions.CustomResourceDefinition, error)
Delete(name string, options *v1.DeleteOptions) error
DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
Get(name string, options v1.GetOptions) (*apiextensions.CustomResourceDefinition, error)
List(opts v1.ListOptions) (*apiextensions.CustomResourceDefinitionList, error)
Watch(opts v1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *apiextensions.CustomResourceDefinition, err error)
CustomResourceDefinitionExpansion
}
// customResourceDefinitions implements CustomResourceDefinitionInterface
type customResourceDefinitions struct {
client rest.Interface
}
// newCustomResourceDefinitions returns a CustomResourceDefinitions
func newCustomResourceDefinitions(c *ApiextensionsClient) *customResourceDefinitions {
return &customResourceDefinitions{
client: c.RESTClient(),
}
}
// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any.
func (c *customResourceDefinitions) Get(name string, options v1.GetOptions) (result *apiextensions.CustomResourceDefinition, err error) {
result = &apiextensions.CustomResourceDefinition{}
err = c.client.Get().
Resource("customresourcedefinitions").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do().
Into(result)
return
}
// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors.
func (c *customResourceDefinitions) List(opts v1.ListOptions) (result *apiextensions.CustomResourceDefinitionList, err error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
result = &apiextensions.CustomResourceDefinitionList{}
err = c.client.Get().
Resource("customresourcedefinitions").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do().
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested customResourceDefinitions.
func (c *customResourceDefinitions) Watch(opts v1.ListOptions) (watch.Interface, error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
opts.Watch = true
return c.client.Get().
Resource("customresourcedefinitions").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch()
}
// Create takes the representation of a customResourceDefinition and creates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *customResourceDefinitions) Create(customResourceDefinition *apiextensions.CustomResourceDefinition) (result *apiextensions.CustomResourceDefinition, err error) {
result = &apiextensions.CustomResourceDefinition{}
err = c.client.Post().
Resource("customresourcedefinitions").
Body(customResourceDefinition).
Do().
Into(result)
return
}
// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *customResourceDefinitions) Update(customResourceDefinition *apiextensions.CustomResourceDefinition) (result *apiextensions.CustomResourceDefinition, err error) {
result = &apiextensions.CustomResourceDefinition{}
err = c.client.Put().
Resource("customresourcedefinitions").
Name(customResourceDefinition.Name).
Body(customResourceDefinition).
Do().
Into(result)
return
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *customResourceDefinitions) UpdateStatus(customResourceDefinition *apiextensions.CustomResourceDefinition) (result *apiextensions.CustomResourceDefinition, err error) {
result = &apiextensions.CustomResourceDefinition{}
err = c.client.Put().
Resource("customresourcedefinitions").
Name(customResourceDefinition.Name).
SubResource("status").
Body(customResourceDefinition).
Do().
Into(result)
return
}
// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs.
func (c *customResourceDefinitions) Delete(name string, options *v1.DeleteOptions) error {
return c.client.Delete().
Resource("customresourcedefinitions").
Name(name).
Body(options).
Do().
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *customResourceDefinitions) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
var timeout time.Duration
if listOptions.TimeoutSeconds != nil {
timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second
}
return c.client.Delete().
Resource("customresourcedefinitions").
VersionedParams(&listOptions, scheme.ParameterCodec).
Timeout(timeout).
Body(options).
Do().
Error()
}
// Patch applies the patch and returns the patched customResourceDefinition.
func (c *customResourceDefinitions) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *apiextensions.CustomResourceDefinition, err error) {
result = &apiextensions.CustomResourceDefinition{}
err = c.client.Patch(pt).
Resource("customresourcedefinitions").
SubResource(subresources...).
Name(name).
Body(data).
Do().
Into(result)
return
}

View File

@@ -1,20 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated typed clients.
package internalversion

View File

@@ -1,20 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
// Package fake has the automatically generated clients.
package fake

View File

@@ -1,40 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
internalversion "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeApiextensions struct {
*testing.Fake
}
func (c *FakeApiextensions) CustomResourceDefinitions() internalversion.CustomResourceDefinitionInterface {
return &FakeCustomResourceDefinitions{c}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeApiextensions) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}

View File

@@ -1,131 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
)
// FakeCustomResourceDefinitions implements CustomResourceDefinitionInterface
type FakeCustomResourceDefinitions struct {
Fake *FakeApiextensions
}
var customresourcedefinitionsResource = schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "", Resource: "customresourcedefinitions"}
var customresourcedefinitionsKind = schema.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "", Kind: "CustomResourceDefinition"}
// Get takes name of the customResourceDefinition, and returns the corresponding customResourceDefinition object, and an error if there is any.
func (c *FakeCustomResourceDefinitions) Get(name string, options v1.GetOptions) (result *apiextensions.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootGetAction(customresourcedefinitionsResource, name), &apiextensions.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensions.CustomResourceDefinition), err
}
// List takes label and field selectors, and returns the list of CustomResourceDefinitions that match those selectors.
func (c *FakeCustomResourceDefinitions) List(opts v1.ListOptions) (result *apiextensions.CustomResourceDefinitionList, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootListAction(customresourcedefinitionsResource, customresourcedefinitionsKind, opts), &apiextensions.CustomResourceDefinitionList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &apiextensions.CustomResourceDefinitionList{ListMeta: obj.(*apiextensions.CustomResourceDefinitionList).ListMeta}
for _, item := range obj.(*apiextensions.CustomResourceDefinitionList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested customResourceDefinitions.
func (c *FakeCustomResourceDefinitions) Watch(opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewRootWatchAction(customresourcedefinitionsResource, opts))
}
// Create takes the representation of a customResourceDefinition and creates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *FakeCustomResourceDefinitions) Create(customResourceDefinition *apiextensions.CustomResourceDefinition) (result *apiextensions.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootCreateAction(customresourcedefinitionsResource, customResourceDefinition), &apiextensions.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensions.CustomResourceDefinition), err
}
// Update takes the representation of a customResourceDefinition and updates it. Returns the server's representation of the customResourceDefinition, and an error, if there is any.
func (c *FakeCustomResourceDefinitions) Update(customResourceDefinition *apiextensions.CustomResourceDefinition) (result *apiextensions.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateAction(customresourcedefinitionsResource, customResourceDefinition), &apiextensions.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensions.CustomResourceDefinition), err
}
// UpdateStatus was generated because the type contains a Status member.
// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
func (c *FakeCustomResourceDefinitions) UpdateStatus(customResourceDefinition *apiextensions.CustomResourceDefinition) (*apiextensions.CustomResourceDefinition, error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateSubresourceAction(customresourcedefinitionsResource, "status", customResourceDefinition), &apiextensions.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensions.CustomResourceDefinition), err
}
// Delete takes name of the customResourceDefinition and deletes it. Returns an error if one occurs.
func (c *FakeCustomResourceDefinitions) Delete(name string, options *v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewRootDeleteAction(customresourcedefinitionsResource, name), &apiextensions.CustomResourceDefinition{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeCustomResourceDefinitions) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
action := testing.NewRootDeleteCollectionAction(customresourcedefinitionsResource, listOptions)
_, err := c.Fake.Invokes(action, &apiextensions.CustomResourceDefinitionList{})
return err
}
// Patch applies the patch and returns the patched customResourceDefinition.
func (c *FakeCustomResourceDefinitions) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *apiextensions.CustomResourceDefinition, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootPatchSubresourceAction(customresourcedefinitionsResource, name, pt, data, subresources...), &apiextensions.CustomResourceDefinition{})
if obj == nil {
return nil, err
}
return obj.(*apiextensions.CustomResourceDefinition), err
}

View File

@@ -1,21 +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.
*/
// Code generated by client-gen. DO NOT EDIT.
package internalversion
type CustomResourceDefinitionExpansion interface{}

View File

@@ -1,46 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package apiextensions
import (
v1beta1 "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1beta1"
internalinterfaces "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/internalinterfaces"
)
// Interface provides access to each of this group's versions.
type Interface interface {
// V1beta1 provides access to shared informers for resources in V1beta1.
V1beta1() v1beta1.Interface
}
type group struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// V1beta1 returns a new v1beta1.Interface.
func (g *group) V1beta1() v1beta1.Interface {
return v1beta1.New(g.factory, g.namespace, g.tweakListOptions)
}

View File

@@ -1,88 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v1beta1
import (
time "time"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
internalinterfaces "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/internalinterfaces"
v1beta1 "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
// CustomResourceDefinitionInformer provides access to a shared informer and lister for
// CustomResourceDefinitions.
type CustomResourceDefinitionInformer interface {
Informer() cache.SharedIndexInformer
Lister() v1beta1.CustomResourceDefinitionLister
}
type customResourceDefinitionInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// NewCustomResourceDefinitionInformer constructs a new informer for CustomResourceDefinition type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewCustomResourceDefinitionInformer(client clientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredCustomResourceDefinitionInformer(client, resyncPeriod, indexers, nil)
}
// NewFilteredCustomResourceDefinitionInformer constructs a new informer for CustomResourceDefinition type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredCustomResourceDefinitionInformer(client clientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.ApiextensionsV1beta1().CustomResourceDefinitions().List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.ApiextensionsV1beta1().CustomResourceDefinitions().Watch(options)
},
},
&apiextensionsv1beta1.CustomResourceDefinition{},
resyncPeriod,
indexers,
)
}
func (f *customResourceDefinitionInformer) defaultInformer(client clientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredCustomResourceDefinitionInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *customResourceDefinitionInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&apiextensionsv1beta1.CustomResourceDefinition{}, f.defaultInformer)
}
func (f *customResourceDefinitionInformer) Lister() v1beta1.CustomResourceDefinitionLister {
return v1beta1.NewCustomResourceDefinitionLister(f.Informer().GetIndexer())
}

View File

@@ -1,45 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v1beta1
import (
internalinterfaces "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/internalinterfaces"
)
// Interface provides access to all the informers in this group version.
type Interface interface {
// CustomResourceDefinitions returns a CustomResourceDefinitionInformer.
CustomResourceDefinitions() CustomResourceDefinitionInformer
}
type version struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// CustomResourceDefinitions returns a CustomResourceDefinitionInformer.
func (v *version) CustomResourceDefinitions() CustomResourceDefinitionInformer {
return &customResourceDefinitionInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
}

View File

@@ -1,180 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package externalversions
import (
reflect "reflect"
sync "sync"
time "time"
clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apiextensions "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions"
internalinterfaces "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/internalinterfaces"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// SharedInformerOption defines the functional option type for SharedInformerFactory.
type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
type sharedInformerFactory struct {
client clientset.Interface
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
lock sync.Mutex
defaultResync time.Duration
customResync map[reflect.Type]time.Duration
informers map[reflect.Type]cache.SharedIndexInformer
// startedInformers is used for tracking which informers have been started.
// This allows Start() to be called multiple times safely.
startedInformers map[reflect.Type]bool
}
// WithCustomResyncConfig sets a custom resync period for the specified informer types.
func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
for k, v := range resyncConfig {
factory.customResync[reflect.TypeOf(k)] = v
}
return factory
}
}
// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory.
func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.tweakListOptions = tweakListOptions
return factory
}
}
// WithNamespace limits the SharedInformerFactory to the specified namespace.
func WithNamespace(namespace string) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.namespace = namespace
return factory
}
}
// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
func NewSharedInformerFactory(client clientset.Interface, defaultResync time.Duration) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync)
}
// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory.
// Listers obtained via this SharedInformerFactory will be subject to the same filters
// as specified here.
// Deprecated: Please use NewSharedInformerFactoryWithOptions instead
func NewFilteredSharedInformerFactory(client clientset.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions))
}
// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options.
func NewSharedInformerFactoryWithOptions(client clientset.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
factory := &sharedInformerFactory{
client: client,
namespace: v1.NamespaceAll,
defaultResync: defaultResync,
informers: make(map[reflect.Type]cache.SharedIndexInformer),
startedInformers: make(map[reflect.Type]bool),
customResync: make(map[reflect.Type]time.Duration),
}
// Apply all options
for _, opt := range options {
factory = opt(factory)
}
return factory
}
// Start initializes all requested informers.
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
go informer.Run(stopCh)
f.startedInformers[informerType] = true
}
}
}
// WaitForCacheSync waits for all started informers' cache were synced.
func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
informers := func() map[reflect.Type]cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informers := map[reflect.Type]cache.SharedIndexInformer{}
for informerType, informer := range f.informers {
if f.startedInformers[informerType] {
informers[informerType] = informer
}
}
return informers
}()
res := map[reflect.Type]bool{}
for informType, informer := range informers {
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
}
return res
}
// InternalInformerFor returns the SharedIndexInformer for obj using an internal
// client.
func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informerType := reflect.TypeOf(obj)
informer, exists := f.informers[informerType]
if exists {
return informer
}
resyncPeriod, exists := f.customResync[informerType]
if !exists {
resyncPeriod = f.defaultResync
}
informer = newFunc(f.client, resyncPeriod)
f.informers[informerType] = informer
return informer
}
// SharedInformerFactory provides shared informers for resources in all known
// API group versions.
type SharedInformerFactory interface {
internalinterfaces.SharedInformerFactory
ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
Apiextensions() apiextensions.Interface
}
func (f *sharedInformerFactory) Apiextensions() apiextensions.Interface {
return apiextensions.New(f, f.namespace, f.tweakListOptions)
}

View File

@@ -1,62 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package externalversions
import (
"fmt"
v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// GenericInformer is type of SharedIndexInformer which will locate and delegate to other
// sharedInformers based on type
type GenericInformer interface {
Informer() cache.SharedIndexInformer
Lister() cache.GenericLister
}
type genericInformer struct {
informer cache.SharedIndexInformer
resource schema.GroupResource
}
// Informer returns the SharedIndexInformer.
func (f *genericInformer) Informer() cache.SharedIndexInformer {
return f.informer
}
// Lister returns the GenericLister.
func (f *genericInformer) Lister() cache.GenericLister {
return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
}
// ForResource gives generic access to a shared informer of the matching type
// TODO extend this to unknown resources with a client pool
func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
switch resource {
// Group=apiextensions.k8s.io, Version=v1beta1
case v1beta1.SchemeGroupVersion.WithResource("customresourcedefinitions"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Apiextensions().V1beta1().CustomResourceDefinitions().Informer()}, nil
}
return nil, fmt.Errorf("no informer found for %v", resource)
}

View File

@@ -1,40 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package internalinterfaces
import (
time "time"
clientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
cache "k8s.io/client-go/tools/cache"
)
// NewInformerFunc takes clientset.Interface and time.Duration to return a SharedIndexInformer.
type NewInformerFunc func(clientset.Interface, time.Duration) cache.SharedIndexInformer
// SharedInformerFactory a small interface to allow for adding an informer without an import cycle
type SharedInformerFactory interface {
Start(stopCh <-chan struct{})
InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
}
// TweakListOptionsFunc is a function that transforms a v1.ListOptions.
type TweakListOptionsFunc func(*v1.ListOptions)

View File

@@ -1,46 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package apiextensions
import (
internalversion "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
internalinterfaces "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/internalinterfaces"
)
// Interface provides access to each of this group's versions.
type Interface interface {
// InternalVersion provides access to shared informers for resources in InternalVersion.
InternalVersion() internalversion.Interface
}
type group struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// InternalVersion returns a new internalversion.Interface.
func (g *group) InternalVersion() internalversion.Interface {
return internalversion.New(g.factory, g.namespace, g.tweakListOptions)
}

View File

@@ -1,88 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package internalversion
import (
time "time"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
internalclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
internalinterfaces "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/internalinterfaces"
internalversion "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
// CustomResourceDefinitionInformer provides access to a shared informer and lister for
// CustomResourceDefinitions.
type CustomResourceDefinitionInformer interface {
Informer() cache.SharedIndexInformer
Lister() internalversion.CustomResourceDefinitionLister
}
type customResourceDefinitionInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// NewCustomResourceDefinitionInformer constructs a new informer for CustomResourceDefinition type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewCustomResourceDefinitionInformer(client internalclientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredCustomResourceDefinitionInformer(client, resyncPeriod, indexers, nil)
}
// NewFilteredCustomResourceDefinitionInformer constructs a new informer for CustomResourceDefinition type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredCustomResourceDefinitionInformer(client internalclientset.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.Apiextensions().CustomResourceDefinitions().List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.Apiextensions().CustomResourceDefinitions().Watch(options)
},
},
&apiextensions.CustomResourceDefinition{},
resyncPeriod,
indexers,
)
}
func (f *customResourceDefinitionInformer) defaultInformer(client internalclientset.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredCustomResourceDefinitionInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *customResourceDefinitionInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&apiextensions.CustomResourceDefinition{}, f.defaultInformer)
}
func (f *customResourceDefinitionInformer) Lister() internalversion.CustomResourceDefinitionLister {
return internalversion.NewCustomResourceDefinitionLister(f.Informer().GetIndexer())
}

View File

@@ -1,45 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package internalversion
import (
internalinterfaces "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/internalinterfaces"
)
// Interface provides access to all the informers in this group version.
type Interface interface {
// CustomResourceDefinitions returns a CustomResourceDefinitionInformer.
CustomResourceDefinitions() CustomResourceDefinitionInformer
}
type version struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// CustomResourceDefinitions returns a CustomResourceDefinitionInformer.
func (v *version) CustomResourceDefinitions() CustomResourceDefinitionInformer {
return &customResourceDefinitionInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
}

View File

@@ -1,180 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package internalversion
import (
reflect "reflect"
sync "sync"
time "time"
internalclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
apiextensions "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions"
internalinterfaces "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/internalinterfaces"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// SharedInformerOption defines the functional option type for SharedInformerFactory.
type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
type sharedInformerFactory struct {
client internalclientset.Interface
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
lock sync.Mutex
defaultResync time.Duration
customResync map[reflect.Type]time.Duration
informers map[reflect.Type]cache.SharedIndexInformer
// startedInformers is used for tracking which informers have been started.
// This allows Start() to be called multiple times safely.
startedInformers map[reflect.Type]bool
}
// WithCustomResyncConfig sets a custom resync period for the specified informer types.
func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
for k, v := range resyncConfig {
factory.customResync[reflect.TypeOf(k)] = v
}
return factory
}
}
// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory.
func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.tweakListOptions = tweakListOptions
return factory
}
}
// WithNamespace limits the SharedInformerFactory to the specified namespace.
func WithNamespace(namespace string) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.namespace = namespace
return factory
}
}
// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
func NewSharedInformerFactory(client internalclientset.Interface, defaultResync time.Duration) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync)
}
// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory.
// Listers obtained via this SharedInformerFactory will be subject to the same filters
// as specified here.
// Deprecated: Please use NewSharedInformerFactoryWithOptions instead
func NewFilteredSharedInformerFactory(client internalclientset.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions))
}
// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options.
func NewSharedInformerFactoryWithOptions(client internalclientset.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
factory := &sharedInformerFactory{
client: client,
namespace: v1.NamespaceAll,
defaultResync: defaultResync,
informers: make(map[reflect.Type]cache.SharedIndexInformer),
startedInformers: make(map[reflect.Type]bool),
customResync: make(map[reflect.Type]time.Duration),
}
// Apply all options
for _, opt := range options {
factory = opt(factory)
}
return factory
}
// Start initializes all requested informers.
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
go informer.Run(stopCh)
f.startedInformers[informerType] = true
}
}
}
// WaitForCacheSync waits for all started informers' cache were synced.
func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
informers := func() map[reflect.Type]cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informers := map[reflect.Type]cache.SharedIndexInformer{}
for informerType, informer := range f.informers {
if f.startedInformers[informerType] {
informers[informerType] = informer
}
}
return informers
}()
res := map[reflect.Type]bool{}
for informType, informer := range informers {
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
}
return res
}
// InternalInformerFor returns the SharedIndexInformer for obj using an internal
// client.
func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informerType := reflect.TypeOf(obj)
informer, exists := f.informers[informerType]
if exists {
return informer
}
resyncPeriod, exists := f.customResync[informerType]
if !exists {
resyncPeriod = f.defaultResync
}
informer = newFunc(f.client, resyncPeriod)
f.informers[informerType] = informer
return informer
}
// SharedInformerFactory provides shared informers for resources in all known
// API group versions.
type SharedInformerFactory interface {
internalinterfaces.SharedInformerFactory
ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
Apiextensions() apiextensions.Interface
}
func (f *sharedInformerFactory) Apiextensions() apiextensions.Interface {
return apiextensions.New(f, f.namespace, f.tweakListOptions)
}

View File

@@ -1,62 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package internalversion
import (
"fmt"
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// GenericInformer is type of SharedIndexInformer which will locate and delegate to other
// sharedInformers based on type
type GenericInformer interface {
Informer() cache.SharedIndexInformer
Lister() cache.GenericLister
}
type genericInformer struct {
informer cache.SharedIndexInformer
resource schema.GroupResource
}
// Informer returns the SharedIndexInformer.
func (f *genericInformer) Informer() cache.SharedIndexInformer {
return f.informer
}
// Lister returns the GenericLister.
func (f *genericInformer) Lister() cache.GenericLister {
return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
}
// ForResource gives generic access to a shared informer of the matching type
// TODO extend this to unknown resources with a client pool
func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
switch resource {
// Group=apiextensions.k8s.io, Version=internalVersion
case apiextensions.SchemeGroupVersion.WithResource("customresourcedefinitions"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Apiextensions().InternalVersion().CustomResourceDefinitions().Informer()}, nil
}
return nil, fmt.Errorf("no informer found for %v", resource)
}

View File

@@ -1,40 +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.
*/
// Code generated by informer-gen. DO NOT EDIT.
package internalinterfaces
import (
time "time"
internalclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
cache "k8s.io/client-go/tools/cache"
)
// NewInformerFunc takes internalclientset.Interface and time.Duration to return a SharedIndexInformer.
type NewInformerFunc func(internalclientset.Interface, time.Duration) cache.SharedIndexInformer
// SharedInformerFactory a small interface to allow for adding an informer without an import cycle
type SharedInformerFactory interface {
Start(stopCh <-chan struct{})
InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
}
// TweakListOptionsFunc is a function that transforms a v1.ListOptions.
type TweakListOptionsFunc func(*v1.ListOptions)

View File

@@ -1,65 +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.
*/
// Code generated by lister-gen. DO NOT EDIT.
package internalversion
import (
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
)
// CustomResourceDefinitionLister helps list CustomResourceDefinitions.
type CustomResourceDefinitionLister interface {
// List lists all CustomResourceDefinitions in the indexer.
List(selector labels.Selector) (ret []*apiextensions.CustomResourceDefinition, err error)
// Get retrieves the CustomResourceDefinition from the index for a given name.
Get(name string) (*apiextensions.CustomResourceDefinition, error)
CustomResourceDefinitionListerExpansion
}
// customResourceDefinitionLister implements the CustomResourceDefinitionLister interface.
type customResourceDefinitionLister struct {
indexer cache.Indexer
}
// NewCustomResourceDefinitionLister returns a new CustomResourceDefinitionLister.
func NewCustomResourceDefinitionLister(indexer cache.Indexer) CustomResourceDefinitionLister {
return &customResourceDefinitionLister{indexer: indexer}
}
// List lists all CustomResourceDefinitions in the indexer.
func (s *customResourceDefinitionLister) List(selector labels.Selector) (ret []*apiextensions.CustomResourceDefinition, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*apiextensions.CustomResourceDefinition))
})
return ret, err
}
// Get retrieves the CustomResourceDefinition from the index for a given name.
func (s *customResourceDefinitionLister) Get(name string) (*apiextensions.CustomResourceDefinition, error) {
obj, exists, err := s.indexer.GetByKey(name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(apiextensions.Resource("customresourcedefinition"), name)
}
return obj.(*apiextensions.CustomResourceDefinition), nil
}

View File

@@ -1,23 +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.
*/
// Code generated by lister-gen. DO NOT EDIT.
package internalversion
// CustomResourceDefinitionListerExpansion allows custom methods to be added to
// CustomResourceDefinitionLister.
type CustomResourceDefinitionListerExpansion interface{}

View File

@@ -1,65 +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.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v1beta1
import (
v1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
)
// CustomResourceDefinitionLister helps list CustomResourceDefinitions.
type CustomResourceDefinitionLister interface {
// List lists all CustomResourceDefinitions in the indexer.
List(selector labels.Selector) (ret []*v1beta1.CustomResourceDefinition, err error)
// Get retrieves the CustomResourceDefinition from the index for a given name.
Get(name string) (*v1beta1.CustomResourceDefinition, error)
CustomResourceDefinitionListerExpansion
}
// customResourceDefinitionLister implements the CustomResourceDefinitionLister interface.
type customResourceDefinitionLister struct {
indexer cache.Indexer
}
// NewCustomResourceDefinitionLister returns a new CustomResourceDefinitionLister.
func NewCustomResourceDefinitionLister(indexer cache.Indexer) CustomResourceDefinitionLister {
return &customResourceDefinitionLister{indexer: indexer}
}
// List lists all CustomResourceDefinitions in the indexer.
func (s *customResourceDefinitionLister) List(selector labels.Selector) (ret []*v1beta1.CustomResourceDefinition, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v1beta1.CustomResourceDefinition))
})
return ret, err
}
// Get retrieves the CustomResourceDefinition from the index for a given name.
func (s *customResourceDefinitionLister) Get(name string) (*v1beta1.CustomResourceDefinition, error) {
obj, exists, err := s.indexer.GetByKey(name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v1beta1.Resource("customresourcedefinition"), name)
}
return obj.(*v1beta1.CustomResourceDefinition), nil
}

View File

@@ -1,23 +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.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v1beta1
// CustomResourceDefinitionListerExpansion allows custom methods to be added to
// CustomResourceDefinitionLister.
type CustomResourceDefinitionListerExpansion interface{}

View File

@@ -1,134 +0,0 @@
/*
Copyright 2018 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 options
import (
"fmt"
"io"
"net"
"net/url"
"github.com/spf13/pflag"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/apiserver"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
genericregistry "k8s.io/apiserver/pkg/registry/generic"
genericapiserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/pkg/util/proxy"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/listers/core/v1"
)
const defaultEtcdPathPrefix = "/registry/apiextensions.kubernetes.io"
// CustomResourceDefinitionsServerOptions describes the runtime options of an apiextensions-apiserver.
type CustomResourceDefinitionsServerOptions struct {
RecommendedOptions *genericoptions.RecommendedOptions
APIEnablement *genericoptions.APIEnablementOptions
StdOut io.Writer
StdErr io.Writer
}
// NewCustomResourceDefinitionsServerOptions creates default options of an apiextensions-apiserver.
func NewCustomResourceDefinitionsServerOptions(out, errOut io.Writer) *CustomResourceDefinitionsServerOptions {
o := &CustomResourceDefinitionsServerOptions{
RecommendedOptions: genericoptions.NewRecommendedOptions(
defaultEtcdPathPrefix,
apiserver.Codecs.LegacyCodec(v1beta1.SchemeGroupVersion),
genericoptions.NewProcessInfo("apiextensions-apiserver", "kube-system"),
),
APIEnablement: genericoptions.NewAPIEnablementOptions(),
StdOut: out,
StdErr: errOut,
}
return o
}
// AddFlags adds the apiextensions-apiserver flags to the flagset.
func (o CustomResourceDefinitionsServerOptions) AddFlags(fs *pflag.FlagSet) {
o.RecommendedOptions.AddFlags(fs)
o.APIEnablement.AddFlags(fs)
}
// Validate validates the apiextensions-apiserver options.
func (o CustomResourceDefinitionsServerOptions) Validate() error {
errors := []error{}
errors = append(errors, o.RecommendedOptions.Validate()...)
errors = append(errors, o.APIEnablement.Validate(apiserver.Scheme)...)
return utilerrors.NewAggregate(errors)
}
// Complete fills in missing options.
func (o *CustomResourceDefinitionsServerOptions) Complete() error {
return nil
}
// Config returns an apiextensions-apiserver configuration.
func (o CustomResourceDefinitionsServerOptions) Config() (*apiserver.Config, error) {
// TODO have a "real" external address
if err := o.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{net.ParseIP("127.0.0.1")}); err != nil {
return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
}
serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
if err := o.RecommendedOptions.ApplyTo(serverConfig, apiserver.Scheme); err != nil {
return nil, err
}
if err := o.APIEnablement.ApplyTo(&serverConfig.Config, apiserver.DefaultAPIResourceConfigSource(), apiserver.Scheme); err != nil {
return nil, err
}
config := &apiserver.Config{
GenericConfig: serverConfig,
ExtraConfig: apiserver.ExtraConfig{
CRDRESTOptionsGetter: NewCRDRESTOptionsGetter(*o.RecommendedOptions.Etcd),
ServiceResolver: &serviceResolver{serverConfig.SharedInformerFactory.Core().V1().Services().Lister()},
AuthResolverWrapper: webhook.NewDefaultAuthenticationInfoResolverWrapper(nil, serverConfig.LoopbackClientConfig),
},
}
return config, nil
}
// NewCRDRESTOptionsGetter create a RESTOptionsGetter for CustomResources.
func NewCRDRESTOptionsGetter(etcdOptions genericoptions.EtcdOptions) genericregistry.RESTOptionsGetter {
ret := apiserver.CRDRESTOptionsGetter{
StorageConfig: etcdOptions.StorageConfig,
StoragePrefix: etcdOptions.StorageConfig.Prefix,
EnableWatchCache: etcdOptions.EnableWatchCache,
DefaultWatchCacheSize: etcdOptions.DefaultWatchCacheSize,
EnableGarbageCollection: etcdOptions.EnableGarbageCollection,
DeleteCollectionWorkers: etcdOptions.DeleteCollectionWorkers,
CountMetricPollPeriod: etcdOptions.StorageConfig.CountMetricPollPeriod,
}
ret.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
return ret
}
type serviceResolver struct {
services v1.ServiceLister
}
func (r *serviceResolver) ResolveEndpoint(namespace, name string) (*url.URL, error) {
return proxy.ResolveCluster(r.services, namespace, name)
}

View File

@@ -1,64 +0,0 @@
/*
Copyright 2018 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 server
import (
"io"
"github.com/spf13/cobra"
"k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
genericapiserver "k8s.io/apiserver/pkg/server"
)
func NewServerCommand(out, errOut io.Writer, stopCh <-chan struct{}) *cobra.Command {
o := options.NewCustomResourceDefinitionsServerOptions(out, errOut)
cmd := &cobra.Command{
Short: "Launch an API extensions API server",
Long: "Launch an API extensions API server",
RunE: func(c *cobra.Command, args []string) error {
if err := o.Complete(); err != nil {
return err
}
if err := o.Validate(); err != nil {
return err
}
if err := Run(o, stopCh); err != nil {
return err
}
return nil
},
}
fs := cmd.Flags()
o.AddFlags(fs)
return cmd
}
func Run(o *options.CustomResourceDefinitionsServerOptions, stopCh <-chan struct{}) error {
config, err := o.Config()
if err != nil {
return err
}
server, err := config.Complete().New(genericapiserver.NewEmptyDelegate())
if err != nil {
return err
}
return server.GenericAPIServer.PrepareRun().Run(stopCh)
}

View File

@@ -1,37 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDGjCCAgKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdsb2Nh
bGhvc3QtY2FAMTUzMTQ2ODA4NTAgFw0xODA3MTMwNjQ4MDVaGA8yMTE4MDYxOTA2
NDgwNVowHzEdMBsGA1UEAwwUbG9jYWxob3N0QDE1MzE0NjgwODYwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDC9Qfx1YAEp+wrSIjbinWw3pWIDbf57Lut
fXgS84ilZpc7M2zeu1QrPyhCedL/gPP0QxKbPS6AR5R/DibH4RWcujL6CU5FB0Y9
on+IpN/Iml2XzgGiU82gTkJg185VgWwDaHOPKvUF9N1GpvxcSvRsNGoiBJ/LlE4N
hxyUQ0V/lAalYxYybxgl8/xghWMkGnQc3YKWKqGmtBaaax3xvMzamxpWPphoLG07
+YZfAf0Q7vslVMmlslRmx9OpJFvRnkelbXoHHx73umbMiFp28njY8NK2dqXwb6Z8
0BCezppCKYpbjnupOIDAAE0KvjzhhzSS68ZgukiBZOcUlnWLzL39AgMBAAGjXDBa
MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8E
AjAAMCUGA1UdEQQeMByCCWxvY2FsaG9zdIIJbG9jYWxob3N0hwR/AAABMA0GCSqG
SIb3DQEBCwUAA4IBAQBm9Z15QxsRqoaRDh/ELA93eE9105gwrXrR3AK/iKuJyxIc
/SXVbpAaYHArMrUzaZs0GXEzgW31tZn8D3dgFy8XdZxk1ztaFTm+QTnFRogMNB8A
kvpq7jwTa44c7G0wuNO2nATMu2Ifi/nSdQadTxzmZacSrevN/zcjmvSoV4VFkKO5
VBnr7e1ruffxAaVAzrRraplpZvuJzlcvqTYAME8fq8H9QidvaXF6yIbEPwwUHK67
8W4rXn9Zp6NDuQhH0eNPAGlEAaYuyCJvJZeM68ootMi7Uh6RJOTDw1HIdekYEr1/
FAg4+rH/9Gi+o/LXsBmYXabO+GjsOwfezv3THDnw
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIC5DCCAcygAwIBAgIBATANBgkqhkiG9w0BAQsFADAiMSAwHgYDVQQDDBdsb2Nh
bGhvc3QtY2FAMTUzMTQ2ODA4NTAgFw0xODA3MTMwNjQ4MDVaGA8yMTE4MDYxOTA2
NDgwNVowIjEgMB4GA1UEAwwXbG9jYWxob3N0LWNhQDE1MzE0NjgwODUwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFDtkZLlseubpzPQr1IT3xE28/1cDM
3UB2vp+pbiwvkmHvfm9RxMmDZbYWqXL5beCG/ladHaJ0RW4f3sBPzxDzUP1zFM94
iYDZmVA0f7ZvtijuiUrwc6TP21ctX5TlN2YYoh4Er+aZ6E5MQIMAxkP+w1lwyYmj
oxopD1wf/4cmZgv9axZV1zR5MrpyVboQa4mQRUoMlLQFE0erMc0yIJsIVWXx9yS7
9dzhEXC2WPYnPHh6AYSQZBdzumj5HdZhOQhL/UOWq6AJGP1p7l0uXPArmDG0g9pS
aFxAQx6IxtdSUOZFBlel/p+iOab3H8eoNkgCPMXmrM3WZLW7Uy5Onx6LAgMBAAGj
IzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB
CwUAA4IBAQC0BaURRhpRbORaOiZJFeQR0gCD9goggXNj+cPh8yYol2GNBdLfed6x
B6ZxGW2Y14pOjo2Ba5FdZyzfOenjhkqhUujQSMxKjkWxX03blLjPEvLo/Wi6TLnp
H3Qmm/Gq1GAxmkZUaovcE71/zpZ/lJLWVVxz21wxcGQIA30DUKgdycfOvWdYkYGf
tnsXpY4yIyHvKvfeIbxo8doBnoRvYd4a7QyY0zw/Q3qaCdINo9zKJYV0CnpSZhKy
5RLcrlfvXttEYryP57RWP9GYJMNmiovGcbls4pOwZMNueUx5qEzIbC9lsfyt13Bc
oZ1lk1TqzwJERoALmgT0ccp7LRxIV90P
-----END CERTIFICATE-----

View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEAwvUH8dWABKfsK0iI24p1sN6ViA23+ey7rX14EvOIpWaXOzNs
3rtUKz8oQnnS/4Dz9EMSmz0ugEeUfw4mx+EVnLoy+glORQdGPaJ/iKTfyJpdl84B
olPNoE5CYNfOVYFsA2hzjyr1BfTdRqb8XEr0bDRqIgSfy5RODYcclENFf5QGpWMW
Mm8YJfP8YIVjJBp0HN2CliqhprQWmmsd8bzM2psaVj6YaCxtO/mGXwH9EO77JVTJ
pbJUZsfTqSRb0Z5HpW16Bx8e97pmzIhadvJ42PDStnal8G+mfNAQns6aQimKW457
qTiAwABNCr484Yc0kuvGYLpIgWTnFJZ1i8y9/QIDAQABAoIBAQC3rgioLpEI0fVO
6m4W+iLcWznjD5nQnNxOHrJsmIqb20aM/myKhVBN1pll2EWVeLdf4Xm8TS5sqgQc
mQast0KXgU7aCL53ht0E4P7rjPtSky2vAubDO9W2PUWI3IqIQQG1TEnkD4L+868v
C9EkW3JiKBf+qQCs27OWR0AD6EWoZfc9NT5AVnL1aTwKnLpSt80umXUMmR5c4ddT
58FO9JgZCzHbcoAyosf4v8fiz588zjBDRRienHourxmUnsKJrIyCMrygBpQ7wIcO
R86e+PuQ+toeSbldq8EfN/XoY0POUvLL/zu6HkbZLhOEjEHQMkMRCMtmLCjuU+sy
TwdRl0xBAoGBAOtC+kqh5eYgQAYtvPaW/XQwrang+G2xIYd/OTXzIlU2T3sTPrDo
IEkwsXwAeKcJx+Z7XcipSG5l1s5QZbc+GIARmJ0DrAwLkDhlwz8RssxBY4q64Jtn
FWRm6CBJkPDq4t16ZbXDdUleXAaqbvxTTnPZMz2o1XHEDpUcPG29MnF1AoGBANQk
hh44qY3IUxRwp6ZoNHVRz7oJC2JLj1++QNoLXX/8LWTXd49RZpI7Byk2lEXl7clf
3rErBuWElrxxuikOahEdzkjKSZq4RSKj1lNpy14spBRo7O9Q+GrJgcr3tcuyT6ri
sXOo6ET5bG72j+uVzv/QbuyCS8elZQHifBIi58FpAoGBAIXsQ8TWcqPUuf2KbeZF
v5Qz3bg1y9XNnpOedbfjZyjw2L/sDaTxDuf7Ix2+uvADnlry4UlILGZD7MrXc3+j
hpjo62J+Y6MbtgaUz1eIwKqLkpm3lgKA5OmZtwwiNLMgUvLXKS0WTh7s6yAUR0e2
OJO0EfpIsPCpNc/mGfQyXpO9AoGBANEVblo63mGvHrL2lUWdTpaSm3lvkJjAf/6N
NL2yleSeVt8cvemzALT/GH+2G00I4OOoaYOUNKfhle8E58WvPzq/daCoPwMOupT5
hTORAa8/sDetclgsJaqaECJLIhIxG/QAmYS05LeWXRjASfiXHf1jIPBZOvb6PCMv
Zbk6TdCBAoGBAIxT9W5FhHl+uUpuHIuFvCUg0AoERk9dUPXVEz1C1lqsYfetZ0uz
wh9KtFq2puWrez+K4o04LoInXHFeb34Hx/9SRDCTawmMicuLxivbTm4yjG2RsShl
nbq376nyawu9rcjM3F5ucYLiGHmQOPYWSOvGr2QhGgFgzXiBQ1RcAYdH
-----END RSA PRIVATE KEY-----

View File

@@ -1,211 +0,0 @@
/*
Copyright 2018 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 testing
import (
"fmt"
"io/ioutil"
"net"
"os"
"path"
"runtime"
"time"
"github.com/spf13/pflag"
"k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/registry/generic/registry"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/storage/storagebackend"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
)
// TearDownFunc is to be called to tear down a test server.
type TearDownFunc func()
// TestServerInstanceOptions Instance options the TestServer
type TestServerInstanceOptions struct {
// DisableStorageCleanup Disable the automatic storage cleanup
DisableStorageCleanup bool
}
// TestServer return values supplied by kube-test-ApiServer
type TestServer struct {
ClientConfig *restclient.Config // Rest client config
ServerOpts *options.CustomResourceDefinitionsServerOptions // ServerOpts
TearDownFn TearDownFunc // TearDown function
TmpDir string // Temp Dir used, by the apiserver
}
// Logger allows t.Testing and b.Testing to be passed to StartTestServer and StartTestServerOrDie
type Logger interface {
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Logf(format string, args ...interface{})
}
// NewDefaultTestServerOptions Default options for TestServer instances
func NewDefaultTestServerOptions() *TestServerInstanceOptions {
return &TestServerInstanceOptions{
DisableStorageCleanup: false,
}
}
// StartTestServer starts a apiextensions-apiserver. A rest client config and a tear-down func,
// and location of the tmpdir are returned.
//
// Note: we return a tear-down func instead of a stop channel because the later will leak temporary
// files that because Golang testing's call to os.Exit will not give a stop channel go routine
// enough time to remove temporary files.
func StartTestServer(t Logger, instanceOptions *TestServerInstanceOptions, customFlags []string, storageConfig *storagebackend.Config) (result TestServer, err error) {
if instanceOptions == nil {
instanceOptions = NewDefaultTestServerOptions()
}
// TODO : Remove TrackStorageCleanup below when PR
// https://github.com/kubernetes/kubernetes/pull/50690
// merges as that shuts down storage properly
if !instanceOptions.DisableStorageCleanup {
registry.TrackStorageCleanup()
}
stopCh := make(chan struct{})
tearDown := func() {
if !instanceOptions.DisableStorageCleanup {
registry.CleanupStorage()
}
close(stopCh)
if len(result.TmpDir) != 0 {
os.RemoveAll(result.TmpDir)
}
}
defer func() {
if result.TearDownFn == nil {
tearDown()
}
}()
result.TmpDir, err = ioutil.TempDir("", "apiextensions-apiserver")
if err != nil {
return result, fmt.Errorf("failed to create temp dir: %v", err)
}
fs := pflag.NewFlagSet("test", pflag.PanicOnError)
s := options.NewCustomResourceDefinitionsServerOptions(os.Stdout, os.Stderr)
s.AddFlags(fs)
s.RecommendedOptions.SecureServing.Listener, s.RecommendedOptions.SecureServing.BindPort, err = createLocalhostListenerOnFreePort()
if err != nil {
return result, fmt.Errorf("failed to create listener: %v", err)
}
s.RecommendedOptions.SecureServing.ServerCert.CertDirectory = result.TmpDir
s.RecommendedOptions.SecureServing.ExternalAddress = s.RecommendedOptions.SecureServing.Listener.Addr().(*net.TCPAddr).IP // use listener addr although it is a loopback device
_, thisFile, _, ok := runtime.Caller(0)
if !ok {
return result, fmt.Errorf("failed to get current file")
}
s.RecommendedOptions.SecureServing.ServerCert.FixtureDirectory = path.Join(path.Dir(thisFile), "testdata")
if storageConfig != nil {
s.RecommendedOptions.Etcd.StorageConfig = *storageConfig
}
s.APIEnablement.RuntimeConfig.Set("api/all=true")
fs.Parse(customFlags)
if err := s.Complete(); err != nil {
return result, fmt.Errorf("failed to set default options: %v", err)
}
if err := s.Validate(); err != nil {
return result, fmt.Errorf("failed to validate options: %v", err)
}
t.Logf("runtime-config=%v", s.APIEnablement.RuntimeConfig)
t.Logf("Starting apiextensions-apiserver on port %d...", s.RecommendedOptions.SecureServing.BindPort)
config, err := s.Config()
if err != nil {
return result, fmt.Errorf("failed to create config from options: %v", err)
}
server, err := config.Complete().New(genericapiserver.NewEmptyDelegate())
if err != nil {
return result, fmt.Errorf("failed to create server: %v", err)
}
go func(stopCh <-chan struct{}) {
if err := server.GenericAPIServer.PrepareRun().Run(stopCh); err != nil {
t.Errorf("apiextensions-apiserver failed run: %v", err)
}
}(stopCh)
t.Logf("Waiting for /healthz to be ok...")
client, err := kubernetes.NewForConfig(server.GenericAPIServer.LoopbackClientConfig)
if err != nil {
return result, fmt.Errorf("failed to create a client: %v", err)
}
err = wait.Poll(100*time.Millisecond, 30*time.Second, func() (bool, error) {
result := client.CoreV1().RESTClient().Get().AbsPath("/healthz").Do()
status := 0
result.StatusCode(&status)
if status == 200 {
return true, nil
}
return false, nil
})
if err != nil {
return result, fmt.Errorf("failed to wait for /healthz to return ok: %v", err)
}
// from here the caller must call tearDown
result.ClientConfig = server.GenericAPIServer.LoopbackClientConfig
result.ServerOpts = s
result.TearDownFn = tearDown
return result, nil
}
// StartTestServerOrDie calls StartTestServer t.Fatal if it does not succeed.
func StartTestServerOrDie(t Logger, instanceOptions *TestServerInstanceOptions, flags []string, storageConfig *storagebackend.Config) *TestServer {
result, err := StartTestServer(t, instanceOptions, flags, storageConfig)
if err == nil {
return &result
}
t.Fatalf("failed to launch server: %v", err)
return nil
}
func createLocalhostListenerOnFreePort() (net.Listener, int, error) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, 0, err
}
// get port
tcpAddr, ok := ln.Addr().(*net.TCPAddr)
if !ok {
ln.Close()
return nil, 0, fmt.Errorf("invalid listen address: %q", ln.Addr().String())
}
return ln, tcpAddr.Port, nil
}

View File

@@ -1,143 +0,0 @@
/*
Copyright 2018 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 establish
import (
"fmt"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
client "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
)
// EstablishingController controls how and when CRD is established.
type EstablishingController struct {
crdClient client.CustomResourceDefinitionsGetter
crdLister listers.CustomResourceDefinitionLister
crdSynced cache.InformerSynced
// To allow injection for testing.
syncFn func(key string) error
queue workqueue.RateLimitingInterface
}
// NewEstablishingController creates new EstablishingController.
func NewEstablishingController(crdInformer informers.CustomResourceDefinitionInformer,
crdClient client.CustomResourceDefinitionsGetter) *EstablishingController {
ec := &EstablishingController{
crdClient: crdClient,
crdLister: crdInformer.Lister(),
crdSynced: crdInformer.Informer().HasSynced,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "crdEstablishing"),
}
ec.syncFn = ec.sync
return ec
}
// QueueCRD adds CRD into the establishing queue.
func (ec *EstablishingController) QueueCRD(key string, timeout time.Duration) {
ec.queue.AddAfter(key, timeout)
}
// Run starts the EstablishingController.
func (ec *EstablishingController) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer ec.queue.ShutDown()
klog.Infof("Starting EstablishingController")
defer klog.Infof("Shutting down EstablishingController")
if !cache.WaitForCacheSync(stopCh, ec.crdSynced) {
return
}
// only start one worker thread since its a slow moving API
go wait.Until(ec.runWorker, time.Second, stopCh)
<-stopCh
}
func (ec *EstablishingController) runWorker() {
for ec.processNextWorkItem() {
}
}
// processNextWorkItem deals with one key off the queue.
// It returns false when it's time to quit.
func (ec *EstablishingController) processNextWorkItem() bool {
key, quit := ec.queue.Get()
if quit {
return false
}
defer ec.queue.Done(key)
err := ec.syncFn(key.(string))
if err == nil {
ec.queue.Forget(key)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
ec.queue.AddRateLimited(key)
return true
}
// sync is used to turn CRDs into the Established state.
func (ec *EstablishingController) sync(key string) error {
cachedCRD, err := ec.crdLister.Get(key)
if apierrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
if !apiextensions.IsCRDConditionTrue(cachedCRD, apiextensions.NamesAccepted) ||
apiextensions.IsCRDConditionTrue(cachedCRD, apiextensions.Established) {
return nil
}
crd := cachedCRD.DeepCopy()
establishedCondition := apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionTrue,
Reason: "InitialNamesAccepted",
Message: "the initial names have been accepted",
}
apiextensions.SetCRDCondition(crd, establishedCondition)
// Update server with new CRD condition.
_, err = ec.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil {
return err
}
return nil
}

View File

@@ -1,327 +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 finalizer
import (
"fmt"
"reflect"
"time"
"k8s.io/klog"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
client "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
)
// CRDFinalizer is a controller that finalizes the CRD by deleting all the CRs associated with it.
type CRDFinalizer struct {
crdClient client.CustomResourceDefinitionsGetter
crClientGetter CRClientGetter
crdLister listers.CustomResourceDefinitionLister
crdSynced cache.InformerSynced
// To allow injection for testing.
syncFn func(key string) error
queue workqueue.RateLimitingInterface
}
// ListerCollectionDeleter combines rest.Lister and rest.CollectionDeleter.
type ListerCollectionDeleter interface {
rest.Lister
rest.CollectionDeleter
}
// CRClientGetter knows how to get a ListerCollectionDeleter for a given CRD UID.
type CRClientGetter interface {
// GetCustomResourceListerCollectionDeleter gets the ListerCollectionDeleter for the given CRD
// UID.
GetCustomResourceListerCollectionDeleter(crd *apiextensions.CustomResourceDefinition) (ListerCollectionDeleter, error)
}
// NewCRDFinalizer creates a new CRDFinalizer.
func NewCRDFinalizer(
crdInformer informers.CustomResourceDefinitionInformer,
crdClient client.CustomResourceDefinitionsGetter,
crClientGetter CRClientGetter,
) *CRDFinalizer {
c := &CRDFinalizer{
crdClient: crdClient,
crdLister: crdInformer.Lister(),
crdSynced: crdInformer.Informer().HasSynced,
crClientGetter: crClientGetter,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "CustomResourceDefinition-CRDFinalizer"),
}
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addCustomResourceDefinition,
UpdateFunc: c.updateCustomResourceDefinition,
})
c.syncFn = c.sync
return c
}
func (c *CRDFinalizer) sync(key string) error {
cachedCRD, err := c.crdLister.Get(key)
if apierrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
// no work to do
if cachedCRD.DeletionTimestamp.IsZero() || !apiextensions.CRDHasFinalizer(cachedCRD, apiextensions.CustomResourceCleanupFinalizer) {
return nil
}
crd := cachedCRD.DeepCopy()
// update the status condition. This cleanup could take a while.
apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionInProgress",
Message: "CustomResource deletion is in progress",
})
crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil {
return err
}
// Now we can start deleting items. We should use the REST API to ensure that all normal admission runs.
// Since we control the endpoints, we know that delete collection works. No need to delete if not established.
if apiextensions.IsCRDConditionTrue(crd, apiextensions.Established) {
cond, deleteErr := c.deleteInstances(crd)
apiextensions.SetCRDCondition(crd, cond)
if deleteErr != nil {
crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil {
utilruntime.HandleError(err)
}
return deleteErr
}
} else {
apiextensions.SetCRDCondition(crd, apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionFalse,
Reason: "NeverEstablished",
Message: "resource was never established",
})
}
apiextensions.CRDRemoveFinalizer(crd, apiextensions.CustomResourceCleanupFinalizer)
crd, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil {
return err
}
// and now issue another delete, which should clean it all up if no finalizers remain or no-op if they do
return c.crdClient.CustomResourceDefinitions().Delete(crd.Name, nil)
}
func (c *CRDFinalizer) deleteInstances(crd *apiextensions.CustomResourceDefinition) (apiextensions.CustomResourceDefinitionCondition, error) {
// Now we can start deleting items. While it would be ideal to use a REST API client, doing so
// could incorrectly delete a ThirdPartyResource with the same URL as the CustomResource, so we go
// directly to the storage instead. Since we control the storage, we know that delete collection works.
crClient, err := c.crClientGetter.GetCustomResourceListerCollectionDeleter(crd)
if err != nil {
err = fmt.Errorf("unable to find a custom resource client for %s.%s: %v", crd.Status.AcceptedNames.Plural, crd.Spec.Group, err)
return apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionFailed",
Message: fmt.Sprintf("could not list instances: %v", err),
}, err
}
ctx := genericapirequest.NewContext()
allResources, err := crClient.List(ctx, nil)
if err != nil {
return apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionFailed",
Message: fmt.Sprintf("could not list instances: %v", err),
}, err
}
deletedNamespaces := sets.String{}
deleteErrors := []error{}
for _, item := range allResources.(*unstructured.UnstructuredList).Items {
metadata, err := meta.Accessor(&item)
if err != nil {
utilruntime.HandleError(err)
continue
}
if deletedNamespaces.Has(metadata.GetNamespace()) {
continue
}
// don't retry deleting the same namespace
deletedNamespaces.Insert(metadata.GetNamespace())
nsCtx := genericapirequest.WithNamespace(ctx, metadata.GetNamespace())
if _, err := crClient.DeleteCollection(nsCtx, nil, nil); err != nil {
deleteErrors = append(deleteErrors, err)
continue
}
}
if deleteError := utilerrors.NewAggregate(deleteErrors); deleteError != nil {
return apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionFailed",
Message: fmt.Sprintf("could not issue all deletes: %v", deleteError),
}, deleteError
}
// now we need to wait until all the resources are deleted. Start with a simple poll before we do anything fancy.
// TODO not all servers are synchronized on caches. It is possible for a stale one to still be creating things.
// Once we have a mechanism for servers to indicate their states, we should check that for concurrence.
err = wait.PollImmediate(5*time.Second, 1*time.Minute, func() (bool, error) {
listObj, err := crClient.List(ctx, nil)
if err != nil {
return false, err
}
if len(listObj.(*unstructured.UnstructuredList).Items) == 0 {
return true, nil
}
klog.V(2).Infof("%s.%s waiting for %d items to be removed", crd.Status.AcceptedNames.Plural, crd.Spec.Group, len(listObj.(*unstructured.UnstructuredList).Items))
return false, nil
})
if err != nil {
return apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionCheck",
Message: fmt.Sprintf("could not confirm zero CustomResources remaining: %v", err),
}, err
}
return apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionFalse,
Reason: "InstanceDeletionCompleted",
Message: "removed all instances",
}, nil
}
func (c *CRDFinalizer) Run(workers int, stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.Infof("Starting CRDFinalizer")
defer klog.Infof("Shutting down CRDFinalizer")
if !cache.WaitForCacheSync(stopCh, c.crdSynced) {
return
}
for i := 0; i < workers; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
<-stopCh
}
func (c *CRDFinalizer) runWorker() {
for c.processNextWorkItem() {
}
}
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
func (c *CRDFinalizer) processNextWorkItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.syncFn(key.(string))
if err == nil {
c.queue.Forget(key)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
c.queue.AddRateLimited(key)
return true
}
func (c *CRDFinalizer) enqueue(obj *apiextensions.CustomResourceDefinition) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %#v: %v", obj, err))
return
}
c.queue.Add(key)
}
func (c *CRDFinalizer) addCustomResourceDefinition(obj interface{}) {
castObj := obj.(*apiextensions.CustomResourceDefinition)
// only queue deleted things
if !castObj.DeletionTimestamp.IsZero() && apiextensions.CRDHasFinalizer(castObj, apiextensions.CustomResourceCleanupFinalizer) {
c.enqueue(castObj)
}
}
func (c *CRDFinalizer) updateCustomResourceDefinition(oldObj, newObj interface{}) {
oldCRD := oldObj.(*apiextensions.CustomResourceDefinition)
newCRD := newObj.(*apiextensions.CustomResourceDefinition)
// only queue deleted things that haven't been finalized by us
if newCRD.DeletionTimestamp.IsZero() || !apiextensions.CRDHasFinalizer(newCRD, apiextensions.CustomResourceCleanupFinalizer) {
return
}
// always requeue resyncs just in case
if oldCRD.ResourceVersion == newCRD.ResourceVersion {
c.enqueue(newCRD)
return
}
// If the only difference is in the terminating condition, then there's no reason to requeue here. This controller
// is likely to be the originator, so requeuing would hot-loop us. Failures are requeued by the workqueue directly.
// This is a low traffic and scale resource, so the copy is terrible. It's not good, so better ideas
// are welcome.
oldCopy := oldCRD.DeepCopy()
newCopy := newCRD.DeepCopy()
oldCopy.ResourceVersion = ""
newCopy.ResourceVersion = ""
apiextensions.RemoveCRDCondition(oldCopy, apiextensions.Terminating)
apiextensions.RemoveCRDCondition(newCopy, apiextensions.Terminating)
if !reflect.DeepEqual(oldCopy, newCopy) {
c.enqueue(newCRD)
}
}

View File

@@ -1,374 +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 status
import (
"fmt"
"reflect"
"strings"
"time"
"k8s.io/klog"
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
client "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion"
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
)
// This controller is reserving names. To avoid conflicts, be sure to run only one instance of the worker at a time.
// This could eventually be lifted, but starting simple.
type NamingConditionController struct {
crdClient client.CustomResourceDefinitionsGetter
crdLister listers.CustomResourceDefinitionLister
crdSynced cache.InformerSynced
// crdMutationCache backs our lister and keeps track of committed updates to avoid racy
// write/lookup cycles. It's got 100 slots by default, so it unlikely to overrun
// TODO to revisit this if naming conflicts are found to occur in the wild
crdMutationCache cache.MutationCache
// To allow injection for testing.
syncFn func(key string) error
queue workqueue.RateLimitingInterface
}
func NewNamingConditionController(
crdInformer informers.CustomResourceDefinitionInformer,
crdClient client.CustomResourceDefinitionsGetter,
) *NamingConditionController {
c := &NamingConditionController{
crdClient: crdClient,
crdLister: crdInformer.Lister(),
crdSynced: crdInformer.Informer().HasSynced,
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "CustomResourceDefinition-NamingConditionController"),
}
informerIndexer := crdInformer.Informer().GetIndexer()
c.crdMutationCache = cache.NewIntegerResourceVersionMutationCache(informerIndexer, informerIndexer, 60*time.Second, false)
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addCustomResourceDefinition,
UpdateFunc: c.updateCustomResourceDefinition,
DeleteFunc: c.deleteCustomResourceDefinition,
})
c.syncFn = c.sync
return c
}
func (c *NamingConditionController) getAcceptedNamesForGroup(group string) (allResources sets.String, allKinds sets.String) {
allResources = sets.String{}
allKinds = sets.String{}
list, err := c.crdLister.List(labels.Everything())
if err != nil {
panic(err)
}
for _, curr := range list {
if curr.Spec.Group != group {
continue
}
// for each item here, see if we have a mutation cache entry that is more recent
// this makes sure that if we tight loop on update and run, our mutation cache will show
// us the version of the objects we just updated to.
item := curr
obj, exists, err := c.crdMutationCache.GetByKey(curr.Name)
if exists && err == nil {
item = obj.(*apiextensions.CustomResourceDefinition)
}
allResources.Insert(item.Status.AcceptedNames.Plural)
allResources.Insert(item.Status.AcceptedNames.Singular)
allResources.Insert(item.Status.AcceptedNames.ShortNames...)
allKinds.Insert(item.Status.AcceptedNames.Kind)
allKinds.Insert(item.Status.AcceptedNames.ListKind)
}
return allResources, allKinds
}
func (c *NamingConditionController) calculateNamesAndConditions(in *apiextensions.CustomResourceDefinition) (apiextensions.CustomResourceDefinitionNames, apiextensions.CustomResourceDefinitionCondition, apiextensions.CustomResourceDefinitionCondition) {
// Get the names that have already been claimed
allResources, allKinds := c.getAcceptedNamesForGroup(in.Spec.Group)
namesAcceptedCondition := apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.NamesAccepted,
Status: apiextensions.ConditionUnknown,
}
requestedNames := in.Spec.Names
acceptedNames := in.Status.AcceptedNames
newNames := in.Status.AcceptedNames
// Check each name for mismatches. If there's a mismatch between spec and status, then try to deconflict.
// Continue on errors so that the status is the best match possible
if err := equalToAcceptedOrFresh(requestedNames.Plural, acceptedNames.Plural, allResources); err != nil {
namesAcceptedCondition.Status = apiextensions.ConditionFalse
namesAcceptedCondition.Reason = "PluralConflict"
namesAcceptedCondition.Message = err.Error()
} else {
newNames.Plural = requestedNames.Plural
}
if err := equalToAcceptedOrFresh(requestedNames.Singular, acceptedNames.Singular, allResources); err != nil {
namesAcceptedCondition.Status = apiextensions.ConditionFalse
namesAcceptedCondition.Reason = "SingularConflict"
namesAcceptedCondition.Message = err.Error()
} else {
newNames.Singular = requestedNames.Singular
}
if !reflect.DeepEqual(requestedNames.ShortNames, acceptedNames.ShortNames) {
errs := []error{}
existingShortNames := sets.NewString(acceptedNames.ShortNames...)
for _, shortName := range requestedNames.ShortNames {
// if the shortname is already ours, then we're fine
if existingShortNames.Has(shortName) {
continue
}
if err := equalToAcceptedOrFresh(shortName, "", allResources); err != nil {
errs = append(errs, err)
}
}
if err := utilerrors.NewAggregate(errs); err != nil {
namesAcceptedCondition.Status = apiextensions.ConditionFalse
namesAcceptedCondition.Reason = "ShortNamesConflict"
namesAcceptedCondition.Message = err.Error()
} else {
newNames.ShortNames = requestedNames.ShortNames
}
}
if err := equalToAcceptedOrFresh(requestedNames.Kind, acceptedNames.Kind, allKinds); err != nil {
namesAcceptedCondition.Status = apiextensions.ConditionFalse
namesAcceptedCondition.Reason = "KindConflict"
namesAcceptedCondition.Message = err.Error()
} else {
newNames.Kind = requestedNames.Kind
}
if err := equalToAcceptedOrFresh(requestedNames.ListKind, acceptedNames.ListKind, allKinds); err != nil {
namesAcceptedCondition.Status = apiextensions.ConditionFalse
namesAcceptedCondition.Reason = "ListKindConflict"
namesAcceptedCondition.Message = err.Error()
} else {
newNames.ListKind = requestedNames.ListKind
}
newNames.Categories = requestedNames.Categories
// if we haven't changed the condition, then our names must be good.
if namesAcceptedCondition.Status == apiextensions.ConditionUnknown {
namesAcceptedCondition.Status = apiextensions.ConditionTrue
namesAcceptedCondition.Reason = "NoConflicts"
namesAcceptedCondition.Message = "no conflicts found"
}
// set EstablishedCondition initially to false, then set it to true in establishing controller.
// The Establishing Controller will see the NamesAccepted condition when it arrives through the shared informer.
// At that time the API endpoint handler will serve the endpoint, avoiding a race
// which we had if we set Established to true here.
establishedCondition := apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionFalse,
Reason: "NotAccepted",
Message: "not all names are accepted",
}
if old := apiextensions.FindCRDCondition(in, apiextensions.Established); old != nil {
establishedCondition = *old
}
if establishedCondition.Status != apiextensions.ConditionTrue && namesAcceptedCondition.Status == apiextensions.ConditionTrue {
establishedCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionFalse,
Reason: "Installing",
Message: "the initial names have been accepted",
}
}
return newNames, namesAcceptedCondition, establishedCondition
}
func equalToAcceptedOrFresh(requestedName, acceptedName string, usedNames sets.String) error {
if requestedName == acceptedName {
return nil
}
if !usedNames.Has(requestedName) {
return nil
}
return fmt.Errorf("%q is already in use", requestedName)
}
func (c *NamingConditionController) sync(key string) error {
inCustomResourceDefinition, err := c.crdLister.Get(key)
if apierrors.IsNotFound(err) {
// CRD was deleted and has freed its names.
// Reconsider all other CRDs in the same group.
if err := c.requeueAllOtherGroupCRDs(key); err != nil {
return err
}
return nil
}
if err != nil {
return err
}
// Skip checking names if Spec and Status names are same.
if equality.Semantic.DeepEqual(inCustomResourceDefinition.Spec.Names, inCustomResourceDefinition.Status.AcceptedNames) {
return nil
}
acceptedNames, namingCondition, establishedCondition := c.calculateNamesAndConditions(inCustomResourceDefinition)
// nothing to do if accepted names and NamesAccepted condition didn't change
if reflect.DeepEqual(inCustomResourceDefinition.Status.AcceptedNames, acceptedNames) &&
apiextensions.IsCRDConditionEquivalent(&namingCondition, apiextensions.FindCRDCondition(inCustomResourceDefinition, apiextensions.NamesAccepted)) {
return nil
}
crd := inCustomResourceDefinition.DeepCopy()
crd.Status.AcceptedNames = acceptedNames
apiextensions.SetCRDCondition(crd, namingCondition)
apiextensions.SetCRDCondition(crd, establishedCondition)
updatedObj, err := c.crdClient.CustomResourceDefinitions().UpdateStatus(crd)
if err != nil {
return err
}
// if the update was successful, go ahead and add the entry to the mutation cache
c.crdMutationCache.Mutation(updatedObj)
// we updated our status, so we may be releasing a name. When this happens, we need to rekick everything in our group
// if we fail to rekick, just return as normal. We'll get everything on a resync
if err := c.requeueAllOtherGroupCRDs(key); err != nil {
return err
}
return nil
}
func (c *NamingConditionController) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
defer c.queue.ShutDown()
klog.Infof("Starting NamingConditionController")
defer klog.Infof("Shutting down NamingConditionController")
if !cache.WaitForCacheSync(stopCh, c.crdSynced) {
return
}
// only start one worker thread since its a slow moving API and the naming conflict resolution bits aren't thread-safe
go wait.Until(c.runWorker, time.Second, stopCh)
<-stopCh
}
func (c *NamingConditionController) runWorker() {
for c.processNextWorkItem() {
}
}
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
func (c *NamingConditionController) processNextWorkItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.syncFn(key.(string))
if err == nil {
c.queue.Forget(key)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err))
c.queue.AddRateLimited(key)
return true
}
func (c *NamingConditionController) enqueue(obj *apiextensions.CustomResourceDefinition) {
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %#v: %v", obj, err))
return
}
c.queue.Add(key)
}
func (c *NamingConditionController) addCustomResourceDefinition(obj interface{}) {
castObj := obj.(*apiextensions.CustomResourceDefinition)
klog.V(4).Infof("Adding %s", castObj.Name)
c.enqueue(castObj)
}
func (c *NamingConditionController) updateCustomResourceDefinition(obj, _ interface{}) {
castObj := obj.(*apiextensions.CustomResourceDefinition)
klog.V(4).Infof("Updating %s", castObj.Name)
c.enqueue(castObj)
}
func (c *NamingConditionController) deleteCustomResourceDefinition(obj interface{}) {
castObj, ok := obj.(*apiextensions.CustomResourceDefinition)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
klog.Errorf("Couldn't get object from tombstone %#v", obj)
return
}
castObj, ok = tombstone.Obj.(*apiextensions.CustomResourceDefinition)
if !ok {
klog.Errorf("Tombstone contained object that is not expected %#v", obj)
return
}
}
klog.V(4).Infof("Deleting %q", castObj.Name)
c.enqueue(castObj)
}
func (c *NamingConditionController) requeueAllOtherGroupCRDs(name string) error {
pluralGroup := strings.SplitN(name, ".", 2)
list, err := c.crdLister.List(labels.Everything())
if err != nil {
return err
}
for _, curr := range list {
if curr.Spec.Group == pluralGroup[1] && curr.Name != name {
c.queue.Add(curr.Name)
}
}
return nil
}

View File

@@ -1,344 +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 status
import (
"reflect"
"strings"
"testing"
"time"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/cache"
)
type crdBuilder struct {
curr apiextensions.CustomResourceDefinition
}
func newCRD(name string) *crdBuilder {
tokens := strings.SplitN(name, ".", 2)
return &crdBuilder{
curr: apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: name},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: tokens[1],
Names: apiextensions.CustomResourceDefinitionNames{
Plural: tokens[0],
},
},
},
}
}
func (b *crdBuilder) SpecNames(plural, singular, kind, listKind string, shortNames ...string) *crdBuilder {
b.curr.Spec.Names.Plural = plural
b.curr.Spec.Names.Singular = singular
b.curr.Spec.Names.Kind = kind
b.curr.Spec.Names.ListKind = listKind
b.curr.Spec.Names.ShortNames = shortNames
return b
}
func (b *crdBuilder) StatusNames(plural, singular, kind, listKind string, shortNames ...string) *crdBuilder {
b.curr.Status.AcceptedNames.Plural = plural
b.curr.Status.AcceptedNames.Singular = singular
b.curr.Status.AcceptedNames.Kind = kind
b.curr.Status.AcceptedNames.ListKind = listKind
b.curr.Status.AcceptedNames.ShortNames = shortNames
return b
}
func (b *crdBuilder) Condition(c apiextensions.CustomResourceDefinitionCondition) *crdBuilder {
b.curr.Status.Conditions = append(b.curr.Status.Conditions, c)
return b
}
func names(plural, singular, kind, listKind string, shortNames ...string) apiextensions.CustomResourceDefinitionNames {
ret := apiextensions.CustomResourceDefinitionNames{
Plural: plural,
Singular: singular,
Kind: kind,
ListKind: listKind,
ShortNames: shortNames,
}
return ret
}
func (b *crdBuilder) NewOrDie() *apiextensions.CustomResourceDefinition {
return &b.curr
}
var acceptedCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.NamesAccepted,
Status: apiextensions.ConditionTrue,
Reason: "NoConflicts",
Message: "no conflicts found",
}
var notAcceptedCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.NamesAccepted,
Status: apiextensions.ConditionFalse,
Reason: "NotAccepted",
Message: "not all names are accepted",
}
var installingCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionFalse,
Reason: "Installing",
Message: "the initial names have been accepted",
}
var notEstablishedCondition = apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Established,
Status: apiextensions.ConditionFalse,
Reason: "NotAccepted",
Message: "not all names are accepted",
}
func nameConflictCondition(reason, message string) apiextensions.CustomResourceDefinitionCondition {
return apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.NamesAccepted,
Status: apiextensions.ConditionFalse,
Reason: reason,
Message: message,
}
}
func TestSync(t *testing.T) {
tests := []struct {
name string
in *apiextensions.CustomResourceDefinition
existing []*apiextensions.CustomResourceDefinition
expectedNames apiextensions.CustomResourceDefinitionNames
expectedNameConflictCondition apiextensions.CustomResourceDefinitionCondition
expectedEstablishedCondition apiextensions.CustomResourceDefinitionCondition
}{
{
name: "first resource",
in: newCRD("alfa.bravo.com").NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{},
expectedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "alfa",
},
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: installingCondition,
},
{
name: "different groups",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("alfa.charlie.com").StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
},
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: installingCondition,
},
{
name: "conflict plural to singular",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "alfa", "", "").NewOrDie(),
},
expectedNames: names("", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("PluralConflict", `"alfa" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
{
name: "conflict singular to shortName",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "indias", "", "", "delta-singular").NewOrDie(),
},
expectedNames: names("alfa", "", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("SingularConflict", `"delta-singular" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
{
name: "conflict on shortName to shortName",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "indias", "", "", "hotel-shortname-2").NewOrDie(),
},
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind"),
expectedNameConflictCondition: nameConflictCondition("ShortNamesConflict", `"hotel-shortname-2" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
{
name: "conflict on kind to listkind",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "indias", "", "echo-kind").NewOrDie(),
},
expectedNames: names("alfa", "delta-singular", "", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("KindConflict", `"echo-kind" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
{
name: "conflict on listkind to kind",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "").NewOrDie(),
},
expectedNames: names("alfa", "delta-singular", "echo-kind", "", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("ListKindConflict", `"foxtrot-listkind" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
{
name: "no conflict on resource and kind",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "echo-kind", "", "").NewOrDie(),
},
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: installingCondition,
},
{
name: "merge on conflicts",
in: newCRD("alfa.bravo.com").
SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
StatusNames("zulu", "yankee-singular", "xray-kind", "whiskey-listkind", "victor-shortname-1", "uniform-shortname-2").
NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "", "delta-singular").NewOrDie(),
},
expectedNames: names("alfa", "yankee-singular", "echo-kind", "whiskey-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("ListKindConflict", `"foxtrot-listkind" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
{
name: "merge on conflicts shortNames as one",
in: newCRD("alfa.bravo.com").
SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
StatusNames("zulu", "yankee-singular", "xray-kind", "whiskey-listkind", "victor-shortname-1", "uniform-shortname-2").
NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "indias", "foxtrot-listkind", "", "delta-singular", "golf-shortname-1").NewOrDie(),
},
expectedNames: names("alfa", "yankee-singular", "echo-kind", "whiskey-listkind", "victor-shortname-1", "uniform-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("ListKindConflict", `"foxtrot-listkind" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
{
name: "no conflicts on self",
in: newCRD("alfa.bravo.com").
SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("alfa.bravo.com").
SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
NewOrDie(),
},
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: installingCondition,
},
{
name: "no conflicts on self, remove shortname",
in: newCRD("alfa.bravo.com").
SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1").
StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("alfa.bravo.com").
SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
StatusNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
NewOrDie(),
},
expectedNames: names("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1"),
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: installingCondition,
},
{
name: "installing before with true condition",
in: newCRD("alfa.bravo.com").Condition(acceptedCondition).NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{},
expectedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "alfa",
},
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: installingCondition,
},
{
name: "not installing before with false condition",
in: newCRD("alfa.bravo.com").Condition(notAcceptedCondition).NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{},
expectedNames: apiextensions.CustomResourceDefinitionNames{
Plural: "alfa",
},
expectedNameConflictCondition: acceptedCondition,
expectedEstablishedCondition: installingCondition,
},
{
name: "conflicting, installing before with true condition",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
Condition(acceptedCondition).
NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "alfa", "", "").NewOrDie(),
},
expectedNames: names("", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("PluralConflict", `"alfa" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
{
name: "conflicting, not installing before with false condition",
in: newCRD("alfa.bravo.com").SpecNames("alfa", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2").
Condition(notAcceptedCondition).
NewOrDie(),
existing: []*apiextensions.CustomResourceDefinition{
newCRD("india.bravo.com").StatusNames("india", "alfa", "", "").NewOrDie(),
},
expectedNames: names("", "delta-singular", "echo-kind", "foxtrot-listkind", "golf-shortname-1", "hotel-shortname-2"),
expectedNameConflictCondition: nameConflictCondition("PluralConflict", `"alfa" is already in use`),
expectedEstablishedCondition: notEstablishedCondition,
},
}
for _, tc := range tests {
crdIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
for _, obj := range tc.existing {
crdIndexer.Add(obj)
}
c := NamingConditionController{
crdLister: listers.NewCustomResourceDefinitionLister(crdIndexer),
crdMutationCache: cache.NewIntegerResourceVersionMutationCache(crdIndexer, crdIndexer, 60*time.Second, false),
}
actualNames, actualNameConflictCondition, establishedCondition := c.calculateNamesAndConditions(tc.in)
if e, a := tc.expectedNames, actualNames; !reflect.DeepEqual(e, a) {
t.Errorf("%v expected %v, got %#v", tc.name, e, a)
}
if e, a := tc.expectedNameConflictCondition, actualNameConflictCondition; !apiextensions.IsCRDConditionEquivalent(&e, &a) {
t.Errorf("%v expected %v, got %v", tc.name, e, a)
}
if e, a := tc.expectedEstablishedCondition, establishedCondition; !apiextensions.IsCRDConditionEquivalent(&e, &a) {
t.Errorf("%v expected %v, got %v", tc.name, e, a)
}
}
}

View File

@@ -1,67 +0,0 @@
/*
Copyright 2016 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 crdserverscheme
import (
"reflect"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// UnstructuredObjectTyper provides a runtime.ObjectTyper implementation for
// runtime.Unstructured object based on discovery information.
type UnstructuredObjectTyper struct {
}
// NewUnstructuredObjectTyper returns a runtime.ObjectTyper for
// unstructured objects based on discovery information. It accepts a list of fallback typers
// for handling objects that are not runtime.Unstructured. It does not delegate the Recognizes
// check, only ObjectKinds.
// TODO this only works for the apiextensions server and doesn't recognize any types. Move to point of use.
func NewUnstructuredObjectTyper() *UnstructuredObjectTyper {
dot := &UnstructuredObjectTyper{}
return dot
}
// ObjectKinds returns a slice of one element with the group,version,kind of the
// provided object, or an error if the object is not runtime.Unstructured or
// has no group,version,kind information. unversionedType will always be false
// because runtime.Unstructured object should always have group,version,kind
// information set.
func (d *UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) (gvks []schema.GroupVersionKind, unversionedType bool, err error) {
if _, ok := obj.(runtime.Unstructured); ok {
gvk := obj.GetObjectKind().GroupVersionKind()
if len(gvk.Kind) == 0 {
return nil, false, runtime.NewMissingKindErr("object has no kind field ")
}
if len(gvk.Version) == 0 {
return nil, false, runtime.NewMissingVersionErr("object has no apiVersion field")
}
return []schema.GroupVersionKind{gvk}, false, nil
}
return nil, false, runtime.NewNotRegisteredErrForType("crdserverscheme.UnstructuredObjectTyper", reflect.TypeOf(obj))
}
// Recognizes returns true if the provided group,version,kind was in the
// discovery information.
func (d *UnstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
return false
}
var _ runtime.ObjectTyper = &UnstructuredObjectTyper{}

View File

@@ -1,2 +0,0 @@
approvers:
- feature-approvers

View File

@@ -1,62 +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 features
import (
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
const (
// Every feature gate should add method here following this template:
//
// // owner: @username
// // alpha: v1.4
// MyFeature() bool
// owner: @sttts, @nikhita
// alpha: v1.8
// beta: v1.9
//
// CustomResourceValidation is a list of validation methods for CustomResources
CustomResourceValidation utilfeature.Feature = "CustomResourceValidation"
// owner: @sttts, @nikhita
// alpha: v1.10
// beta: v1.11
//
// CustomResourceSubresources defines the subresources for CustomResources
CustomResourceSubresources utilfeature.Feature = "CustomResourceSubresources"
// owner: @mbohlool, @roycaihw
// alpha: v1.13
//
// CustomResourceWebhookConversion defines the webhook conversion for Custom Resources.
CustomResourceWebhookConversion utilfeature.Feature = "CustomResourceWebhookConversion"
)
func init() {
utilfeature.DefaultFeatureGate.Add(defaultKubernetesFeatureGates)
}
// defaultKubernetesFeatureGates consists of all known Kubernetes-specific feature keys.
// To add a new feature, define a key for it above and add it here. The features will be
// available throughout Kubernetes binaries.
var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{
CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
CustomResourceSubresources: {Default: true, PreRelease: utilfeature.Beta},
CustomResourceWebhookConversion: {Default: false, PreRelease: utilfeature.Alpha},
}

View File

@@ -1,330 +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 customresource
import (
"context"
"fmt"
"strings"
autoscalingv1 "k8s.io/api/autoscaling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
)
// CustomResourceStorage includes dummy storage for CustomResources, and their Status and Scale subresources.
type CustomResourceStorage struct {
CustomResource *REST
Status *StatusREST
Scale *ScaleREST
}
func NewStorage(resource schema.GroupResource, kind, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter, categories []string, tableConvertor rest.TableConvertor) CustomResourceStorage {
customResourceREST, customResourceStatusREST := newREST(resource, kind, listKind, strategy, optsGetter, categories, tableConvertor)
s := CustomResourceStorage{
CustomResource: customResourceREST,
}
if strategy.status != nil {
s.Status = customResourceStatusREST
}
if scale := strategy.scale; scale != nil {
var labelSelectorPath string
if scale.LabelSelectorPath != nil {
labelSelectorPath = *scale.LabelSelectorPath
}
s.Scale = &ScaleREST{
store: customResourceREST.Store,
specReplicasPath: scale.SpecReplicasPath,
statusReplicasPath: scale.StatusReplicasPath,
labelSelectorPath: labelSelectorPath,
}
}
return s
}
// REST implements a RESTStorage for API services against etcd
type REST struct {
*genericregistry.Store
categories []string
}
// newREST returns a RESTStorage object that will work against API services.
func newREST(resource schema.GroupResource, kind, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter, categories []string, tableConvertor rest.TableConvertor) (*REST, *StatusREST) {
store := &genericregistry.Store{
NewFunc: func() runtime.Object {
// set the expected group/version/kind in the new object as a signal to the versioning decoder
ret := &unstructured.Unstructured{}
ret.SetGroupVersionKind(kind)
return ret
},
NewListFunc: func() runtime.Object {
// lists are never stored, only manufactured, so stomp in the right kind
ret := &unstructured.UnstructuredList{}
ret.SetGroupVersionKind(listKind)
return ret
},
PredicateFunc: strategy.MatchCustomResourceDefinitionStorage,
DefaultQualifiedResource: resource,
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
TableConvertor: tableConvertor,
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: strategy.GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
panic(err) // TODO: Propagate error up
}
statusStore := *store
statusStore.UpdateStrategy = NewStatusStrategy(strategy)
return &REST{store, categories}, &StatusREST{store: &statusStore}
}
// Implement CategoriesProvider
var _ rest.CategoriesProvider = &REST{}
// List returns a list of items matching labels and field according to the store's PredicateFunc.
func (e *REST) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
l, err := e.Store.List(ctx, options)
if err != nil {
return nil, err
}
// Shallow copy ObjectMeta in returned list for each item. Native types have `Items []Item` fields and therefore
// implicitly shallow copy ObjectMeta. The generic store sets the self-link for each item. So this is necessary
// to avoid mutation of the objects from the cache.
if ul, ok := l.(*unstructured.UnstructuredList); ok {
for i := range ul.Items {
shallowCopyObjectMeta(&ul.Items[i])
}
}
return l, nil
}
func (r *REST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
o, err := r.Store.Get(ctx, name, options)
if err != nil {
return nil, err
}
if u, ok := o.(*unstructured.Unstructured); ok {
shallowCopyObjectMeta(u)
}
return o, nil
}
func shallowCopyObjectMeta(u runtime.Unstructured) {
obj := shallowMapDeepCopy(u.UnstructuredContent())
if metadata, ok := obj["metadata"]; ok {
if metadata, ok := metadata.(map[string]interface{}); ok {
obj["metadata"] = shallowMapDeepCopy(metadata)
u.SetUnstructuredContent(obj)
}
}
}
func shallowMapDeepCopy(in map[string]interface{}) map[string]interface{} {
if in == nil {
return nil
}
out := make(map[string]interface{}, len(in))
for k, v := range in {
out[k] = v
}
return out
}
// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of.
func (r *REST) Categories() []string {
return r.categories
}
// StatusREST implements the REST endpoint for changing the status of a CustomResource
type StatusREST struct {
store *genericregistry.Store
}
var _ = rest.Patcher(&StatusREST{})
func (r *StatusREST) New() runtime.Object {
return &unstructured.Unstructured{}
}
// Get retrieves the object from the storage. It is required to support Patch.
func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return r.store.Get(ctx, name, options)
}
// Update alters the status subset of an object.
func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
// We are explicitly setting forceAllowCreate to false in the call to the underlying storage because
// subresources should never allow create on update.
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options)
}
type ScaleREST struct {
store *genericregistry.Store
specReplicasPath string
statusReplicasPath string
labelSelectorPath string
}
// ScaleREST implements Patcher
var _ = rest.Patcher(&ScaleREST{})
var _ = rest.GroupVersionKindProvider(&ScaleREST{})
func (r *ScaleREST) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind {
return autoscalingv1.SchemeGroupVersion.WithKind("Scale")
}
// New creates a new Scale object
func (r *ScaleREST) New() runtime.Object {
return &autoscalingv1.Scale{}
}
func (r *ScaleREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
obj, err := r.store.Get(ctx, name, options)
if err != nil {
return nil, err
}
cr := obj.(*unstructured.Unstructured)
scaleObject, replicasFound, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, err
}
if !replicasFound {
return nil, apierrors.NewInternalError(fmt.Errorf("the spec replicas field %q does not exist", r.specReplicasPath))
}
return scaleObject, err
}
func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
obj, err := r.store.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
cr := obj.(*unstructured.Unstructured)
const invalidSpecReplicas = -2147483648 // smallest int32
oldScale, replicasFound, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, false, err
}
if !replicasFound {
oldScale.Spec.Replicas = invalidSpecReplicas // signal that this was not set before
}
obj, err = objInfo.UpdatedObject(ctx, oldScale)
if err != nil {
return nil, false, err
}
if obj == nil {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("nil update passed to Scale"))
}
scale, ok := obj.(*autoscalingv1.Scale)
if !ok {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj))
}
if scale.Spec.Replicas == invalidSpecReplicas {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("the spec replicas field %q cannot be empty", r.specReplicasPath))
}
specReplicasPath := strings.TrimPrefix(r.specReplicasPath, ".") // ignore leading period
if err = unstructured.SetNestedField(cr.Object, int64(scale.Spec.Replicas), strings.Split(specReplicasPath, ".")...); err != nil {
return nil, false, err
}
cr.SetResourceVersion(scale.ResourceVersion)
obj, _, err = r.store.Update(ctx, cr.GetName(), rest.DefaultUpdatedObjectInfo(cr), createValidation, updateValidation, false, options)
if err != nil {
return nil, false, err
}
cr = obj.(*unstructured.Unstructured)
newScale, _, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, false, apierrors.NewBadRequest(err.Error())
}
return newScale, false, err
}
// scaleFromCustomResource returns a scale subresource for a customresource and a bool signalling wether
// the specReplicas value was found.
func scaleFromCustomResource(cr *unstructured.Unstructured, specReplicasPath, statusReplicasPath, labelSelectorPath string) (*autoscalingv1.Scale, bool, error) {
specReplicasPath = strings.TrimPrefix(specReplicasPath, ".") // ignore leading period
specReplicas, foundSpecReplicas, err := unstructured.NestedInt64(cr.UnstructuredContent(), strings.Split(specReplicasPath, ".")...)
if err != nil {
return nil, false, err
} else if !foundSpecReplicas {
specReplicas = 0
}
statusReplicasPath = strings.TrimPrefix(statusReplicasPath, ".") // ignore leading period
statusReplicas, found, err := unstructured.NestedInt64(cr.UnstructuredContent(), strings.Split(statusReplicasPath, ".")...)
if err != nil {
return nil, false, err
} else if !found {
statusReplicas = 0
}
var labelSelector string
if len(labelSelectorPath) > 0 {
labelSelectorPath = strings.TrimPrefix(labelSelectorPath, ".") // ignore leading period
labelSelector, found, err = unstructured.NestedString(cr.UnstructuredContent(), strings.Split(labelSelectorPath, ".")...)
if err != nil {
return nil, false, err
}
}
scale := &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: cr.GetName(),
Namespace: cr.GetNamespace(),
UID: cr.GetUID(),
ResourceVersion: cr.GetResourceVersion(),
CreationTimestamp: cr.GetCreationTimestamp(),
},
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(specReplicas),
},
Status: autoscalingv1.ScaleStatus{
Replicas: int32(statusReplicas),
Selector: labelSelector,
},
}
return scale, foundSpecReplicas, nil
}

View File

@@ -1,576 +0,0 @@
/*
Copyright 2018 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 customresource_test
import (
"io"
"reflect"
"strings"
"testing"
"time"
autoscalingv1 "k8s.io/api/autoscaling/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metainternal "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/diff"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
registrytest "k8s.io/apiserver/pkg/registry/generic/testing"
"k8s.io/apiserver/pkg/registry/rest"
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver"
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
)
func newStorage(t *testing.T) (customresource.CustomResourceStorage, *etcdtesting.EtcdTestServer) {
server, etcdStorage := etcdtesting.NewUnsecuredEtcd3TestClientServer(t)
etcdStorage.Codec = unstructuredJsonCodec{}
restOptions := generic.RESTOptions{StorageConfig: etcdStorage, Decorator: generic.UndecoratedStorage, DeleteCollectionWorkers: 1, ResourcePrefix: "noxus"}
parameterScheme := runtime.NewScheme()
parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: "mygroup.example.com", Version: "v1beta1"},
&metav1.ListOptions{},
&metav1.ExportOptions{},
&metav1.GetOptions{},
&metav1.DeleteOptions{},
)
typer := apiserver.UnstructuredObjectTyper{
Delegate: parameterScheme,
UnstructuredTyper: crdserverscheme.NewUnstructuredObjectTyper(),
}
kind := schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "Noxu"}
labelSelectorPath := ".status.labelSelector"
scale := &apiextensions.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: &labelSelectorPath,
}
status := &apiextensions.CustomResourceSubresourceStatus{}
headers := []apiextensions.CustomResourceColumnDefinition{
{Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
{Name: "Replicas", Type: "integer", JSONPath: ".spec.replicas"},
{Name: "Missing", Type: "string", JSONPath: ".spec.missing"},
{Name: "Invalid", Type: "integer", JSONPath: ".spec.string"},
{Name: "String", Type: "string", JSONPath: ".spec.string"},
{Name: "StringFloat64", Type: "string", JSONPath: ".spec.float64"},
{Name: "StringInt64", Type: "string", JSONPath: ".spec.replicas"},
{Name: "StringBool", Type: "string", JSONPath: ".spec.bool"},
{Name: "Float64", Type: "number", JSONPath: ".spec.float64"},
{Name: "Bool", Type: "boolean", JSONPath: ".spec.bool"},
}
table, _ := tableconvertor.New(headers)
storage := customresource.NewStorage(
schema.GroupResource{Group: "mygroup.example.com", Resource: "noxus"},
kind,
schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "NoxuItemList"},
customresource.NewStrategy(
typer,
true,
kind,
nil,
nil,
status,
scale,
),
restOptions,
[]string{"all"},
table,
)
return storage, server
}
// createCustomResource is a helper function that returns a CustomResource with the updated resource version.
func createCustomResource(storage *customresource.REST, cr unstructured.Unstructured, t *testing.T) (unstructured.Unstructured, error) {
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), cr.GetNamespace())
obj, err := storage.Create(ctx, &cr, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
if err != nil {
t.Errorf("Failed to create CustomResource, %v", err)
}
newCR := obj.(*unstructured.Unstructured)
return *newCR, nil
}
func validNewCustomResource() *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "Noxu",
"metadata": map[string]interface{}{
"namespace": "default",
"name": "foo",
"creationTimestamp": time.Now().Add(-time.Hour*12 - 30*time.Minute).UTC().Format(time.RFC3339),
},
"spec": map[string]interface{}{
"replicas": int64(7),
"string": "string",
"float64": float64(3.1415926),
"bool": true,
"stringList": []interface{}{"foo", "bar"},
"mixedList": []interface{}{"foo", int64(42)},
"nonPrimitiveList": []interface{}{"foo", []interface{}{int64(1), int64(2)}},
},
},
}
}
var validCustomResource = *validNewCustomResource()
func TestCreate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
test := registrytest.New(t, storage.CustomResource.Store)
cr := validNewCustomResource()
cr.SetNamespace("")
test.TestCreate(
cr,
)
}
func TestGet(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
test := registrytest.New(t, storage.CustomResource.Store)
test.TestGet(validNewCustomResource())
}
func TestList(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
test := registrytest.New(t, storage.CustomResource.Store)
test.TestList(validNewCustomResource())
}
func TestDelete(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
test := registrytest.New(t, storage.CustomResource.Store)
test.TestDelete(validNewCustomResource())
}
func TestGenerationNumber(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
modifiedRno := *validNewCustomResource()
modifiedRno.SetGeneration(10)
ctx := genericapirequest.NewDefaultContext()
cr, err := createCustomResource(storage.CustomResource, modifiedRno, t)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
etcdCR, err := storage.CustomResource.Get(ctx, cr.GetName(), &metav1.GetOptions{})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
storedCR, _ := etcdCR.(*unstructured.Unstructured)
// Generation initialization
if storedCR.GetGeneration() != 1 {
t.Fatalf("Unexpected generation number %v", storedCR.GetGeneration())
}
// Updates to spec should increment the generation number
setSpecReplicas(storedCR, getSpecReplicas(storedCR)+1)
if _, _, err := storage.CustomResource.Update(ctx, storedCR.GetName(), rest.DefaultUpdatedObjectInfo(storedCR), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Errorf("unexpected error: %v", err)
}
etcdCR, err = storage.CustomResource.Get(ctx, cr.GetName(), &metav1.GetOptions{})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
storedCR, _ = etcdCR.(*unstructured.Unstructured)
if storedCR.GetGeneration() != 2 {
t.Fatalf("Unexpected generation, spec: %v", storedCR.GetGeneration())
}
// Updates to status should not increment the generation number
setStatusReplicas(storedCR, getStatusReplicas(storedCR)+1)
if _, _, err := storage.CustomResource.Update(ctx, storedCR.GetName(), rest.DefaultUpdatedObjectInfo(storedCR), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Errorf("unexpected error: %v", err)
}
etcdCR, err = storage.CustomResource.Get(ctx, cr.GetName(), &metav1.GetOptions{})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
storedCR, _ = etcdCR.(*unstructured.Unstructured)
if storedCR.GetGeneration() != 2 {
t.Fatalf("Unexpected generation, spec: %v", storedCR.GetGeneration())
}
}
func TestCategories(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
expected := []string{"all"}
actual := storage.CustomResource.Categories()
ok := reflect.DeepEqual(actual, expected)
if !ok {
t.Errorf("categories are not equal. expected = %v actual = %v", expected, actual)
}
}
func TestColumns(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/foo"
validCustomResource := validNewCustomResource()
if err := storage.CustomResource.Storage.Create(ctx, key, validCustomResource, nil, 0, false); err != nil {
t.Fatalf("unexpected error: %v", err)
}
gottenList, err := storage.CustomResource.List(ctx, &metainternal.ListOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
tbl, err := storage.CustomResource.ConvertToTable(ctx, gottenList, &metav1beta1.TableOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedColumns := []struct {
Name, Type string
}{
{"Name", "string"},
{"Age", "date"},
{"Replicas", "integer"},
{"Missing", "string"},
{"Invalid", "integer"},
{"String", "string"},
{"StringFloat64", "string"},
{"StringInt64", "string"},
{"StringBool", "string"},
{"Float64", "number"},
{"Bool", "boolean"},
}
if len(tbl.ColumnDefinitions) != len(expectedColumns) {
t.Fatalf("got %d columns, expected %d. Got: %+v", len(tbl.ColumnDefinitions), len(expectedColumns), tbl.ColumnDefinitions)
}
for i, d := range tbl.ColumnDefinitions {
if d.Name != expectedColumns[i].Name {
t.Errorf("got column %d name %q, expected %q", i, d.Name, expectedColumns[i].Name)
}
if d.Type != expectedColumns[i].Type {
t.Errorf("got column %d type %q, expected %q", i, d.Type, expectedColumns[i].Type)
}
}
expectedRows := [][]interface{}{
{
"foo",
"12h",
int64(7),
nil,
nil,
"string",
"3.1415926",
"7",
"true",
float64(3.1415926),
true,
},
}
for i, r := range tbl.Rows {
if !reflect.DeepEqual(r.Cells, expectedRows[i]) {
t.Errorf("got row %d with cells %#v, expected %#v", i, r.Cells, expectedRows[i])
}
}
}
func TestStatusUpdate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/foo"
validCustomResource := validNewCustomResource()
if err := storage.CustomResource.Storage.Create(ctx, key, validCustomResource, nil, 0, false); err != nil {
t.Fatalf("unexpected error: %v", err)
}
gottenObj, err := storage.CustomResource.Get(ctx, "foo", &metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
update := gottenObj.(*unstructured.Unstructured)
updateContent := update.Object
updateContent["status"] = map[string]interface{}{
"replicas": int64(7),
}
if _, _, err := storage.Status.Update(ctx, update.GetName(), rest.DefaultUpdatedObjectInfo(update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
obj, err := storage.CustomResource.Get(ctx, "foo", &metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
cr, ok := obj.(*unstructured.Unstructured)
if !ok {
t.Fatal("unexpected error: custom resource should be of type Unstructured")
}
content := cr.UnstructuredContent()
spec := content["spec"].(map[string]interface{})
status := content["status"].(map[string]interface{})
if spec["replicas"].(int64) != 7 {
t.Errorf("we expected .spec.replicas to not be updated but it was updated to %v", spec["replicas"].(int64))
}
if status["replicas"].(int64) != 7 {
t.Errorf("we expected .status.replicas to be updated to %d but it was %v", 7, status["replicas"].(int64))
}
}
func TestScaleGet(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
name := "foo"
var cr unstructured.Unstructured
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
}
want := &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: cr.GetName(),
Namespace: metav1.NamespaceDefault,
UID: cr.GetUID(),
ResourceVersion: cr.GetResourceVersion(),
CreationTimestamp: cr.GetCreationTimestamp(),
},
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(7),
},
}
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
got := obj.(*autoscalingv1.Scale)
if !apiequality.Semantic.DeepEqual(got, want) {
t.Errorf("unexpected scale: %s", diff.ObjectDiff(got, want))
}
}
func TestScaleGetWithoutSpecReplicas(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
name := "foo"
var cr unstructured.Unstructured
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
withoutSpecReplicas := validCustomResource.DeepCopy()
unstructured.RemoveNestedField(withoutSpecReplicas.Object, "spec", "replicas")
if err := storage.CustomResource.Storage.Create(ctx, key, withoutSpecReplicas, &cr, 0, false); err != nil {
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, withoutSpecReplicas, err)
}
_, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err == nil {
t.Fatalf("error expected for %s", name)
}
if expected := `the spec replicas field ".spec.replicas" does not exist`; !strings.Contains(err.Error(), expected) {
t.Fatalf("expected error string %q, got: %v", expected, err)
}
}
func TestScaleUpdate(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
name := "foo"
var cr unstructured.Unstructured
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0, false); err != nil {
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
}
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
scale, ok := obj.(*autoscalingv1.Scale)
if !ok {
t.Fatalf("%v is not of the type autoscalingv1.Scale", scale)
}
replicas := 12
update := autoscalingv1.Scale{
ObjectMeta: scale.ObjectMeta,
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(replicas),
},
}
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Fatalf("error updating scale %v: %v", update, err)
}
obj, err = storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
scale = obj.(*autoscalingv1.Scale)
if scale.Spec.Replicas != int32(replicas) {
t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
}
update.ResourceVersion = scale.ResourceVersion
update.Spec.Replicas = 15
if _, _, err = storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil && !errors.IsConflict(err) {
t.Fatalf("unexpected error, expecting an update conflict but got %v", err)
}
}
func TestScaleUpdateWithoutSpecReplicas(t *testing.T) {
storage, server := newStorage(t)
defer server.Terminate(t)
defer storage.CustomResource.Store.DestroyFunc()
name := "foo"
var cr unstructured.Unstructured
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
withoutSpecReplicas := validCustomResource.DeepCopy()
unstructured.RemoveNestedField(withoutSpecReplicas.Object, "spec", "replicas")
if err := storage.CustomResource.Storage.Create(ctx, key, withoutSpecReplicas, &cr, 0, false); err != nil {
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, withoutSpecReplicas, err)
}
replicas := 12
update := autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: name,
ResourceVersion: cr.GetResourceVersion(),
},
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(replicas),
},
}
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Fatalf("error updating scale %v: %v", update, err)
}
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("error fetching scale for %s: %v", name, err)
}
scale := obj.(*autoscalingv1.Scale)
if scale.Spec.Replicas != int32(replicas) {
t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
}
}
type unstructuredJsonCodec struct{}
func (c unstructuredJsonCodec) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
obj := into.(*unstructured.Unstructured)
err := obj.UnmarshalJSON(data)
if err != nil {
return nil, nil, err
}
gvk := obj.GroupVersionKind()
return obj, &gvk, nil
}
func (c unstructuredJsonCodec) Encode(obj runtime.Object, w io.Writer) error {
u := obj.(*unstructured.Unstructured)
bs, err := u.MarshalJSON()
if err != nil {
return err
}
w.Write(bs)
return nil
}
func setSpecReplicas(u *unstructured.Unstructured, replicas int64) {
setNestedField(u, replicas, "spec", "replicas")
}
func getSpecReplicas(u *unstructured.Unstructured) int64 {
val, found, err := unstructured.NestedInt64(u.Object, "spec", "replicas")
if !found || err != nil {
return 0
}
return val
}
func setStatusReplicas(u *unstructured.Unstructured, replicas int64) {
setNestedField(u, replicas, "status", "replicas")
}
func getStatusReplicas(u *unstructured.Unstructured) int64 {
val, found, err := unstructured.NestedInt64(u.Object, "status", "replicas")
if !found || err != nil {
return 0
}
return val
}
func setNestedField(u *unstructured.Unstructured, value interface{}, fields ...string) {
if u.Object == nil {
u.Object = make(map[string]interface{})
}
unstructured.SetNestedField(u.Object, value, fields...)
}

View File

@@ -1,59 +0,0 @@
/*
Copyright 2018 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 customresource
import (
"context"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
)
type statusStrategy struct {
customResourceStrategy
}
func NewStatusStrategy(strategy customResourceStrategy) statusStrategy {
return statusStrategy{strategy}
}
func (a statusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
// update is only allowed to set status
newCustomResourceObject := obj.(*unstructured.Unstructured)
newCustomResource := newCustomResourceObject.UnstructuredContent()
status, ok := newCustomResource["status"]
// copy old object into new object
oldCustomResourceObject := old.(*unstructured.Unstructured)
// overridding the resourceVersion in metadata is safe here, we have already checked that
// new object and old object have the same resourceVersion.
*newCustomResourceObject = *oldCustomResourceObject.DeepCopy()
// set status
newCustomResource = newCustomResourceObject.UnstructuredContent()
if ok {
newCustomResource["status"] = status
} else {
delete(newCustomResource, "status")
}
}
// ValidateUpdate is the default update validation for an end user updating status.
func (a statusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return a.customResourceStrategy.validator.ValidateStatusUpdate(ctx, obj, old, a.scale)
}

View File

@@ -1,138 +0,0 @@
/*
Copyright 2018 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 customresource
import (
"context"
"reflect"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestPrepareForUpdate(t *testing.T) {
strategy := statusStrategy{}
tcs := []struct {
old *unstructured.Unstructured
obj *unstructured.Unstructured
expected *unstructured.Unstructured
}{
{
// changes to spec are ignored
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "new",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
},
},
},
{
// changes to other places are also ignored
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"new": "new",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
},
},
},
{
// delete status
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
},
},
},
{
// update status
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": "new",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
"status": "new",
},
},
},
{
// update status and other parts
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "new",
"new": "new",
"status": "new",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": "old",
"status": "new",
},
},
},
}
for index, tc := range tcs {
strategy.PrepareForUpdate(context.TODO(), tc.obj, tc.old)
if !reflect.DeepEqual(tc.obj, tc.expected) {
t.Errorf("test %d failed: expected: %v, got %v", index, tc.expected, tc.obj)
}
}
}

View File

@@ -1,186 +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 customresource
import (
"context"
"github.com/go-openapi/validate"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
apiserverstorage "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
)
// customResourceStrategy implements behavior for CustomResources.
type customResourceStrategy struct {
runtime.ObjectTyper
names.NameGenerator
namespaceScoped bool
validator customResourceValidator
status *apiextensions.CustomResourceSubresourceStatus
scale *apiextensions.CustomResourceSubresourceScale
}
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator *validate.SchemaValidator, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
return customResourceStrategy{
ObjectTyper: typer,
NameGenerator: names.SimpleNameGenerator,
namespaceScoped: namespaceScoped,
status: status,
scale: scale,
validator: customResourceValidator{
namespaceScoped: namespaceScoped,
kind: kind,
schemaValidator: schemaValidator,
statusSchemaValidator: statusSchemaValidator,
},
}
}
func (a customResourceStrategy) NamespaceScoped() bool {
return a.namespaceScoped
}
// PrepareForCreate clears the status of a CustomResource before creation.
func (a customResourceStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && a.status != nil {
customResourceObject := obj.(*unstructured.Unstructured)
customResource := customResourceObject.UnstructuredContent()
// create cannot set status
if _, ok := customResource["status"]; ok {
delete(customResource, "status")
}
}
accessor, _ := meta.Accessor(obj)
accessor.SetGeneration(1)
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (a customResourceStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newCustomResourceObject := obj.(*unstructured.Unstructured)
oldCustomResourceObject := old.(*unstructured.Unstructured)
newCustomResource := newCustomResourceObject.UnstructuredContent()
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
// If the /status subresource endpoint is installed, update is not allowed to set status.
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && a.status != nil {
_, ok1 := newCustomResource["status"]
_, ok2 := oldCustomResource["status"]
switch {
case ok2:
newCustomResource["status"] = oldCustomResource["status"]
case ok1:
delete(newCustomResource, "status")
}
}
// except for the changes to `metadata`, any other changes
// cause the generation to increment.
newCopyContent := copyNonMetadata(newCustomResource)
oldCopyContent := copyNonMetadata(oldCustomResource)
if !apiequality.Semantic.DeepEqual(newCopyContent, oldCopyContent) {
oldAccessor, _ := meta.Accessor(oldCustomResourceObject)
newAccessor, _ := meta.Accessor(newCustomResourceObject)
newAccessor.SetGeneration(oldAccessor.GetGeneration() + 1)
}
}
func copyNonMetadata(original map[string]interface{}) map[string]interface{} {
ret := make(map[string]interface{})
for key, val := range original {
if key == "metadata" {
continue
}
ret[key] = val
}
return ret
}
// Validate validates a new CustomResource.
func (a customResourceStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
return a.validator.Validate(ctx, obj, a.scale)
}
// Canonicalize normalizes the object after validation.
func (customResourceStrategy) Canonicalize(obj runtime.Object) {
}
// AllowCreateOnUpdate is false for CustomResources; this means a POST is
// needed to create one.
func (customResourceStrategy) AllowCreateOnUpdate() bool {
return false
}
// AllowUnconditionalUpdate is the default update policy for CustomResource objects.
func (customResourceStrategy) AllowUnconditionalUpdate() bool {
return false
}
// ValidateUpdate is the default update validation for an end user updating status.
func (a customResourceStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return a.validator.ValidateUpdate(ctx, obj, old, a.scale)
}
// GetAttrs returns labels and fields of a given object for filtering purposes.
func (a customResourceStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
accessor, err := meta.Accessor(obj)
if err != nil {
return nil, nil, false, err
}
return labels.Set(accessor.GetLabels()), objectMetaFieldsSet(accessor, a.namespaceScoped), accessor.GetInitializers() != nil, nil
}
// objectMetaFieldsSet returns a fields that represent the ObjectMeta.
func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields.Set {
if namespaceScoped {
return fields.Set{
"metadata.name": objectMeta.GetName(),
"metadata.namespace": objectMeta.GetNamespace(),
}
}
return fields.Set{
"metadata.name": objectMeta.GetName(),
}
}
// MatchCustomResourceDefinitionStorage is the filter used by the generic etcd backend to route
// watch events from etcd to clients of the apiserver only interested in specific
// labels/fields.
func (a customResourceStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) apiserverstorage.SelectionPredicate {
return apiserverstorage.SelectionPredicate{
Label: label,
Field: field,
GetAttrs: a.GetAttrs,
}
}

View File

@@ -1,257 +0,0 @@
/*
Copyright 2018 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 customresource
import (
"context"
"reflect"
"testing"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func generation1() map[string]interface{} {
return map[string]interface{}{
"generation": int64(1),
}
}
func generation2() map[string]interface{} {
return map[string]interface{}{
"generation": int64(2),
}
}
func TestStrategyPrepareForUpdate(t *testing.T) {
strategy := customResourceStrategy{}
tcs := []struct {
name string
old *unstructured.Unstructured
obj *unstructured.Unstructured
statusEnabled bool
expected *unstructured.Unstructured
}{
{
name: "/status is enabled, spec changes increment generation",
statusEnabled: true,
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "new",
"status": "old",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation2(),
"spec": "new",
"status": "old",
},
},
},
{
name: "/status is enabled, status changes do not increment generation, status changes removed",
statusEnabled: true,
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"status": "new",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"status": "old",
},
},
},
{
name: "/status is enabled, metadata changes do not increment generation",
statusEnabled: true,
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"generation": int64(1),
"other": "old",
},
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"generation": int64(1),
"other": "new",
},
"spec": "old",
"status": "old",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"generation": int64(1),
"other": "new",
},
"spec": "old",
"status": "old",
},
},
},
{
name: "/status is disabled, spec changes increment generation",
statusEnabled: false,
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "new",
"status": "old",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation2(),
"spec": "new",
"status": "old",
},
},
},
{
name: "/status is disabled, status changes increment generation",
statusEnabled: false,
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"status": "new",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation2(),
"spec": "old",
"status": "new",
},
},
},
{
name: "/status is disabled, other top-level field changes increment generation",
statusEnabled: false,
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"image": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation1(),
"spec": "old",
"image": "new",
"status": "old",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": generation2(),
"spec": "old",
"image": "new",
"status": "old",
},
},
},
{
name: "/status is disabled, metadata changes do not increment generation",
statusEnabled: false,
old: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"generation": int64(1),
"other": "old",
},
"spec": "old",
"status": "old",
},
},
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"generation": int64(1),
"other": "new",
},
"spec": "old",
"status": "old",
},
},
expected: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"generation": int64(1),
"other": "new",
},
"spec": "old",
"status": "old",
},
},
},
}
for _, tc := range tcs {
if tc.statusEnabled {
strategy.status = &apiextensions.CustomResourceSubresourceStatus{}
} else {
strategy.status = nil
}
strategy.PrepareForUpdate(context.TODO(), tc.obj, tc.old)
if !reflect.DeepEqual(tc.obj, tc.expected) {
t.Errorf("test %q failed: expected: %v, got %v", tc.name, tc.expected, tc.obj)
}
}
}

View File

@@ -1,172 +0,0 @@
/*
Copyright 2018 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 tableconvertor
import (
"bytes"
"context"
"encoding/json"
"fmt"
"reflect"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apimachinery/pkg/api/meta"
metatable "k8s.io/apimachinery/pkg/api/meta/table"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/client-go/util/jsonpath"
)
var swaggerMetadataDescriptions = metav1.ObjectMeta{}.SwaggerDoc()
// New creates a new table convertor for the provided CRD column definition. If the printer definition cannot be parsed,
// error will be returned along with a default table convertor.
func New(crdColumns []apiextensions.CustomResourceColumnDefinition) (rest.TableConvertor, error) {
headers := []metav1beta1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: swaggerMetadataDescriptions["name"]},
}
c := &convertor{
headers: headers,
}
for _, col := range crdColumns {
path := jsonpath.New(col.Name)
if err := path.Parse(fmt.Sprintf("{%s}", col.JSONPath)); err != nil {
return c, fmt.Errorf("unrecognized column definition %q", col.JSONPath)
}
path.AllowMissingKeys(true)
desc := fmt.Sprintf("Custom resource definition column (in JSONPath format): %s", col.JSONPath)
if len(col.Description) > 0 {
desc = col.Description
}
c.additionalColumns = append(c.additionalColumns, path)
c.headers = append(c.headers, metav1beta1.TableColumnDefinition{
Name: col.Name,
Type: col.Type,
Format: col.Format,
Description: desc,
Priority: col.Priority,
})
}
return c, nil
}
type convertor struct {
headers []metav1beta1.TableColumnDefinition
additionalColumns []*jsonpath.JSONPath
}
func (c *convertor) ConvertToTable(ctx context.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1beta1.Table, error) {
table := &metav1beta1.Table{
ColumnDefinitions: c.headers,
}
if m, err := meta.ListAccessor(obj); err == nil {
table.ResourceVersion = m.GetResourceVersion()
table.SelfLink = m.GetSelfLink()
table.Continue = m.GetContinue()
} else {
if m, err := meta.CommonAccessor(obj); err == nil {
table.ResourceVersion = m.GetResourceVersion()
table.SelfLink = m.GetSelfLink()
}
}
var err error
buf := &bytes.Buffer{}
table.Rows, err = metatable.MetaToTableRow(obj, func(obj runtime.Object, m metav1.Object, name, age string) ([]interface{}, error) {
cells := make([]interface{}, 1, 1+len(c.additionalColumns))
cells[0] = name
customHeaders := c.headers[1:]
for i, column := range c.additionalColumns {
results, err := column.FindResults(obj.(runtime.Unstructured).UnstructuredContent())
if err != nil || len(results) == 0 || len(results[0]) == 0 {
cells = append(cells, nil)
continue
}
// as we only support simple JSON path, we can assume to have only one result (or none, filtered out above)
value := results[0][0].Interface()
if customHeaders[i].Type == "string" {
if err := column.PrintResults(buf, []reflect.Value{reflect.ValueOf(value)}); err == nil {
cells = append(cells, buf.String())
buf.Reset()
} else {
cells = append(cells, nil)
}
} else {
cells = append(cells, cellForJSONValue(customHeaders[i].Type, value))
}
}
return cells, nil
})
return table, err
}
func cellForJSONValue(headerType string, value interface{}) interface{} {
if value == nil {
return nil
}
switch headerType {
case "integer":
switch typed := value.(type) {
case int64:
return typed
case float64:
return int64(typed)
case json.Number:
if i64, err := typed.Int64(); err == nil {
return i64
}
}
case "number":
switch typed := value.(type) {
case int64:
return float64(typed)
case float64:
return typed
case json.Number:
if f, err := typed.Float64(); err == nil {
return f
}
}
case "boolean":
if b, ok := value.(bool); ok {
return b
}
case "string":
if s, ok := value.(string); ok {
return s
}
case "date":
if typed, ok := value.(string); ok {
var timestamp metav1.Time
err := timestamp.UnmarshalQueryParameter(typed)
if err != nil {
return "<invalid>"
}
return metatable.ConvertToHumanReadableDateType(timestamp)
}
}
return nil
}

View File

@@ -1,68 +0,0 @@
/*
Copyright 2018 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 tableconvertor
import (
"fmt"
"reflect"
"testing"
"time"
)
func Test_cellForJSONValue(t *testing.T) {
tests := []struct {
headerType string
value interface{}
want interface{}
}{
{"integer", int64(42), int64(42)},
{"integer", float64(3.14), int64(3)},
{"integer", true, nil},
{"integer", "foo", nil},
{"number", int64(42), float64(42)},
{"number", float64(3.14), float64(3.14)},
{"number", true, nil},
{"number", "foo", nil},
{"boolean", int64(42), nil},
{"boolean", float64(3.14), nil},
{"boolean", true, true},
{"boolean", "foo", nil},
{"string", int64(42), nil},
{"string", float64(3.14), nil},
{"string", true, nil},
{"string", "foo", "foo"},
{"date", int64(42), nil},
{"date", float64(3.14), nil},
{"date", true, nil},
{"date", time.Now().Add(-time.Hour*12 - 30*time.Minute).UTC().Format(time.RFC3339), "12h"},
{"date", time.Now().Add(+time.Hour*12 + 30*time.Minute).UTC().Format(time.RFC3339), "<invalid>"},
{"date", "", "<unknown>"},
{"unknown", "foo", nil},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%#v of type %s", tt.value, tt.headerType), func(t *testing.T) {
if got := cellForJSONValue(tt.headerType, tt.value); !reflect.DeepEqual(got, tt.want) {
t.Errorf("cellForJSONValue() = %#v, want %#v", got, tt.want)
}
})
}
}

View File

@@ -1,195 +0,0 @@
/*
Copyright 2018 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 customresource
import (
"context"
"fmt"
"math"
"strings"
"github.com/go-openapi/validate"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
)
type customResourceValidator struct {
namespaceScoped bool
kind schema.GroupVersionKind
schemaValidator *validate.SchemaValidator
statusSchemaValidator *validate.SchemaValidator
}
func (a customResourceValidator) Validate(ctx context.Context, obj runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
}
accessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
if errs := a.ValidateTypeMeta(ctx, u); len(errs) > 0 {
return errs
}
var allErrs field.ErrorList
allErrs = append(allErrs, validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))...)
if err = apiservervalidation.ValidateCustomResource(u.UnstructuredContent(), a.schemaValidator); err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
}
allErrs = append(allErrs, a.ValidateScaleSpec(ctx, u, scale)...)
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
return allErrs
}
func (a customResourceValidator) ValidateUpdate(ctx context.Context, obj, old runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
}
objAccessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
oldAccessor, err := meta.Accessor(old)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
if errs := a.ValidateTypeMeta(ctx, u); len(errs) > 0 {
return errs
}
var allErrs field.ErrorList
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
if err = apiservervalidation.ValidateCustomResource(u.UnstructuredContent(), a.schemaValidator); err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
}
allErrs = append(allErrs, a.ValidateScaleSpec(ctx, u, scale)...)
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
return allErrs
}
func (a customResourceValidator) ValidateStatusUpdate(ctx context.Context, obj, old runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
u, ok := obj.(*unstructured.Unstructured)
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), u, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", u))}
}
objAccessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
oldAccessor, err := meta.Accessor(old)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
if errs := a.ValidateTypeMeta(ctx, u); len(errs) > 0 {
return errs
}
var allErrs field.ErrorList
allErrs = append(allErrs, validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))...)
if err = apiservervalidation.ValidateCustomResource(u.UnstructuredContent(), a.schemaValidator); err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath(""), u.UnstructuredContent(), err.Error()))
}
allErrs = append(allErrs, a.ValidateScaleStatus(ctx, u, scale)...)
return allErrs
}
func (a customResourceValidator) ValidateTypeMeta(ctx context.Context, obj *unstructured.Unstructured) field.ErrorList {
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
var allErrs field.ErrorList
if typeAccessor.GetKind() != a.kind.Kind {
allErrs = append(allErrs, field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind)))
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
allErrs = append(allErrs, field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version)))
}
return allErrs
}
func (a customResourceValidator) ValidateScaleSpec(ctx context.Context, obj *unstructured.Unstructured, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
if scale == nil {
return nil
}
var allErrs field.ErrorList
// validate specReplicas
specReplicasPath := strings.TrimPrefix(scale.SpecReplicasPath, ".") // ignore leading period
specReplicas, _, err := unstructured.NestedInt64(obj.UnstructuredContent(), strings.Split(specReplicasPath, ".")...)
if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, err.Error()))
} else if specReplicas < 0 {
allErrs = append(allErrs, field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, "should be a non-negative integer"))
} else if specReplicas > math.MaxInt32 {
allErrs = append(allErrs, field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32)))
}
return allErrs
}
func (a customResourceValidator) ValidateScaleStatus(ctx context.Context, obj *unstructured.Unstructured, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
if scale == nil {
return nil
}
var allErrs field.ErrorList
// validate statusReplicas
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
statusReplicas, _, err := unstructured.NestedInt64(obj.UnstructuredContent(), strings.Split(statusReplicasPath, ".")...)
if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error()))
} else if statusReplicas < 0 {
allErrs = append(allErrs, field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer"))
} else if statusReplicas > math.MaxInt32 {
allErrs = append(allErrs, field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32)))
}
// validate labelSelector
if scale.LabelSelectorPath != nil {
labelSelectorPath := strings.TrimPrefix(*scale.LabelSelectorPath, ".") // ignore leading period
labelSelector, _, err := unstructured.NestedString(obj.UnstructuredContent(), strings.Split(labelSelectorPath, ".")...)
if err != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error()))
}
}
return allErrs
}

View File

@@ -1,181 +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 customresourcedefinition
import (
"context"
"fmt"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage"
storageerr "k8s.io/apiserver/pkg/storage/errors"
"k8s.io/apiserver/pkg/util/dryrun"
)
// rest implements a RESTStorage for API services against etcd
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against API services.
func NewREST(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) *REST {
strategy := NewStrategy(scheme)
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &apiextensions.CustomResourceDefinition{} },
NewListFunc: func() runtime.Object { return &apiextensions.CustomResourceDefinitionList{} },
PredicateFunc: MatchCustomResourceDefinition,
DefaultQualifiedResource: apiextensions.Resource("customresourcedefinitions"),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
panic(err) // TODO: Propagate error up
}
return &REST{store}
}
// Implement ShortNamesProvider
var _ rest.ShortNamesProvider = &REST{}
// ShortNames implements the ShortNamesProvider interface. Returns a list of short names for a resource.
func (r *REST) ShortNames() []string {
return []string{"crd", "crds"}
}
// Delete adds the CRD finalizer to the list
func (r *REST) Delete(ctx context.Context, name string, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
obj, err := r.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
crd := obj.(*apiextensions.CustomResourceDefinition)
// Ensure we have a UID precondition
if options == nil {
options = metav1.NewDeleteOptions(0)
}
if options.Preconditions == nil {
options.Preconditions = &metav1.Preconditions{}
}
if options.Preconditions.UID == nil {
options.Preconditions.UID = &crd.UID
} else if *options.Preconditions.UID != crd.UID {
err = apierrors.NewConflict(
apiextensions.Resource("customresourcedefinitions"),
name,
fmt.Errorf("Precondition failed: UID in precondition: %v, UID in object meta: %v", *options.Preconditions.UID, crd.UID),
)
return nil, false, err
}
// upon first request to delete, add our finalizer and then delegate
if crd.DeletionTimestamp.IsZero() {
key, err := r.Store.KeyFunc(ctx, name)
if err != nil {
return nil, false, err
}
preconditions := storage.Preconditions{UID: options.Preconditions.UID}
out := r.Store.NewFunc()
err = r.Store.Storage.GuaranteedUpdate(
ctx, key, out, false, &preconditions,
storage.SimpleUpdate(func(existing runtime.Object) (runtime.Object, error) {
existingCRD, ok := existing.(*apiextensions.CustomResourceDefinition)
if !ok {
// wrong type
return nil, fmt.Errorf("expected *apiextensions.CustomResourceDefinition, got %v", existing)
}
// Set the deletion timestamp if needed
if existingCRD.DeletionTimestamp.IsZero() {
now := metav1.Now()
existingCRD.DeletionTimestamp = &now
}
if !apiextensions.CRDHasFinalizer(existingCRD, apiextensions.CustomResourceCleanupFinalizer) {
existingCRD.Finalizers = append(existingCRD.Finalizers, apiextensions.CustomResourceCleanupFinalizer)
}
// update the status condition too
apiextensions.SetCRDCondition(existingCRD, apiextensions.CustomResourceDefinitionCondition{
Type: apiextensions.Terminating,
Status: apiextensions.ConditionTrue,
Reason: "InstanceDeletionPending",
Message: "CustomResourceDefinition marked for deletion; CustomResource deletion will begin soon",
})
return existingCRD, nil
}),
dryrun.IsDryRun(options.DryRun),
)
if err != nil {
err = storageerr.InterpretGetError(err, apiextensions.Resource("customresourcedefinitions"), name)
err = storageerr.InterpretUpdateError(err, apiextensions.Resource("customresourcedefinitions"), name)
if _, ok := err.(*apierrors.StatusError); !ok {
err = apierrors.NewInternalError(err)
}
return nil, false, err
}
return out, false, nil
}
return r.Store.Delete(ctx, name, options)
}
// NewStatusREST makes a RESTStorage for status that has more limited options.
// It is based on the original REST so that we can share the same underlying store
func NewStatusREST(scheme *runtime.Scheme, rest *REST) *StatusREST {
statusStore := *rest.Store
statusStore.CreateStrategy = nil
statusStore.DeleteStrategy = nil
statusStore.UpdateStrategy = NewStatusStrategy(scheme)
return &StatusREST{store: &statusStore}
}
type StatusREST struct {
store *genericregistry.Store
}
var _ = rest.Patcher(&StatusREST{})
func (r *StatusREST) New() runtime.Object {
return &apiextensions.CustomResourceDefinition{}
}
// Get retrieves the object from the storage. It is required to support Patch.
func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return r.store.Get(ctx, name, options)
}
// Update alters the status subset of an object.
func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
// We are explicitly setting forceAllowCreate to false in the call to the underlying storage because
// subresources should never allow create on update.
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options)
}

View File

@@ -1,261 +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 customresourcedefinition
import (
"context"
"fmt"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
)
// strategy implements behavior for CustomResources.
type strategy struct {
runtime.ObjectTyper
names.NameGenerator
}
func NewStrategy(typer runtime.ObjectTyper) strategy {
return strategy{typer, names.SimpleNameGenerator}
}
func (strategy) NamespaceScoped() bool {
return false
}
// PrepareForCreate clears the status of a CustomResourceDefinition before creation.
func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
crd := obj.(*apiextensions.CustomResourceDefinition)
crd.Status = apiextensions.CustomResourceDefinitionStatus{}
crd.Generation = 1
// if the feature gate is disabled, drop the feature.
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
crd.Spec.Validation = nil
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Schema = nil
}
}
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
crd.Spec.Subresources = nil
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Subresources = nil
}
}
// On CREATE, if the CustomResourceWebhookConversion feature gate is off, we auto-clear
// the per-version fields. This is to be consistent with the other built-in types, as the
// apiserver drops unknown fields.
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) {
for i := range crd.Spec.Versions {
crd.Spec.Versions[i].Schema = nil
crd.Spec.Versions[i].Subresources = nil
crd.Spec.Versions[i].AdditionalPrinterColumns = nil
}
}
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) && crd.Spec.Conversion != nil {
crd.Spec.Conversion.WebhookClientConfig = nil
}
for _, v := range crd.Spec.Versions {
if v.Storage {
if !apiextensions.IsStoredVersion(crd, v.Name) {
crd.Status.StoredVersions = append(crd.Status.StoredVersions, v.Name)
}
break
}
}
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newCRD := obj.(*apiextensions.CustomResourceDefinition)
oldCRD := old.(*apiextensions.CustomResourceDefinition)
newCRD.Status = oldCRD.Status
// Any changes to the spec increment the generation number, any changes to the
// status should reflect the generation number of the corresponding object. We push
// the burden of managing the status onto the clients because we can't (in general)
// know here what version of spec the writer of the status has seen. It may seem like
// we can at first -- since obj contains spec -- but in the future we will probably make
// status its own object, and even if we don't, writes may be the result of a
// read-update-write loop, so the contents of spec may not actually be the spec that
// the controller has *seen*.
if !apiequality.Semantic.DeepEqual(oldCRD.Spec, newCRD.Spec) {
newCRD.Generation = oldCRD.Generation + 1
}
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
newCRD.Spec.Validation = nil
oldCRD.Spec.Validation = nil
for i := range newCRD.Spec.Versions {
newCRD.Spec.Versions[i].Schema = nil
}
for i := range oldCRD.Spec.Versions {
oldCRD.Spec.Versions[i].Schema = nil
}
}
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
newCRD.Spec.Subresources = nil
oldCRD.Spec.Subresources = nil
for i := range newCRD.Spec.Versions {
newCRD.Spec.Versions[i].Subresources = nil
}
for i := range oldCRD.Spec.Versions {
oldCRD.Spec.Versions[i].Subresources = nil
}
}
// On UPDATE, if the CustomResourceWebhookConversion feature gate is off, we auto-clear
// the per-version fields if the old CRD doesn't use per-version fields already.
// This is to be consistent with the other built-in types, as the apiserver drops unknown
// fields. If the old CRD already uses per-version fields, the CRD is allowed to continue
// use per-version fields.
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) &&
!hasPerVersionField(oldCRD.Spec.Versions) {
for i := range newCRD.Spec.Versions {
newCRD.Spec.Versions[i].Schema = nil
newCRD.Spec.Versions[i].Subresources = nil
newCRD.Spec.Versions[i].AdditionalPrinterColumns = nil
}
}
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceWebhookConversion) && newCRD.Spec.Conversion != nil {
if oldCRD.Spec.Conversion == nil || newCRD.Spec.Conversion.WebhookClientConfig == nil {
newCRD.Spec.Conversion.WebhookClientConfig = nil
}
}
for _, v := range newCRD.Spec.Versions {
if v.Storage {
if !apiextensions.IsStoredVersion(newCRD, v.Name) {
newCRD.Status.StoredVersions = append(newCRD.Status.StoredVersions, v.Name)
}
break
}
}
}
// hasPerVersionField returns true if a CRD uses per-version schema/subresources/columns fields.
func hasPerVersionField(versions []apiextensions.CustomResourceDefinitionVersion) bool {
for _, v := range versions {
if v.Schema != nil || v.Subresources != nil || len(v.AdditionalPrinterColumns) > 0 {
return true
}
}
return false
}
// Validate validates a new CustomResourceDefinition.
func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition))
}
// AllowCreateOnUpdate is false for CustomResourceDefinition; this means a POST is
// needed to create one.
func (strategy) AllowCreateOnUpdate() bool {
return false
}
// AllowUnconditionalUpdate is the default update policy for CustomResourceDefinition objects.
func (strategy) AllowUnconditionalUpdate() bool {
return false
}
// Canonicalize normalizes the object after validation.
func (strategy) Canonicalize(obj runtime.Object) {
}
// ValidateUpdate is the default update validation for an end user updating status.
func (strategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
}
type statusStrategy struct {
runtime.ObjectTyper
names.NameGenerator
}
func NewStatusStrategy(typer runtime.ObjectTyper) statusStrategy {
return statusStrategy{typer, names.SimpleNameGenerator}
}
func (statusStrategy) NamespaceScoped() bool {
return false
}
func (statusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newObj := obj.(*apiextensions.CustomResourceDefinition)
oldObj := old.(*apiextensions.CustomResourceDefinition)
newObj.Spec = oldObj.Spec
// Status updates are for only for updating status, not objectmeta.
// TODO: Update after ResetObjectMetaForStatus is added to meta/v1.
newObj.Labels = oldObj.Labels
newObj.Annotations = oldObj.Annotations
newObj.OwnerReferences = oldObj.OwnerReferences
newObj.Generation = oldObj.Generation
newObj.SelfLink = oldObj.SelfLink
}
func (statusStrategy) AllowCreateOnUpdate() bool {
return false
}
func (statusStrategy) AllowUnconditionalUpdate() bool {
return false
}
func (statusStrategy) Canonicalize(obj runtime.Object) {
}
func (statusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateUpdateCustomResourceDefinitionStatus(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
}
// GetAttrs returns labels and fields of a given object for filtering purposes.
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
apiserver, ok := obj.(*apiextensions.CustomResourceDefinition)
if !ok {
return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition")
}
return labels.Set(apiserver.ObjectMeta.Labels), CustomResourceDefinitionToSelectableFields(apiserver), apiserver.Initializers != nil, nil
}
// MatchCustomResourceDefinition is the filter used by the generic etcd backend to watch events
// from etcd to clients of the apiserver only interested in specific labels/fields.
func MatchCustomResourceDefinition(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
return storage.SelectionPredicate{
Label: label,
Field: field,
GetAttrs: GetAttrs,
}
}
// CustomResourceDefinitionToSelectableFields returns a field set that represents the object.
func CustomResourceDefinitionToSelectableFields(obj *apiextensions.CustomResourceDefinition) fields.Set {
return generic.ObjectMetaFieldsSet(&obj.ObjectMeta, true)
}