add subresources for custom resources

This commit is contained in:
Nikhita Raghunath 2017-11-06 18:19:15 +05:30
parent 66a4e5122a
commit 6fbe8157e3
28 changed files with 2584 additions and 221 deletions

View File

@ -24,7 +24,7 @@ import (
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
discocache "k8s.io/client-go/discovery/cached" // Saturday Night Fever discocache "k8s.io/client-go/discovery/cached"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/scale" "k8s.io/client-go/scale"
"k8s.io/kubernetes/pkg/controller/podautoscaler" "k8s.io/kubernetes/pkg/controller/podautoscaler"

View File

@ -477,7 +477,6 @@ staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server
staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer
staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status
staging/src/k8s.io/apiextensions-apiserver/pkg/features staging/src/k8s.io/apiextensions-apiserver/pkg/features
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition
staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver
staging/src/k8s.io/apimachinery/pkg/api/meta staging/src/k8s.io/apimachinery/pkg/api/meta

View File

@ -297,7 +297,8 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
// inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed // inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side: // unintentionally on either side:
apiextensionsfeatures.CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta}, apiextensionsfeatures.CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
apiextensionsfeatures.CustomResourceSubresources: {Default: false, PreRelease: utilfeature.Alpha},
// features that enable backwards compatibility but are scheduled to be removed // features that enable backwards compatibility but are scheduled to be removed
ServiceProxyAllowExternalIPs: {Default: false, PreRelease: utilfeature.Deprecated}, ServiceProxyAllowExternalIPs: {Default: false, PreRelease: utilfeature.Deprecated},

View File

@ -16,7 +16,9 @@ limitations under the License.
package apiextensions package apiextensions
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CustomResourceDefinitionSpec describes how a user wants their resource to appear // CustomResourceDefinitionSpec describes how a user wants their resource to appear
type CustomResourceDefinitionSpec struct { type CustomResourceDefinitionSpec struct {
@ -30,6 +32,8 @@ type CustomResourceDefinitionSpec struct {
Scope ResourceScope Scope ResourceScope
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
Validation *CustomResourceValidation Validation *CustomResourceValidation
// Subresources describes the subresources for CustomResources
Subresources *CustomResourceSubresources
} }
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition // CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
@ -146,3 +150,41 @@ type CustomResourceValidation struct {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against. // OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
OpenAPIV3Schema *JSONSchemaProps OpenAPIV3Schema *JSONSchemaProps
} }
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
type CustomResourceSubresources struct {
// Status denotes the status subresource for CustomResources
Status *CustomResourceSubresourceStatus
// Scale denotes the scale subresource for CustomResources
Scale *CustomResourceSubresourceScale
}
// 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
type CustomResourceSubresourceStatus struct{}
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
type CustomResourceSubresourceScale struct {
// 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.
SpecReplicasPath string
// 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.
StatusReplicasPath string
// 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
LabelSelectorPath *string
}

View File

@ -16,7 +16,9 @@ limitations under the License.
package v1beta1 package v1beta1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CustomResourceDefinitionSpec describes how a user wants their resource to appear // CustomResourceDefinitionSpec describes how a user wants their resource to appear
type CustomResourceDefinitionSpec struct { type CustomResourceDefinitionSpec struct {
@ -31,6 +33,11 @@ type CustomResourceDefinitionSpec struct {
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
// +optional // +optional
Validation *CustomResourceValidation `json:"validation,omitempty" protobuf:"bytes,5,opt,name=validation"` Validation *CustomResourceValidation `json:"validation,omitempty" protobuf:"bytes,5,opt,name=validation"`
// Subresources describes the subresources for CustomResources
// This field is alpha-level and should only be sent to servers that enable
// subresources via the CustomResourceSubresources feature gate.
// +optional
Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,6,opt,name=subresources"`
} }
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition // CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
@ -147,3 +154,41 @@ type CustomResourceValidation struct {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against. // OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
OpenAPIV3Schema *JSONSchemaProps `json:"openAPIV3Schema,omitempty" protobuf:"bytes,1,opt,name=openAPIV3Schema"` OpenAPIV3Schema *JSONSchemaProps `json:"openAPIV3Schema,omitempty" protobuf:"bytes,1,opt,name=openAPIV3Schema"`
} }
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
type CustomResourceSubresources struct {
// Status denotes the status subresource for CustomResources
Status *CustomResourceSubresourceStatus `json:"status,omitempty" protobuf:"bytes,1,opt,name=status"`
// Scale denotes the scale subresource for CustomResources
Scale *CustomResourceSubresourceScale `json:"scale,omitempty" protobuf:"bytes,2,opt,name=scale"`
}
// 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
type CustomResourceSubresourceStatus struct{}
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
type CustomResourceSubresourceScale struct {
// 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.
SpecReplicasPath string `json:"specReplicasPath" protobuf:"bytes,1,name=specReplicasPath"`
// 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.
StatusReplicasPath string `json:"statusReplicasPath" protobuf:"bytes,2,opt,name=statusReplicasPath"`
// 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
LabelSelectorPath *string `json:"labelSelectorPath,omitempty" protobuf:"bytes,3,opt,name=labelSelectorPath"`
}

View File

@ -18,6 +18,7 @@ package validation
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
genericvalidation "k8s.io/apimachinery/pkg/api/validation" genericvalidation "k8s.io/apimachinery/pkg/api/validation"
@ -107,7 +108,13 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, fldPath.Child("validation"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, fldPath.Child("validation"))...)
} else if spec.Validation != nil { } else if spec.Validation != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate")) 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"))
} }
return allErrs return allErrs
@ -182,9 +189,27 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
return allErrs return allErrs
} }
if customResourceValidation.OpenAPIV3Schema != nil { if schema := customResourceValidation.OpenAPIV3Schema; schema != nil {
// if subresources are enabled, only properties is allowed inside the root schema
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
v := reflect.ValueOf(schema).Elem()
fieldsPresent := 0
for i := 0; i < v.NumField(); i++ {
field := v.Field(i).Interface()
if !reflect.DeepEqual(field, reflect.Zero(reflect.TypeOf(field)).Interface()) {
fieldsPresent++
}
}
if fieldsPresent > 1 || (fieldsPresent == 1 && v.FieldByName("Properties").IsNil()) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), *schema, fmt.Sprintf("if subresources for custom resources are enabled, only properties can be used at the root of the schema")))
return allErrs
}
}
openAPIV3Schema := &specStandardValidatorV3{} openAPIV3Schema := &specStandardValidatorV3{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(customResourceValidation.OpenAPIV3Schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...) 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 validation passed otherwise, make sure we can actually construct a schema validator from this custom resource validation.
@ -326,3 +351,64 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
return allErrs 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
}

View File

@ -22,6 +22,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
autoscaling "k8s.io/api/autoscaling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@ -117,6 +118,26 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
Verbs: verbs, Verbs: verbs,
ShortNames: crd.Status.AcceptedNames.ShortNames, ShortNames: crd.Status.AcceptedNames.ShortNames,
}) })
if crd.Spec.Subresources != nil && crd.Spec.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 crd.Spec.Subresources != nil && crd.Spec.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 { if !foundGroup {

View File

@ -25,6 +25,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/golang/glog" "github.com/golang/glog"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
@ -35,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "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/json"
"k8s.io/apimachinery/pkg/runtime/serializer/versioning" "k8s.io/apimachinery/pkg/runtime/serializer/versioning"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
@ -47,7 +51,10 @@ import (
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/apiserver/pkg/storage/storagebackend"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/client-go/scale"
"k8s.io/client-go/scale/scheme/autoscalingv1"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
@ -55,6 +62,7 @@ import (
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion" informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion" listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer" "k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource" "k8s.io/apiextensions-apiserver/pkg/registry/customresource"
) )
@ -87,8 +95,11 @@ type crdInfo struct {
spec *apiextensions.CustomResourceDefinitionSpec spec *apiextensions.CustomResourceDefinitionSpec
acceptedNames *apiextensions.CustomResourceDefinitionNames acceptedNames *apiextensions.CustomResourceDefinitionNames
storage *customresource.REST storage customresource.CustomResourceStorage
requestScope handlers.RequestScope
requestScope handlers.RequestScope
scaleRequestScope handlers.RequestScope
statusRequestScope handlers.RequestScope
} }
// crdStorageMap goes from customresourcedefinition to its storage // crdStorageMap goes from customresourcedefinition to its storage
@ -172,10 +183,6 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.delegate.ServeHTTP(w, req) r.delegate.ServeHTTP(w, req)
return return
} }
if len(requestInfo.Subresource) > 0 {
http.NotFound(w, req)
return
}
terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating) terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating)
@ -185,61 +192,126 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
storage := crdInfo.storage
requestScope := crdInfo.requestScope
minRequestTimeout := 1 * time.Minute
verb := strings.ToUpper(requestInfo.Verb) verb := strings.ToUpper(requestInfo.Verb)
resource := requestInfo.Resource resource := requestInfo.Resource
subresource := requestInfo.Subresource subresource := requestInfo.Subresource
scope := metrics.CleanScope(requestInfo) scope := metrics.CleanScope(requestInfo)
supportedTypes := []string{
string(types.JSONPatchType),
string(types.MergePatchType),
}
var handler http.HandlerFunc var handler http.HandlerFunc
switch {
case subresource == "status" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil:
handler = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case subresource == "scale" && crd.Spec.Subresources != nil && crd.Spec.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.requestScope
storage := crdInfo.storage.CustomResource
minRequestTimeout := 1 * time.Minute
switch requestInfo.Verb { switch requestInfo.Verb {
case "get": case "get":
handler = handlers.GetResource(storage, storage, requestScope) return handlers.GetResource(storage, storage, requestScope)
case "list": case "list":
forceWatch := false forceWatch := false
handler = handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout) return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
case "watch": case "watch":
forceWatch := true forceWatch := true
handler = handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout) return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
case "create": case "create":
if terminating { if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return return nil
} }
handler = handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission) return handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "update": case "update":
if terminating { if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return return nil
} }
handler = handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission) return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch": case "patch":
if terminating { if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return return nil
} }
supportedTypes := []string{ return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
string(types.JSONPatchType),
string(types.MergePatchType),
}
handler = handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
case "delete": case "delete":
allowsOptions := true allowsOptions := true
handler = handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission) return handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
case "deletecollection": case "deletecollection":
checkBody := true checkBody := true
handler = handlers.DeleteCollection(storage, checkBody, requestScope, r.admission) return handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
default: default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed) http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return 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.statusRequestScope
storage := crdInfo.storage.Status
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, nil, requestScope)
case "update":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, 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.scaleRequestScope
storage := crdInfo.storage.Scale
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, nil, requestScope)
case "update":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
} }
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
handler(w, req)
return
} }
func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) { func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) {
@ -265,7 +337,8 @@ func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{})
// as it is used without locking elsewhere. // as it is used without locking elsewhere.
storageMap2 := storageMap.clone() storageMap2 := storageMap.clone()
if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok { if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok {
oldInfo.storage.DestroyFunc() // destroy only the main storage. Those for the subresources share cacher and etcd clients.
oldInfo.storage.CustomResource.DestroyFunc()
delete(storageMap2, types.UID(oldCRD.UID)) delete(storageMap2, types.UID(oldCRD.UID))
} }
@ -297,7 +370,8 @@ func (r *crdHandler) removeDeadStorage() {
} }
if !found { if !found {
glog.V(4).Infof("Removing dead CRD storage for %v", s.requestScope.Resource) glog.V(4).Infof("Removing dead CRD storage for %v", s.requestScope.Resource)
s.storage.DestroyFunc() // destroy only the main storage. Those for the subresources share cacher and etcd clients.
s.storage.CustomResource.DestroyFunc()
delete(storageMap2, uid) delete(storageMap2, uid)
} }
} }
@ -311,7 +385,7 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions
if err != nil { if err != nil {
return nil, err return nil, err
} }
return info.storage, nil return info.storage.CustomResource, nil
} }
func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) { func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) {
@ -340,9 +414,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
parameterCodec := runtime.NewParameterCodec(parameterScheme) parameterCodec := runtime.NewParameterCodec(parameterScheme)
kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.Kind} kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.Kind}
typer := unstructuredObjectTyper{ typer := UnstructuredObjectTyper{
delegate: parameterScheme, Delegate: parameterScheme,
unstructuredTyper: discovery.NewUnstructuredObjectTyper(nil), UnstructuredTyper: discovery.NewUnstructuredObjectTyper(nil),
} }
creator := unstructuredCreator{} creator := unstructuredCreator{}
@ -351,7 +425,29 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
return nil, err return nil, err
} }
storage := customresource.NewREST( var statusSpec *apiextensions.CustomResourceSubresourceStatus
var statusValidator *validate.SchemaValidator
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
statusSpec = crd.Spec.Subresources.Status
// for the status subresource, validate only against the status schema
if crd.Spec.Validation != nil && crd.Spec.Validation.OpenAPIV3Schema != nil && crd.Spec.Validation.OpenAPIV3Schema.Properties != nil {
if statusSchema, ok := crd.Spec.Validation.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) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
scaleSpec = crd.Spec.Subresources.Scale
}
customResourceStorage := customresource.NewStorage(
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural}, schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural},
schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.ListKind}, schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.ListKind},
customresource.NewStrategy( customresource.NewStrategy(
@ -359,6 +455,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
crd.Spec.Scope == apiextensions.NamespaceScoped, crd.Spec.Scope == apiextensions.NamespaceScoped,
kind, kind,
validator, validator,
statusValidator,
statusSpec,
scaleSpec,
), ),
r.restOptionsGetter, r.restOptionsGetter,
) )
@ -373,12 +472,15 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
var ctxFn handlers.ContextFunc
ctxFn = func(req *http.Request) apirequest.Context {
ret, _ := r.requestContextMapper.Get(req)
return ret
}
requestScope := handlers.RequestScope{ requestScope := handlers.RequestScope{
Namer: handlers.ContextBasedNaming{ Namer: handlers.ContextBasedNaming{
GetContext: func(req *http.Request) apirequest.Context { GetContext: ctxFn,
ret, _ := r.requestContextMapper.Get(req)
return ret
},
SelfLinker: meta.NewAccessor(), SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped, ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix, SelfLinkPathPrefix: selfLinkPrefix,
@ -400,9 +502,8 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
Typer: typer, Typer: typer,
UnsafeConvertor: unstructured.UnstructuredObjectConverter{}, UnsafeConvertor: unstructured.UnstructuredObjectConverter{},
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural}, Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural},
Kind: kind, Kind: kind,
Subresource: "",
MetaGroupVersion: metav1.SchemeGroupVersion, MetaGroupVersion: metav1.SchemeGroupVersion,
} }
@ -411,8 +512,33 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
spec: &crd.Spec, spec: &crd.Spec,
acceptedNames: &crd.Status.AcceptedNames, acceptedNames: &crd.Status.AcceptedNames,
storage: storage, storage: customResourceStorage,
requestScope: requestScope, requestScope: requestScope,
scaleRequestScope: requestScope, // shallow copy
statusRequestScope: requestScope, // shallow copy
}
// override scaleSpec subresource values
scaleConverter := scale.NewScaleConverter()
ret.scaleRequestScope.Subresource = "scale"
ret.scaleRequestScope.Serializer = serializer.NewCodecFactory(scaleConverter.Scheme())
ret.scaleRequestScope.Kind = autoscalingv1.SchemeGroupVersion.WithKind("Scale")
ret.scaleRequestScope.Namer = handlers.ContextBasedNaming{
GetContext: ctxFn,
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
SelfLinkPathSuffix: "/scale",
}
// override status subresource values
ret.statusRequestScope.Subresource = "status"
ret.statusRequestScope.Namer = handlers.ContextBasedNaming{
GetContext: ctxFn,
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
SelfLinkPathSuffix: "/status",
} }
// Copy because we cannot write to storageMap without a race // Copy because we cannot write to storageMap without a race
@ -477,21 +603,21 @@ func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decod
return versioning.NewDefaultingCodecForScheme(Scheme, nil, decoder, nil, gv) return versioning.NewDefaultingCodecForScheme(Scheme, nil, decoder, nil, gv)
} }
type unstructuredObjectTyper struct { type UnstructuredObjectTyper struct {
delegate runtime.ObjectTyper Delegate runtime.ObjectTyper
unstructuredTyper runtime.ObjectTyper UnstructuredTyper runtime.ObjectTyper
} }
func (t unstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) { func (t UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
// Delegate for things other than Unstructured. // Delegate for things other than Unstructured.
if _, ok := obj.(runtime.Unstructured); !ok { if _, ok := obj.(runtime.Unstructured); !ok {
return t.delegate.ObjectKinds(obj) return t.Delegate.ObjectKinds(obj)
} }
return t.unstructuredTyper.ObjectKinds(obj) return t.UnstructuredTyper.ObjectKinds(obj)
} }
func (t unstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool { func (t UnstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
return t.delegate.Recognizes(gvk) || t.unstructuredTyper.Recognizes(gvk) return t.Delegate.Recognizes(gvk) || t.UnstructuredTyper.Recognizes(gvk)
} }
type unstructuredCreator struct{} type unstructuredCreator struct{}

View File

@ -29,7 +29,7 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
// Convert CRD schema to openapi schema // Convert CRD schema to openapi schema
openapiSchema := &spec.Schema{} openapiSchema := &spec.Schema{}
if customResourceValidation != nil { if customResourceValidation != nil {
if err := convertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil { if err := ConvertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
return nil, err return nil, err
} }
} }
@ -39,6 +39,10 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition. // ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
// CustomResource is a JSON data structure. // CustomResource is a JSON data structure.
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error { func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
if validator == nil {
return nil
}
result := validator.Validate(customResource) result := validator.Validate(customResource)
if result.AsError() != nil { if result.AsError() != nil {
return result.AsError() return result.AsError()
@ -46,7 +50,8 @@ func ValidateCustomResource(customResource interface{}, validator *validate.Sche
return nil return nil
} }
func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error { // ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema
func ConvertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
if in == nil { if in == nil {
return nil return nil
} }
@ -99,7 +104,7 @@ func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema)
if in.Not != nil { if in.Not != nil {
in, out := &in.Not, &out.Not in, out := &in.Not, &out.Not
*out = new(spec.Schema) *out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil { if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err return err
} }
} }
@ -176,7 +181,7 @@ func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]s
if in != nil { if in != nil {
for _, jsonSchemaProps := range *in { for _, jsonSchemaProps := range *in {
schema := spec.Schema{} schema := spec.Schema{}
if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil { if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return err return err
} }
*out = append(*out, schema) *out = append(*out, schema)
@ -190,7 +195,7 @@ func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (m
if len(in) != 0 { if len(in) != 0 {
for k, jsonSchemaProps := range in { for k, jsonSchemaProps := range in {
schema := spec.Schema{} schema := spec.Schema{}
if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil { if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return nil, err return nil, err
} }
out[k] = schema out[k] = schema
@ -203,7 +208,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
if in.Schema != nil { if in.Schema != nil {
in, out := &in.Schema, &out.Schema in, out := &in.Schema, &out.Schema
*out = new(spec.Schema) *out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil { if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err return err
} }
} }
@ -211,7 +216,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
in, out := &in.JSONSchemas, &out.Schemas in, out := &in.JSONSchemas, &out.Schemas
*out = make([]spec.Schema, len(*in)) *out = make([]spec.Schema, len(*in))
for i := range *in { for i := range *in {
if err := convertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil { if err := ConvertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
return err return err
} }
} }
@ -224,7 +229,7 @@ func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *
if in.Schema != nil { if in.Schema != nil {
in, out := &in.Schema, &out.Schema in, out := &in.Schema, &out.Schema
*out = new(spec.Schema) *out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil { if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err return err
} }
} }
@ -236,7 +241,7 @@ func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStri
if in.Schema != nil { if in.Schema != nil {
in, out := &in.Schema, &out.Schema in, out := &in.Schema, &out.Schema
*out = new(spec.Schema) *out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil { if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err return err
} }
} }

View File

@ -58,7 +58,7 @@ func TestRoundTrip(t *testing.T) {
// internal -> go-openapi // internal -> go-openapi
openAPITypes := &spec.Schema{} openAPITypes := &spec.Schema{}
if err := convertJSONSchemaProps(internal, openAPITypes); err != nil { if err := ConvertJSONSchemaProps(internal, openAPITypes); err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -33,6 +33,12 @@ const (
// //
// CustomResourceValidation is a list of validation methods for CustomResources // CustomResourceValidation is a list of validation methods for CustomResources
CustomResourceValidation utilfeature.Feature = "CustomResourceValidation" CustomResourceValidation utilfeature.Feature = "CustomResourceValidation"
// owner: @sttts, @nikhita
// alpha: v1.10
//
// CustomResourceSubresources defines the subresources for CustomResources
CustomResourceSubresources utilfeature.Feature = "CustomResourceSubresources"
) )
func init() { func init() {
@ -43,5 +49,6 @@ func init() {
// To add a new feature, define a key for it above and add it here. The features will be // To add a new feature, define a key for it above and add it here. The features will be
// available throughout Kubernetes binaries. // available throughout Kubernetes binaries.
var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{
CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta}, CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
CustomResourceSubresources: {Default: false, PreRelease: utilfeature.Alpha},
} }

View File

@ -17,20 +17,64 @@ limitations under the License.
package customresource package customresource
import ( import (
"fmt"
"strings"
autoscalingv1 "k8s.io/api/autoscaling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" 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, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter) CustomResourceStorage {
customResourceREST, customResourceStatusREST := newREST(resource, listKind, strategy, optsGetter)
customResourceRegistry := NewRegistry(customResourceREST)
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{
registry: customResourceRegistry,
specReplicasPath: scale.SpecReplicasPath,
statusReplicasPath: scale.StatusReplicasPath,
labelSelectorPath: labelSelectorPath,
}
}
return s
}
// REST implements a RESTStorage for API services against etcd // REST implements a RESTStorage for API services against etcd
type REST struct { type REST struct {
*genericregistry.Store *genericregistry.Store
} }
// NewREST returns a RESTStorage object that will work against API services. // newREST returns a RESTStorage object that will work against API services.
func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceDefinitionStorageStrategy, optsGetter generic.RESTOptionsGetter) *REST { func newREST(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST) {
store := &genericregistry.Store{ store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &unstructured.Unstructured{} }, NewFunc: func() runtime.Object { return &unstructured.Unstructured{} },
NewListFunc: func() runtime.Object { NewListFunc: func() runtime.Object {
@ -50,5 +94,161 @@ func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, st
if err := store.CompleteWithOptions(options); err != nil { if err := store.CompleteWithOptions(options); err != nil {
panic(err) // TODO: Propagate error up panic(err) // TODO: Propagate error up
} }
return &REST{store}
statusStore := *store
statusStore.UpdateStrategy = NewStatusStrategy(strategy)
return &REST{store}, &StatusREST{store: &statusStore}
}
// StatusREST implements the REST endpoint for changing the status of a CustomResource
type StatusREST struct {
store *genericregistry.Store
}
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 genericapirequest.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 genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) {
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation)
}
type ScaleREST struct {
registry Registry
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 genericapirequest.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
cr, err := r.registry.GetCustomResource(ctx, name, options)
if err != nil {
return nil, err
}
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 genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) {
cr, err := r.registry.GetCustomResource(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
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)
cr, err = r.registry.UpdateCustomResource(ctx, cr, createValidation, updateValidation)
if err != nil {
return nil, false, err
}
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

@ -0,0 +1,380 @@
/*
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"
"strings"
"testing"
autoscalingv1 "k8s.io/api/autoscaling/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
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/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/client-go/discovery"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/apiserver"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
)
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: discovery.NewUnstructuredObjectTyper(nil),
}
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{}
storage := customresource.NewStorage(
schema.GroupResource{Group: "mygroup.example.com", Resource: "noxus"},
schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "NoxuItemList"},
customresource.NewStrategy(
typer,
true,
kind,
nil,
nil,
status,
scale,
),
restOptions,
)
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, false)
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",
},
"spec": map[string]interface{}{
"replicas": int64(7),
},
},
}
}
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 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); 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); 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); 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); 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); 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); 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); 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); 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); 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
}

View File

@ -0,0 +1,104 @@
/*
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 (
"fmt"
"strings"
"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/schema"
"k8s.io/apimachinery/pkg/watch"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
)
// Registry is an interface for things that know how to store CustomResources.
type Registry interface {
ListCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*unstructured.UnstructuredList, error)
WatchCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error)
GetCustomResource(ctx genericapirequest.Context, customResourceID string, options *metav1.GetOptions) (*unstructured.Unstructured, error)
CreateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc) (*unstructured.Unstructured, error)
UpdateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (*unstructured.Unstructured, error)
DeleteCustomResource(ctx genericapirequest.Context, customResourceID string) error
}
// storage puts strong typing around storage calls
type storage struct {
rest.StandardStorage
}
// NewRegistry returns a new Registry interface for the given Storage. Any mismatched
// types will panic.
func NewRegistry(s rest.StandardStorage) Registry {
return &storage{s}
}
func (s *storage) ListCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*unstructured.UnstructuredList, error) {
if options != nil && options.FieldSelector != nil && !options.FieldSelector.Empty() {
return nil, fmt.Errorf("field selector not supported yet")
}
obj, err := s.List(ctx, options)
if err != nil {
return nil, err
}
return obj.(*unstructured.UnstructuredList), err
}
func (s *storage) WatchCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
return s.Watch(ctx, options)
}
func (s *storage) GetCustomResource(ctx genericapirequest.Context, customResourceID string, options *metav1.GetOptions) (*unstructured.Unstructured, error) {
obj, err := s.Get(ctx, customResourceID, options)
customResource, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("custom resource must be of type Unstructured")
}
if err != nil {
apiVersion := customResource.GetAPIVersion()
groupVersion := strings.Split(apiVersion, "/")
group := groupVersion[0]
return nil, errors.NewNotFound(schema.GroupResource{Group: group, Resource: "scale"}, customResourceID)
}
return customResource, nil
}
func (s *storage) CreateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc) (*unstructured.Unstructured, error) {
obj, err := s.Create(ctx, customResource, rest.ValidateAllObjectFunc, false)
if err != nil {
return nil, err
}
return obj.(*unstructured.Unstructured), nil
}
func (s *storage) UpdateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (*unstructured.Unstructured, error) {
obj, _, err := s.Update(ctx, customResource.GetName(), rest.DefaultUpdatedObjectInfo(customResource), createValidation, updateValidation)
if err != nil {
return nil, err
}
return obj.(*unstructured.Unstructured), nil
}
func (s *storage) DeleteCustomResource(ctx genericapirequest.Context, customResourceID string) error {
_, _, err := s.Delete(ctx, customResourceID, nil)
return err
}

View File

@ -0,0 +1,62 @@
/*
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 (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
)
type statusStrategy struct {
customResourceStrategy
}
func NewStatusStrategy(strategy customResourceStrategy) statusStrategy {
return statusStrategy{strategy}
}
func (a statusStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
newCustomResourceObject := obj.(*unstructured.Unstructured)
oldCustomResourceObject := old.(*unstructured.Unstructured)
newCustomResource := newCustomResourceObject.UnstructuredContent()
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
// update is not allowed to set spec and metadata
_, ok1 := newCustomResource["spec"]
_, ok2 := oldCustomResource["spec"]
switch {
case ok2:
newCustomResource["spec"] = oldCustomResource["spec"]
case ok1:
delete(newCustomResource, "spec")
}
newCustomResourceObject.SetAnnotations(oldCustomResourceObject.GetAnnotations())
newCustomResourceObject.SetFinalizers(oldCustomResourceObject.GetFinalizers())
newCustomResourceObject.SetGeneration(oldCustomResourceObject.GetGeneration())
newCustomResourceObject.SetLabels(oldCustomResourceObject.GetLabels())
newCustomResourceObject.SetOwnerReferences(oldCustomResourceObject.GetOwnerReferences())
newCustomResourceObject.SetSelfLink(oldCustomResourceObject.GetSelfLink())
}
// ValidateUpdate is the default update validation for an end user updating status.
func (a statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return a.customResourceStrategy.validator.ValidateStatusUpdate(ctx, obj, old, a.scale)
}

View File

@ -17,12 +17,10 @@ limitations under the License.
package customresource package customresource
import ( import (
"fmt"
"github.com/go-openapi/validate" "github.com/go-openapi/validate"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
@ -31,63 +29,129 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage" apiserverstorage "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
) )
type customResourceDefinitionStorageStrategy struct { // customResourceStrategy implements behavior for CustomResources.
type customResourceStrategy struct {
runtime.ObjectTyper runtime.ObjectTyper
names.NameGenerator names.NameGenerator
namespaceScoped bool namespaceScoped bool
validator customResourceValidator validator customResourceValidator
status *apiextensions.CustomResourceSubresourceStatus
scale *apiextensions.CustomResourceSubresourceScale
} }
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, validator *validate.SchemaValidator) customResourceDefinitionStorageStrategy { func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator *validate.SchemaValidator, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
return customResourceDefinitionStorageStrategy{ return customResourceStrategy{
ObjectTyper: typer, ObjectTyper: typer,
NameGenerator: names.SimpleNameGenerator, NameGenerator: names.SimpleNameGenerator,
namespaceScoped: namespaceScoped, namespaceScoped: namespaceScoped,
status: status,
scale: scale,
validator: customResourceValidator{ validator: customResourceValidator{
namespaceScoped: namespaceScoped, namespaceScoped: namespaceScoped,
kind: kind, kind: kind,
validator: validator, schemaValidator: schemaValidator,
statusSchemaValidator: statusSchemaValidator,
}, },
} }
} }
func (a customResourceDefinitionStorageStrategy) NamespaceScoped() bool { func (a customResourceStrategy) NamespaceScoped() bool {
return a.namespaceScoped return a.namespaceScoped
} }
func (customResourceDefinitionStorageStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { // PrepareForCreate clears the status of a CustomResource before creation.
func (a customResourceStrategy) PrepareForCreate(ctx genericapirequest.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)
} }
func (customResourceDefinitionStorageStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { // PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (a customResourceStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) || a.status == nil {
return
}
newCustomResourceObject := obj.(*unstructured.Unstructured)
oldCustomResourceObject := old.(*unstructured.Unstructured)
newCustomResource := newCustomResourceObject.UnstructuredContent()
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
// update is not allowed to set status
_, ok1 := newCustomResource["status"]
_, ok2 := oldCustomResource["status"]
switch {
case ok2:
newCustomResource["status"] = oldCustomResource["status"]
case ok1:
delete(newCustomResource, "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 CustomResource has *seen*.
newSpec, ok1 := newCustomResource["spec"]
oldSpec, ok2 := oldCustomResource["spec"]
// spec is changed, created or deleted
if (ok1 && ok2 && !apiequality.Semantic.DeepEqual(oldSpec, newSpec)) || (ok1 && !ok2) || (!ok1 && ok2) {
oldAccessor, _ := meta.Accessor(oldCustomResourceObject)
newAccessor, _ := meta.Accessor(newCustomResourceObject)
newAccessor.SetGeneration(oldAccessor.GetGeneration() + 1)
}
} }
func (a customResourceDefinitionStorageStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { // Validate validates a new CustomResource.
return a.validator.Validate(ctx, obj) func (a customResourceStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
return a.validator.Validate(ctx, obj, a.scale)
} }
func (customResourceDefinitionStorageStrategy) AllowCreateOnUpdate() bool { // 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 return false
} }
func (customResourceDefinitionStorageStrategy) AllowUnconditionalUpdate() bool { // AllowUnconditionalUpdate is the default update policy for CustomResource objects.
func (customResourceStrategy) AllowUnconditionalUpdate() bool {
return false return false
} }
func (customResourceDefinitionStorageStrategy) Canonicalize(obj runtime.Object) { // ValidateUpdate is the default update validation for an end user updating status.
func (a customResourceStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return a.validator.ValidateUpdate(ctx, obj, old, a.scale)
} }
func (a customResourceDefinitionStorageStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { // GetAttrs returns labels and fields of a given object for filtering purposes.
return a.validator.ValidateUpdate(ctx, obj, old) func (a customResourceStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
}
func (a customResourceDefinitionStorageStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
accessor, err := meta.Accessor(obj) accessor, err := meta.Accessor(obj)
if err != nil { if err != nil {
return nil, nil, false, err return nil, nil, false, err
@ -108,80 +172,13 @@ func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields.
} }
} }
func (a customResourceDefinitionStorageStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) storage.SelectionPredicate { // MatchCustomResourceDefinitionStorage is the filter used by the generic etcd backend to route
return storage.SelectionPredicate{ // 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, Label: label,
Field: field, Field: field,
GetAttrs: a.GetAttrs, GetAttrs: a.GetAttrs,
} }
} }
type customResourceValidator struct {
namespaceScoped bool
kind schema.GroupVersionKind
validator *validate.SchemaValidator
}
func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
accessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
}
func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
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())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
}

View File

@ -0,0 +1,241 @@
/*
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 (
"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"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"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 genericapirequest.Context, obj runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
accessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.schemaValidator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
if scale != nil {
// validate specReplicas
specReplicasPath := strings.TrimPrefix(scale.SpecReplicasPath, ".") // ignore leading period
specReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(specReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, err.Error())}
}
if specReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, "should be a non-negative integer")}
}
if specReplicas > math.MaxInt32 {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
}
// validate statusReplicas
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
}
if statusReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
}
if statusReplicas > math.MaxInt32 {
return field.ErrorList{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(customResource, strings.Split(labelSelectorPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
}
}
}
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
}
func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
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())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.schemaValidator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
if scale != nil {
// validate specReplicas
specReplicasPath := strings.TrimPrefix(scale.SpecReplicasPath, ".") // ignore leading period
specReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(specReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, err.Error())}
}
if specReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, "should be a non-negative integer")}
}
if specReplicas > math.MaxInt32 {
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
}
// validate statusReplicas
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
}
if statusReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
}
if statusReplicas > math.MaxInt32 {
return field.ErrorList{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(customResource, strings.Split(labelSelectorPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
}
}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
}
func (a customResourceValidator) ValidateStatusUpdate(ctx genericapirequest.Context, obj, old runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
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())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
// validate only the status
customResourceStatus := customResource["status"]
if err = apiservervalidation.ValidateCustomResource(customResourceStatus, a.statusSchemaValidator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath("status"), customResourceStatus, err.Error())}
}
if scale != nil {
// validate statusReplicas
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
}
if statusReplicas < 0 {
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
}
if statusReplicas > math.MaxInt32 {
return field.ErrorList{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(customResource, strings.Split(labelSelectorPath, ".")...)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
}
}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
}

View File

@ -35,6 +35,7 @@ import (
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
) )
// strategy implements behavior for CustomResources.
type strategy struct { type strategy struct {
runtime.ObjectTyper runtime.ObjectTyper
names.NameGenerator names.NameGenerator
@ -48,6 +49,7 @@ func (strategy) NamespaceScoped() bool {
return false return false
} }
// PrepareForCreate clears the status of a CustomResourceDefinition before creation.
func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
crd := obj.(*apiextensions.CustomResourceDefinition) crd := obj.(*apiextensions.CustomResourceDefinition)
crd.Status = apiextensions.CustomResourceDefinitionStatus{} crd.Status = apiextensions.CustomResourceDefinitionStatus{}
@ -57,8 +59,12 @@ func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Obje
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
crd.Spec.Validation = nil crd.Spec.Validation = nil
} }
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
crd.Spec.Subresources = nil
}
} }
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
newCRD := obj.(*apiextensions.CustomResourceDefinition) newCRD := obj.(*apiextensions.CustomResourceDefinition)
oldCRD := old.(*apiextensions.CustomResourceDefinition) oldCRD := old.(*apiextensions.CustomResourceDefinition)
@ -80,23 +86,33 @@ func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime
newCRD.Spec.Validation = nil newCRD.Spec.Validation = nil
oldCRD.Spec.Validation = nil oldCRD.Spec.Validation = nil
} }
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
newCRD.Spec.Subresources = nil
oldCRD.Spec.Subresources = nil
}
} }
// Validate validates a new CustomResourceDefinition.
func (strategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { func (strategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition)) return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition))
} }
// AllowCreateOnUpdate is false for CustomResourceDefinition; this means a POST is
// needed to create one.
func (strategy) AllowCreateOnUpdate() bool { func (strategy) AllowCreateOnUpdate() bool {
return false return false
} }
// AllowUnconditionalUpdate is the default update policy for CustomResourceDefinition objects.
func (strategy) AllowUnconditionalUpdate() bool { func (strategy) AllowUnconditionalUpdate() bool {
return false return false
} }
// Canonicalize normalizes the object after validation.
func (strategy) Canonicalize(obj runtime.Object) { func (strategy) Canonicalize(obj runtime.Object) {
} }
// ValidateUpdate is the default update validation for an end user updating status.
func (strategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { func (strategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
} }
@ -143,10 +159,11 @@ func (statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old run
return validation.ValidateUpdateCustomResourceDefinitionStatus(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) 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) { func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
apiserver, ok := obj.(*apiextensions.CustomResourceDefinition) apiserver, ok := obj.(*apiextensions.CustomResourceDefinition)
if !ok { if !ok {
return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition.") return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition")
} }
return labels.Set(apiserver.ObjectMeta.Labels), CustomResourceDefinitionToSelectableFields(apiserver), apiserver.Initializers != nil, nil return labels.Set(apiserver.ObjectMeta.Labels), CustomResourceDefinitionToSelectableFields(apiserver), apiserver.Initializers != nil, nil
} }

View File

@ -35,7 +35,7 @@ import (
) )
func TestServerUp(t *testing.T) { func TestServerUp(t *testing.T) {
stopCh, _, _, err := testserver.StartDefaultServer() stopCh, _, _, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -43,7 +43,7 @@ func TestServerUp(t *testing.T) {
} }
func TestNamespaceScopedCRUD(t *testing.T) { func TestNamespaceScopedCRUD(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -61,7 +61,7 @@ func TestNamespaceScopedCRUD(t *testing.T) {
} }
func TestClusterScopedCRUD(t *testing.T) { func TestClusterScopedCRUD(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -347,7 +347,7 @@ func TestDiscovery(t *testing.T) {
group := "mygroup.example.com" group := "mygroup.example.com"
version := "v1beta1" version := "v1beta1"
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -391,7 +391,7 @@ func TestDiscovery(t *testing.T) {
} }
func TestNoNamespaceReject(t *testing.T) { func TestNoNamespaceReject(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -430,7 +430,7 @@ func TestNoNamespaceReject(t *testing.T) {
} }
func TestSameNameDiffNamespace(t *testing.T) { func TestSameNameDiffNamespace(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -450,7 +450,7 @@ func TestSameNameDiffNamespace(t *testing.T) {
} }
func TestSelfLink(t *testing.T) { func TestSelfLink(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -503,7 +503,7 @@ func TestSelfLink(t *testing.T) {
} }
func TestPreserveInt(t *testing.T) { func TestPreserveInt(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -548,7 +548,7 @@ func TestPreserveInt(t *testing.T) {
} }
func TestPatch(t *testing.T) { func TestPatch(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -622,7 +622,7 @@ func TestPatch(t *testing.T) {
} }
func TestCrossNamespaceListWatch(t *testing.T) { func TestCrossNamespaceListWatch(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -758,7 +758,7 @@ func checkNamespacesWatchHelper(t *testing.T, ns string, namespacedwatch watch.I
} }
func TestNameConflict(t *testing.T) { func TestNameConflict(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -28,7 +28,7 @@ import (
) )
func TestFinalization(t *testing.T) { func TestFinalization(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
require.NoError(t, err) require.NoError(t, err)
defer close(stopCh) defer close(stopCh)

View File

@ -30,6 +30,7 @@ import (
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/testserver" "k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -73,8 +74,22 @@ func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, defi
}, ns) }, ns)
} }
func NewNamespacedCustomResourceStatusClient(ns string, client dynamic.Interface, definition *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
return client.Resource(&metav1.APIResource{
Name: definition.Spec.Names.Plural + "/status",
Namespaced: definition.Spec.Scope == apiextensionsv1beta1.NamespaceScoped,
}, ns)
}
func NewNamespacedCustomResourceScaleClient(ns string, client dynamic.Interface, definition *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
return client.Resource(&metav1.APIResource{
Name: definition.Spec.Names.Plural + "/scale",
Namespaced: definition.Spec.Scope == apiextensionsv1beta1.NamespaceScoped,
}, ns)
}
func TestMultipleResourceInstances(t *testing.T) { func TestMultipleResourceInstances(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -198,7 +213,7 @@ func TestMultipleResourceInstances(t *testing.T) {
} }
func TestMultipleRegistration(t *testing.T) { func TestMultipleRegistration(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -254,7 +269,7 @@ func TestMultipleRegistration(t *testing.T) {
} }
func TestDeRegistrationAndReRegistration(t *testing.T) { func TestDeRegistrationAndReRegistration(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -347,12 +362,18 @@ func TestEtcdStorage(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config) stopCh, clientConfig, err := testserver.StartServer(config)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer close(stopCh) defer close(stopCh)
apiExtensionClient, err := apiextensionsclientset.NewForConfig(clientConfig)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(clientConfig)
etcdPrefix := getPrefixFromConfig(t, config) etcdPrefix := getPrefixFromConfig(t, config)
ns1 := "another-default-is-possible" ns1 := "another-default-is-possible"

View File

@ -0,0 +1,787 @@
/*
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 integration
import (
"math"
"reflect"
"sort"
"strings"
"testing"
"time"
autoscaling "k8s.io/api/autoscaling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
"k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
)
var labelSelectorPath = ".status.labelSelector"
func NewNoxuSubresourcesCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
return &apiextensionsv1beta1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
Group: "mygroup.example.com",
Version: "v1beta1",
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
Plural: "noxus",
Singular: "nonenglishnoxu",
Kind: "WishIHadChosenNoxu",
ShortNames: []string{"foo", "bar", "abc", "def"},
ListKind: "NoxuItemList",
},
Scope: scope,
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
SpecReplicasPath: ".spec.replicas",
StatusReplicasPath: ".status.replicas",
LabelSelectorPath: &labelSelectorPath,
},
},
},
}
}
func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "mygroup.example.com/v1beta1",
"kind": "WishIHadChosenNoxu",
"metadata": map[string]interface{}{
"namespace": namespace,
"name": name,
},
"spec": map[string]interface{}{
"num": int64(10),
"replicas": int64(3),
},
"status": map[string]interface{}{
"replicas": int64(7),
},
},
}
}
func TestStatusSubresource(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// status should not be set after creation
if val, ok := gottenNoxuInstance.Object["status"]; ok {
t.Fatalf("status should not be set after creation, got %v", val)
}
// .status.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// UpdateStatus should not update spec.
// Check that .spec.num = 10 and .status.num = 20
updatedStatusInstance, err := noxuStatusResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != int64(10) {
t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum)
}
statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != int64(20) {
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
}
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// .status.num = 40
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num = 40
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Update should not update status.
// Check that .spec.num = 40 and .status.num = 20
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update instance: %v", err)
}
specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != int64(40) {
t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum)
}
statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != int64(20) {
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
}
}
func TestScaleSubresource(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
groupResource := schema.GroupResource{
Group: "mygroup.example.com",
Resource: "noxus",
}
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
// set invalid json path for specReplicasPath
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = "foo,bar"
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err == nil {
t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec")
}
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = ".spec.replicas"
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
scaleClient, err := testserver.CreateNewScaleClient(noxuDefinition, config)
if err != nil {
t.Fatal(err)
}
// set .status.labelSelector = bar
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", "status", "labelSelector")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = noxuStatusResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
// get the scale object
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
if err != nil {
t.Fatal(err)
}
if gottenScale.Spec.Replicas != 3 {
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas)
}
if gottenScale.Status.Selector != "bar" {
t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector)
}
// check self link
expectedSelfLink := "/apis/mygroup.example.com/v1beta1/namespaces/not-the-default/noxus/foo/scale"
if gottenScale.GetSelfLink() != expectedSelfLink {
t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink())
}
// update the scale object
// check that spec is updated, but status is not
gottenScale.Spec.Replicas = 5
gottenScale.Status.Selector = "baz"
updatedScale, err := scaleClient.Scales("not-the-default").Update(groupResource, gottenScale)
if err != nil {
t.Fatal(err)
}
if updatedScale.Spec.Replicas != 5 {
t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas)
}
if updatedScale.Status.Selector != "bar" {
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector)
}
// check that .spec.replicas = 5, but status is not updated
updatedNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas")
if !found || err != nil {
t.Fatalf("unable to get .spec.replicas")
}
if specReplicas != 5 {
t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas)
}
statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, "status", "labelSelector")
if !found || err != nil {
t.Fatalf("unable to get .status.labelSelector")
}
if statusLabelSelector != "bar" {
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector)
}
// validate maximum value
// set .spec.replicas = math.MaxInt64
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = noxuResourceClient.Update(gottenNoxuInstance)
if err == nil {
t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647")
}
}
func TestValidationSchema(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
// fields other than properties in root schema are not allowed
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err == nil {
t.Fatalf("unexpected non-error: if subresources for custom resources are enabled, only properties can be used at the root of the schema")
}
// make sure we are not restricting fields to properties even in subschemas
noxuDefinition.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"spec": {
Description: "Validation for spec",
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"replicas": {
Type: "integer",
},
},
},
},
}
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatalf("unable to created crd %v: %v", noxuDefinition.Name, err)
}
}
func TestValidateOnlyStatus(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
// UpdateStatus should validate only status
// 1. create a crd with max value of .spec.num = 10 and .status.num = 10
// 2. create a cr with .spec.num = 10 and .status.num = 10 (valid)
// 3. update the crd so that max value of .spec.num = 5 and .status.num = 10
// 4. update the status of the cr with .status.num = 5 (spec is invalid)
// validation passes becauses spec is not validated
// max value of spec.num = 10 and status.num = 10
schema := &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"spec": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(10),
},
},
},
"status": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(10),
},
},
},
},
}
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
OpenAPIV3Schema: schema,
}
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
// set .spec.num = 10 and .status.num = 10
noxuInstance := NewNoxuSubresourceInstance(ns, "foo")
err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
createdNoxuInstance, err := instantiateCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
gottenCRD, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get("noxus.mygroup.example.com", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// update the crd so that max value of spec.num = 5 and status.num = 10
gottenCRD.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"spec": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(5),
},
},
},
"status": {
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
"num": {
Type: "integer",
Maximum: float64Ptr(10),
},
},
},
},
}
if _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(gottenCRD); err != nil {
t.Fatal(err)
}
// update the status with .status.num = 5
err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(5), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// cr is updated even though spec is invalid
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
_, err := noxuStatusResourceClient.Update(createdNoxuInstance)
if statusError, isStatus := err.(*apierrors.StatusError); isStatus {
if strings.Contains(statusError.Error(), "is invalid") {
return false, nil
}
}
if err != nil {
return false, err
}
return true, nil
})
if err != nil {
t.Fatal(err)
}
}
func TestSubresourcesDiscovery(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
group := "mygroup.example.com"
version := "v1beta1"
resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version)
if err != nil {
t.Fatal(err)
}
if len(resources.APIResources) != 3 {
t.Fatalf("Expected exactly the resources \"noxus\", \"noxus/status\" and \"noxus/scale\" in group version %v/%v via discovery, got: %v", group, version, resources.APIResources)
}
// check discovery info for status
status := resources.APIResources[1]
if status.Name != "noxus/status" {
t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name)
}
if status.Namespaced != true {
t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced)
}
if status.Kind != "WishIHadChosenNoxu" {
t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind)
}
expectedVerbs := []string{"get", "patch", "update"}
sort.Strings(status.Verbs)
if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) {
t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs)
}
// check discovery info for scale
scale := resources.APIResources[2]
if scale.Group != autoscaling.GroupName {
t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group)
}
if scale.Version != "v1" {
t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version)
}
if scale.Name != "noxus/scale" {
t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name)
}
if scale.Namespaced != true {
t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced)
}
if scale.Kind != "Scale" {
t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind)
}
sort.Strings(scale.Verbs)
if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) {
t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs)
}
}
func TestGeneration(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
// .metadata.generation = 1
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
if gottenNoxuInstance.GetGeneration() != 1 {
t.Fatalf(".metadata.generation should be 1 after creation")
}
// .status.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// UpdateStatus does not increment generation
updatedStatusInstance, err := noxuStatusResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update status: %v", err)
}
if updatedStatusInstance.GetGeneration() != 1 {
t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration())
}
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
if err != nil {
t.Fatal(err)
}
// .spec.num = 20
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Update increments generation
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance)
if err != nil {
t.Fatalf("unable to update instance: %v", err)
}
if updatedInstance.GetGeneration() != 2 {
t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration())
}
}
func TestSubresourcePatch(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
groupResource := schema.GroupResource{
Group: "mygroup.example.com",
Resource: "noxus",
}
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
ns := "not-the-default"
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
noxuScaleResourceClient := NewNamespacedCustomResourceScaleClient(ns, noxuVersionClient, noxuDefinition)
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
if err != nil {
t.Fatalf("unable to create noxu instance: %v", err)
}
scaleClient, err := testserver.CreateNewScaleClient(noxuDefinition, config)
if err != nil {
t.Fatal(err)
}
patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`)
patchedNoxuInstance, err := noxuStatusResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// .spec.num should remain 10
specNum, found, err := unstructured.NestedInt64(patchedNoxuInstance.Object, "spec", "num")
if !found || err != nil {
t.Fatalf("unable to get .spec.num")
}
if specNum != 10 {
t.Fatalf(".spec.num: expected: %v, got: %v", 10, specNum)
}
// .status.num should be 999
statusNum, found, err := unstructured.NestedInt64(patchedNoxuInstance.Object, "status", "num")
if !found || err != nil {
t.Fatalf("unable to get .status.num")
}
if statusNum != 999 {
t.Fatalf(".status.num: expected: %v, got: %v", 999, statusNum)
}
// this call waits for the resourceVersion to be reached in the cache before returning.
// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
// and then the updated object shows a conflicting diff, which permanently fails the patch.
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
// See https://issue.k8s.io/42644
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// no-op patch
_, err = noxuStatusResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// empty patch
_, err = noxuStatusResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`)
patchedNoxuInstance, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// this call waits for the resourceVersion to be reached in the cache before returning.
// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
// and then the updated object shows a conflicting diff, which permanently fails the patch.
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
// See https://issue.k8s.io/42644
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 7
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
if err != nil {
t.Fatal(err)
}
if gottenScale.Spec.Replicas != 7 {
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas)
}
if gottenScale.Status.Replicas != 0 {
t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas)
}
// no-op patch
_, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, patch)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// empty patch
_, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// make sure strategic merge patch is not supported for both status and scale
_, err = noxuStatusResourceClient.Patch("foo", types.StrategicMergePatchType, patch)
if err == nil {
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
}
_, err = noxuScaleResourceClient.Patch("foo", types.StrategicMergePatchType, patch)
if err == nil {
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
}
}

View File

@ -30,7 +30,10 @@ import (
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/scale"
) )
const ( const (
@ -293,3 +296,34 @@ func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefi
func GetCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1beta1.CustomResourceDefinition, error) { func GetCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1beta1.CustomResourceDefinition, error) {
return apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{}) return apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
} }
func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, config *rest.Config) (scale.ScalesGetter, error) {
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
groupResource, err := discoveryClient.ServerResourcesForGroupVersion(crd.Spec.Group + "/" + crd.Spec.Version)
if err != nil {
return nil, err
}
resources := []*discovery.APIGroupResources{
{
Group: metav1.APIGroup{
Name: crd.Spec.Group,
Versions: []metav1.GroupVersionForDiscovery{
{Version: crd.Spec.Version},
},
PreferredVersion: metav1.GroupVersionForDiscovery{Version: crd.Spec.Version},
},
VersionedResources: map[string][]metav1.APIResource{
crd.Spec.Version: groupResource.APIResources,
},
},
}
restMapper := discovery.NewRESTMapper(resources, nil)
resolver := scale.NewDiscoveryScaleKindResolver(discoveryClient)
return scale.NewForConfig(config, restMapper, dynamic.LegacyAPIPathResolverFunc, resolver)
}

View File

@ -32,6 +32,7 @@ import (
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
genericapiserveroptions "k8s.io/apiserver/pkg/server/options" genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
) )
func DefaultServerConfig() (*extensionsapiserver.Config, error) { func DefaultServerConfig() (*extensionsapiserver.Config, error) {
@ -87,11 +88,11 @@ func DefaultServerConfig() (*extensionsapiserver.Config, error) {
return config, nil return config, nil
} }
func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.Interface, dynamic.ClientPool, error) { func StartServer(config *extensionsapiserver.Config) (chan struct{}, *rest.Config, error) {
stopCh := make(chan struct{}) stopCh := make(chan struct{})
server, err := config.Complete().New(genericapiserver.EmptyDelegate) server, err := config.Complete().New(genericapiserver.EmptyDelegate)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, err
} }
go func() { go func() {
err := server.GenericAPIServer.PrepareRun().Run(stopCh) err := server.GenericAPIServer.PrepareRun().Run(stopCh)
@ -123,26 +124,32 @@ func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.I
}) })
if err != nil { if err != nil {
close(stopCh) close(stopCh)
return nil, nil, err
}
return stopCh, config.GenericConfig.LoopbackClientConfig, nil
}
func StartDefaultServer() (chan struct{}, *rest.Config, error) {
config, err := DefaultServerConfig()
if err != nil {
return nil, nil, err
}
return StartServer(config)
}
func StartDefaultServerWithClients() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
stopCh, config, err := StartDefaultServer()
if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
apiExtensionsClient, err := clientset.NewForConfig(server.GenericAPIServer.LoopbackClientConfig) apiExtensionsClient, err := clientset.NewForConfig(config)
if err != nil { if err != nil {
close(stopCh) close(stopCh)
return nil, nil, nil, err return nil, nil, nil, err
} }
bytes, _ := apiExtensionsClient.Discovery().RESTClient().Get().AbsPath("/apis/apiextensions.k8s.io/v1beta1").DoRaw() return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(config), nil
fmt.Print(string(bytes))
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(server.GenericAPIServer.LoopbackClientConfig), nil
}
func StartDefaultServer() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
config, err := DefaultServerConfig()
if err != nil {
return nil, nil, nil, err
}
return StartServer(config)
} }

View File

@ -31,7 +31,7 @@ import (
) )
func TestForProperValidationErrors(t *testing.T) { func TestForProperValidationErrors(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -169,7 +169,7 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure
} }
func TestCustomResourceValidation(t *testing.T) { func TestCustomResourceValidation(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -190,7 +190,7 @@ func TestCustomResourceValidation(t *testing.T) {
} }
func TestCustomResourceUpdateValidation(t *testing.T) { func TestCustomResourceUpdateValidation(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -233,7 +233,7 @@ func TestCustomResourceUpdateValidation(t *testing.T) {
} }
func TestCustomResourceValidationErrors(t *testing.T) { func TestCustomResourceValidationErrors(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -324,7 +324,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
} }
func TestCRValidationOnCRDUpdate(t *testing.T) { func TestCRValidationOnCRDUpdate(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -378,7 +378,7 @@ func TestCRValidationOnCRDUpdate(t *testing.T) {
} }
func TestForbiddenFieldsInSchema(t *testing.T) { func TestForbiddenFieldsInSchema(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -24,25 +24,32 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
"k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
) )
func TestYAML(t *testing.T) { func TestYAML(t *testing.T) {
config, err := testserver.DefaultServerConfig() stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer close(stopCh) defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped) noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) _, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil { if err != nil {
@ -232,7 +239,7 @@ values:
Param("watch", "true"). Param("watch", "true").
DoRaw() DoRaw()
if !errors.IsNotAcceptable(err) { if !errors.IsNotAcceptable(err) {
t.Fatal("expected not acceptable error, got %v (%s)", err, string(result)) t.Fatalf("expected not acceptable error, got %v (%s)", err, string(result))
} }
obj, err := decodeYAML(result) obj, err := decodeYAML(result)
if err != nil { if err != nil {
@ -294,7 +301,7 @@ values:
t.Fatal(v, ok, err, string(result)) t.Fatal(v, ok, err, string(result))
} }
if obj.GetUID() != uid { if obj.GetUID() != uid {
t.Fatal("uid changed: %v vs %v", uid, obj.GetUID()) t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
} }
} }
@ -346,6 +353,179 @@ values:
} }
} }
func TestYAMLSubresource(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.ClusterScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
rest := apiExtensionClient.Discovery().RESTClient()
uid := types.UID("")
resourceVersion := ""
// Create
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
spec:
replicas: 3`, apiVersion, kind))
result, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
uid = obj.GetUID()
resourceVersion = obj.GetResourceVersion()
}
// Get at /status
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// Update at /status
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
uid: %s
resourceVersion: "%s"
spec:
replicas: 5
status:
replicas: 3`, apiVersion, kind, uid, resourceVersion))
result, err := rest.Put().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if obj.GetUID() != uid {
t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
}
}
// Get at /scale
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "scale").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != "autoscaling/v1" {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != "Scale" {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
}
func decodeYAML(data []byte) (*unstructured.Unstructured, error) { func decodeYAML(data []byte) (*unstructured.Unstructured, error) {
retval := &unstructured.Unstructured{Object: map[string]interface{}{}} retval := &unstructured.Unstructured{Object: map[string]interface{}{}}
// ensure this isn't JSON // ensure this isn't JSON

View File

@ -280,6 +280,7 @@ func (rc *ResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error
Watch() Watch()
} }
// Patch applies the patch and returns the patched resource.
func (rc *ResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) { func (rc *ResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) {
result := new(unstructured.Unstructured) result := new(unstructured.Unstructured)
resourceName, subresourceName := rc.parseResourceSubresourceName() resourceName, subresourceName := rc.parseResourceSubresourceName()

View File

@ -99,7 +99,7 @@ func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) {
restMapperRes, err := discovery.GetAPIGroupResources(fakeDiscoveryClient) restMapperRes, err := discovery.GetAPIGroupResources(fakeDiscoveryClient)
if err != nil { if err != nil {
t.Fatalf("unexpected error while constructing resource list from fake discovery client: %v") t.Fatalf("unexpected error while constructing resource list from fake discovery client: %v", err)
} }
restMapper := discovery.NewRESTMapper(restMapperRes, apimeta.InterfacesForUnstructured) restMapper := discovery.NewRESTMapper(restMapperRes, apimeta.InterfacesForUnstructured)