Unverified Commit 6e856480 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #55168 from nikhita/customresources-subresources

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. apiextensions: add subresources for custom resources Fixes #38113 Fixes #58778 **Related**: - Proposal: https://github.com/kubernetes/community/pull/913 - For custom resources to work with `kubectl scale`: https://github.com/kubernetes/kubernetes/pull/58283 **Add types**: - Add `CustomResourceSubResources` type to CRD. - Fix proto generation for `CustomResourceSubResourceStatus`: https://github.com/kubernetes/kubernetes/pull/55970. - Add feature gate for `CustomResourceSubResources`. - Update CRD strategy: if feature gate is disabled, this feature is dropped (i.e. set to `nil`). - Add validation for `CustomResourceSubResources`: - `SpecReplicasPath` should not be empty and should be a valid 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. - `StatusReplicasPath` should not be empty and should be a valid 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. - If present, `LabelSelectorPath` should be a valid json path. If there is no value under `LabelSelectorPath` in the CustomResource, the status label selector value in the `/scale` subresource will default to the empty string. - `ScaleGroupVersion` should be `autoscaling/v1`. - If `CustomResourceSubResources` is enabled, only `properties` is allowed under the root schema for CRD validation. **Add status and scale subresources**: - Use helper functions from `apimachinery/pkg/apis/meta/v1/unstructured/helpers.go`. - Improve error handling: https://github.com/kubernetes/kubernetes/pull/56563, https://github.com/kubernetes/kubernetes/pull/58215. - Introduce Registry interface for storage. - Update storage: - Introduce `CustomResourceStorage` which acts as storage for the custom resource and its status and scale subresources. Note: storage for status and scale is only enabled when the feature gate is enabled _and_ the respective fields are enabled in the CRD. - Introduce `StatusREST` and its `New()`, `Get()` and `Update()` methods. - Introduce `ScaleREST` and its `New()`, `Get()` and `Update()` methods. - Get and Update use the json paths from the CRD and use it to return an `autoscaling/v1.Scale` object. - Update strategy: - In `PrepareForCreate`, - Clear `.status`. - Set `.metadata.generation` = 1 - In `PrepareForUpdate`, - Do not update `.status`. - If both the old and new objects have `.status` and it is changed, set it back to its old value. - If the old object has a `.status` but the new object doesn't, set it to the old value. - If old object did not have a `.status` but the new object does, delete it. - Increment generation if spec changes i.e. in the following cases: - If both the old and new objects had `.spec` and it changed. - If the old object did not have `.spec` but the new object does. - If the old object had a `.spec` but the new object doesn't. - In `Validate` and `ValidateUpdate`, - ensure that values at `specReplicasPath` and `statusReplicasPath` are >=0 and < maxInt32. - make sure there are no errors in getting the value at all the paths. - Introduce `statusStrategy` with its methods. - In `PrepareForUpdate`: - Do not update `.spec`. - If both the old and new objects have `.spec` and it is changed, set it back to its old value. - If the old object has a `.spec` but the new object doesn't, set it to the old value. - If old object did not have a `.spec` but the new object does, delete it. - Do not update `.metadata`. - In `ValidateStatusUpdate`: - For CRD validation, validate only under `.status`. - Validate value at `statusReplicasPath` as above. If `labelSelectorPath` is a path under `.status`, then validate it as well. - Plug into the custom resource handler: - Store all three storage - customResource, status and scale in `crdInfo`. - Use the storage as per the subresource in the request. - Use the validator as per the subresource (for status, only use the schema for `status`, if present). - Serve the endpoint as per the subresource - see `serveResource`, `serveStatus` and `serveScale`. - Update discovery by adding the `/status` and `/scale` resources, if enabled. **Add tests**: - Add unit tests in `etcd_test.go`. - Add integration tests. - In `subresources_test.go`, use the [polymporphic scale client](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/client-go/scale) to get and update `Scale`. - Add a test to check everything works fine with yaml in `yaml_test.go`. **Release note**: ```release-note `/status` and `/scale` subresources are added for custom resources. ```
parents fd1527a9 55ce3ded
...@@ -1263,7 +1263,7 @@ ...@@ -1263,7 +1263,7 @@
}, },
{ {
"ImportPath": "github.com/go-openapi/validate", "ImportPath": "github.com/go-openapi/validate",
"Rev": "deaf2c9013bc1a7f4c774662259a506ba874d80f" "Rev": "d509235108fcf6ab4913d2dcb3a2260c0db2108e"
}, },
{ {
"ImportPath": "github.com/godbus/dbus", "ImportPath": "github.com/godbus/dbus",
......
...@@ -83905,6 +83905,10 @@ ...@@ -83905,6 +83905,10 @@
"description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced", "description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced",
"type": "string" "type": "string"
}, },
"subresources": {
"description": "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.",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources"
},
"validation": { "validation": {
"description": "Validation describes the validation methods for CustomResources", "description": "Validation describes the validation methods for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation" "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation"
...@@ -83935,6 +83939,43 @@ ...@@ -83935,6 +83939,43 @@
} }
} }
}, },
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceScale": {
"description": "CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.",
"required": [
"specReplicasPath",
"statusReplicasPath"
],
"properties": {
"labelSelectorPath": {
"description": "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.",
"type": "string"
},
"specReplicasPath": {
"description": "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.",
"type": "string"
},
"statusReplicasPath": {
"description": "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.",
"type": "string"
}
}
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceStatus": {
"description": "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"
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources": {
"description": "CustomResourceSubresources defines the status and scale subresources for CustomResources.",
"properties": {
"scale": {
"description": "Scale denotes the scale subresource for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceScale"
},
"status": {
"description": "Status denotes the status subresource for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceStatus"
}
}
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation": { "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation": {
"description": "CustomResourceValidation is a list of validation methods for CustomResources.", "description": "CustomResourceValidation is a list of validation methods for CustomResources.",
"properties": { "properties": {
...@@ -24,7 +24,7 @@ import ( ...@@ -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"
......
...@@ -477,7 +477,6 @@ staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server ...@@ -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
......
...@@ -305,6 +305,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS ...@@ -305,6 +305,7 @@ 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},
......
...@@ -16,7 +16,9 @@ limitations under the License. ...@@ -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 { ...@@ -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 { ...@@ -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
}
...@@ -107,6 +107,12 @@ message CustomResourceDefinitionSpec { ...@@ -107,6 +107,12 @@ message CustomResourceDefinitionSpec {
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
// +optional // +optional
optional CustomResourceValidation validation = 5; optional CustomResourceValidation validation = 5;
// 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
optional CustomResourceSubresources subresources = 6;
} }
// CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition // CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition
...@@ -119,6 +125,48 @@ message CustomResourceDefinitionStatus { ...@@ -119,6 +125,48 @@ message CustomResourceDefinitionStatus {
optional CustomResourceDefinitionNames acceptedNames = 2; optional CustomResourceDefinitionNames acceptedNames = 2;
} }
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
message CustomResourceSubresourceScale {
// SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .spec.
// If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.
optional string specReplicasPath = 1;
// StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource
// will default to 0.
optional string statusReplicasPath = 2;
// LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// Must be set to work with HPA.
// If there is no value under the given path in the CustomResource, the status label selector value in the /scale
// subresource will default to the empty string.
// +optional
optional string labelSelectorPath = 3;
}
// CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources.
// Status is represented by the `.status` JSON path inside of a CustomResource. When set,
// * exposes a /status subresource for the custom resource
// * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza
// * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza
message CustomResourceSubresourceStatus {
}
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
message CustomResourceSubresources {
// Status denotes the status subresource for CustomResources
optional CustomResourceSubresourceStatus status = 1;
// Scale denotes the scale subresource for CustomResources
optional CustomResourceSubresourceScale scale = 2;
}
// CustomResourceValidation is a list of validation methods for CustomResources. // CustomResourceValidation is a list of validation methods for CustomResources.
message CustomResourceValidation { message CustomResourceValidation {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against. // OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
......
...@@ -16,7 +16,9 @@ limitations under the License. ...@@ -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 { ...@@ -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 { ...@@ -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"`
}
...@@ -48,6 +48,12 @@ func RegisterConversions(scheme *runtime.Scheme) error { ...@@ -48,6 +48,12 @@ func RegisterConversions(scheme *runtime.Scheme) error {
Convert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomResourceDefinitionSpec, Convert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomResourceDefinitionSpec,
Convert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomResourceDefinitionStatus, Convert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomResourceDefinitionStatus,
Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus, Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus,
Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale,
Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale,
Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus,
Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus,
Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources,
Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources,
Convert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation, Convert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation,
Convert_apiextensions_CustomResourceValidation_To_v1beta1_CustomResourceValidation, Convert_apiextensions_CustomResourceValidation_To_v1beta1_CustomResourceValidation,
Convert_v1beta1_ExternalDocumentation_To_apiextensions_ExternalDocumentation, Convert_v1beta1_ExternalDocumentation_To_apiextensions_ExternalDocumentation,
...@@ -211,6 +217,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes ...@@ -211,6 +217,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes
} else { } else {
out.Validation = nil out.Validation = nil
} }
out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
return nil return nil
} }
...@@ -235,6 +242,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes ...@@ -235,6 +242,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes
} else { } else {
out.Validation = nil out.Validation = nil
} }
out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
return nil return nil
} }
...@@ -269,6 +277,70 @@ func Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResou ...@@ -269,6 +277,70 @@ func Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResou
return autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus(in, out, s) return autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus(in, out, s)
} }
func autoConvert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in *CustomResourceSubresourceScale, out *apiextensions.CustomResourceSubresourceScale, s conversion.Scope) error {
out.SpecReplicasPath = in.SpecReplicasPath
out.StatusReplicasPath = in.StatusReplicasPath
out.LabelSelectorPath = (*string)(unsafe.Pointer(in.LabelSelectorPath))
return nil
}
// Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in *CustomResourceSubresourceScale, out *apiextensions.CustomResourceSubresourceScale, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in *apiextensions.CustomResourceSubresourceScale, out *CustomResourceSubresourceScale, s conversion.Scope) error {
out.SpecReplicasPath = in.SpecReplicasPath
out.StatusReplicasPath = in.StatusReplicasPath
out.LabelSelectorPath = (*string)(unsafe.Pointer(in.LabelSelectorPath))
return nil
}
// Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in *apiextensions.CustomResourceSubresourceScale, out *CustomResourceSubresourceScale, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in, out, s)
}
func autoConvert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in *CustomResourceSubresourceStatus, out *apiextensions.CustomResourceSubresourceStatus, s conversion.Scope) error {
return nil
}
// Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in *CustomResourceSubresourceStatus, out *apiextensions.CustomResourceSubresourceStatus, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in *apiextensions.CustomResourceSubresourceStatus, out *CustomResourceSubresourceStatus, s conversion.Scope) error {
return nil
}
// Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in *apiextensions.CustomResourceSubresourceStatus, out *CustomResourceSubresourceStatus, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in, out, s)
}
func autoConvert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in *CustomResourceSubresources, out *apiextensions.CustomResourceSubresources, s conversion.Scope) error {
out.Status = (*apiextensions.CustomResourceSubresourceStatus)(unsafe.Pointer(in.Status))
out.Scale = (*apiextensions.CustomResourceSubresourceScale)(unsafe.Pointer(in.Scale))
return nil
}
// Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in *CustomResourceSubresources, out *apiextensions.CustomResourceSubresources, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in *apiextensions.CustomResourceSubresources, out *CustomResourceSubresources, s conversion.Scope) error {
out.Status = (*CustomResourceSubresourceStatus)(unsafe.Pointer(in.Status))
out.Scale = (*CustomResourceSubresourceScale)(unsafe.Pointer(in.Scale))
return nil
}
// Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in *apiextensions.CustomResourceSubresources, out *CustomResourceSubresources, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in, out, s)
}
func autoConvert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(in *CustomResourceValidation, out *apiextensions.CustomResourceValidation, s conversion.Scope) error { func autoConvert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(in *CustomResourceValidation, out *apiextensions.CustomResourceValidation, s conversion.Scope) error {
if in.OpenAPIV3Schema != nil { if in.OpenAPIV3Schema != nil {
in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema
......
...@@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
if in.Subresources != nil {
in, out := &in.Subresources, &out.Subresources
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresources)
(*in).DeepCopyInto(*out)
}
}
return return
} }
...@@ -176,6 +185,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt ...@@ -176,6 +185,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) {
*out = *in
if in.LabelSelectorPath != nil {
in, out := &in.LabelSelectorPath, &out.LabelSelectorPath
if *in == nil {
*out = nil
} else {
*out = new(string)
**out = **in
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceScale.
func (in *CustomResourceSubresourceScale) DeepCopy() *CustomResourceSubresourceScale {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceScale)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceStatus) DeepCopyInto(out *CustomResourceSubresourceStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceStatus.
func (in *CustomResourceSubresourceStatus) DeepCopy() *CustomResourceSubresourceStatus {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresources) DeepCopyInto(out *CustomResourceSubresources) {
*out = *in
if in.Status != nil {
in, out := &in.Status, &out.Status
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceStatus)
**out = **in
}
}
if in.Scale != nil {
in, out := &in.Scale, &out.Scale
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceScale)
(*in).DeepCopyInto(*out)
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresources.
func (in *CustomResourceSubresources) DeepCopy() *CustomResourceSubresources {
if in == nil {
return nil
}
out := new(CustomResourceSubresources)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) { func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) {
*out = *in *out = *in
if in.OpenAPIV3Schema != nil { if in.OpenAPIV3Schema != nil {
......
...@@ -18,6 +18,7 @@ package validation ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
}
...@@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
if in.Subresources != nil {
in, out := &in.Subresources, &out.Subresources
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresources)
(*in).DeepCopyInto(*out)
}
}
return return
} }
...@@ -176,6 +185,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt ...@@ -176,6 +185,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) {
*out = *in
if in.LabelSelectorPath != nil {
in, out := &in.LabelSelectorPath, &out.LabelSelectorPath
if *in == nil {
*out = nil
} else {
*out = new(string)
**out = **in
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceScale.
func (in *CustomResourceSubresourceScale) DeepCopy() *CustomResourceSubresourceScale {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceScale)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceStatus) DeepCopyInto(out *CustomResourceSubresourceStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceStatus.
func (in *CustomResourceSubresourceStatus) DeepCopy() *CustomResourceSubresourceStatus {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresources) DeepCopyInto(out *CustomResourceSubresources) {
*out = *in
if in.Status != nil {
in, out := &in.Status, &out.Status
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceStatus)
**out = **in
}
}
if in.Scale != nil {
in, out := &in.Scale, &out.Scale
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceScale)
(*in).DeepCopyInto(*out)
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresources.
func (in *CustomResourceSubresources) DeepCopy() *CustomResourceSubresources {
if in == nil {
return nil
}
out := new(CustomResourceSubresources)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) { func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) {
*out = *in *out = *in
if in.OpenAPIV3Schema != nil { if in.OpenAPIV3Schema != nil {
......
...@@ -16,7 +16,11 @@ go_library( ...@@ -16,7 +16,11 @@ go_library(
], ],
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver", importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver",
deps = [ deps = [
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/go-openapi/strfmt:go_default_library",
"//vendor/github.com/go-openapi/validate:go_default_library",
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
...@@ -29,6 +33,7 @@ go_library( ...@@ -29,6 +33,7 @@ go_library(
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/controller/status:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/controller/status:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
...@@ -60,7 +65,10 @@ go_library( ...@@ -60,7 +65,10 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/server:go_default_library", "//vendor/k8s.io/apiserver/pkg/server:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/storage:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/storage:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library", "//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/scale:go_default_library",
"//vendor/k8s.io/client-go/scale/scheme/autoscalingv1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library", "//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/util/workqueue:go_default_library", "//vendor/k8s.io/client-go/util/workqueue:go_default_library",
], ],
......
...@@ -22,6 +22,7 @@ import ( ...@@ -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 { ...@@ -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 {
......
...@@ -29,7 +29,7 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa ...@@ -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 ...@@ -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 ...@@ -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) ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 * ...@@ -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 ...@@ -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
} }
} }
......
...@@ -58,7 +58,7 @@ func TestRoundTrip(t *testing.T) { ...@@ -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)
} }
......
...@@ -33,6 +33,12 @@ const ( ...@@ -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() {
...@@ -44,4 +50,5 @@ func init() { ...@@ -44,4 +50,5 @@ func init() {
// 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},
} }
...@@ -3,20 +3,30 @@ package(default_visibility = ["//visibility:public"]) ...@@ -3,20 +3,30 @@ package(default_visibility = ["//visibility:public"])
load( load(
"@io_bazel_rules_go//go:def.bzl", "@io_bazel_rules_go//go:def.bzl",
"go_library", "go_library",
"go_test",
) )
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"etcd.go", "etcd.go",
"registry.go",
"status_strategy.go",
"strategy.go", "strategy.go",
"validator.go",
], ],
importpath = "k8s.io/apiextensions-apiserver/pkg/registry/customresource", importpath = "k8s.io/apiextensions-apiserver/pkg/registry/customresource",
deps = [ deps = [
"//vendor/github.com/go-openapi/validate:go_default_library", "//vendor/github.com/go-openapi/validate:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
...@@ -24,11 +34,14 @@ go_library( ...@@ -24,11 +34,14 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
], ],
) )
...@@ -44,3 +57,27 @@ filegroup( ...@@ -44,3 +57,27 @@ filegroup(
srcs = [":package-srcs"], srcs = [":package-srcs"],
tags = ["automanaged"], tags = ["automanaged"],
) )
go_test(
name = "go_default_xtest",
srcs = ["etcd_test.go"],
deps = [
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic/testing:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/etcd/testing:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
],
)
...@@ -17,20 +17,64 @@ limitations under the License. ...@@ -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 ...@@ -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
} }
/*
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
}
/*
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)
}
...@@ -35,6 +35,7 @@ import ( ...@@ -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 { ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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
} }
......
...@@ -11,6 +11,7 @@ go_test( ...@@ -11,6 +11,7 @@ go_test(
"basic_test.go", "basic_test.go",
"finalization_test.go", "finalization_test.go",
"registration_test.go", "registration_test.go",
"subresources_test.go",
"validation_test.go", "validation_test.go",
"yaml_test.go", "yaml_test.go",
], ],
...@@ -19,17 +20,23 @@ go_test( ...@@ -19,17 +20,23 @@ go_test(
"//vendor/github.com/coreos/etcd/clientv3:go_default_library", "//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library", "//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/test/integration/testserver:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/test/integration/testserver:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library",
], ],
) )
......
...@@ -35,7 +35,7 @@ import ( ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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 ...@@ -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)
} }
......
...@@ -28,7 +28,7 @@ import ( ...@@ -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)
......
...@@ -30,6 +30,7 @@ import ( ...@@ -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 ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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"
......
...@@ -28,7 +28,10 @@ go_library( ...@@ -28,7 +28,10 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/server:go_default_library", "//vendor/k8s.io/apiserver/pkg/server:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/options:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/options:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/scale:go_default_library",
], ],
) )
......
...@@ -30,7 +30,10 @@ import ( ...@@ -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 ...@@ -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)
}
...@@ -32,6 +32,7 @@ import ( ...@@ -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) { ...@@ -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 ...@@ -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, nil, err return nil, nil, err
} }
apiExtensionsClient, err := clientset.NewForConfig(server.GenericAPIServer.LoopbackClientConfig) return stopCh, config.GenericConfig.LoopbackClientConfig, nil
}
func StartDefaultServer() (chan struct{}, *rest.Config, error) {
config, err := DefaultServerConfig()
if err != nil { if err != nil {
close(stopCh) return nil, nil, err
return nil, nil, nil, err
} }
bytes, _ := apiExtensionsClient.Discovery().RESTClient().Get().AbsPath("/apis/apiextensions.k8s.io/v1beta1").DoRaw() return StartServer(config)
fmt.Print(string(bytes))
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(server.GenericAPIServer.LoopbackClientConfig), nil
} }
func StartDefaultServer() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) { func StartDefaultServerWithClients() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
config, err := DefaultServerConfig() stopCh, config, err := StartDefaultServer()
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
return StartServer(config) apiExtensionsClient, err := clientset.NewForConfig(config)
if err != nil {
close(stopCh)
return nil, nil, nil, err
}
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(config), nil
} }
...@@ -31,7 +31,7 @@ import ( ...@@ -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 ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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)
} }
......
...@@ -24,24 +24,31 @@ import ( ...@@ -24,24 +24,31 @@ 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer close(stopCh)
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config) apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer close(stopCh) 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)
...@@ -232,7 +239,7 @@ values: ...@@ -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: ...@@ -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: ...@@ -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
......
...@@ -280,6 +280,7 @@ func (rc *ResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error ...@@ -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()
......
...@@ -99,7 +99,7 @@ func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) { ...@@ -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)
......
eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.lisG21ATunZSCBP6vaqK_AZCIR18tN563RdqkAb6PGOipqTeg8R7VwZQIeDqS-2Vond6NX_KSC_D_uxxv0hBf2DiGXmwMUmP4nRXrsmbzT2qQKKIHYRDC_6jb2-FSfK14ezIe1Q07UiiJecDsN3CFEccS8E68Tdnp78p7yDwbTvpumnZmwYfyhlImtjFQv2YpyFVsjEHWK0R4e9T3ONQWcx6D2rSoxABbutrS03QwsJhHCeD9joL_gxfkFKm3CW8yWPSk2QYtx_Q1hu-tZR4IPb2tQPXPX3mtyhwBqziWgmJRDFCEjlCO5aCobiMm_9K5X05gue_DcgW163zh1P9jg.nleER2An8CUn_OuR.b77RFEFp0gC8j5yCAoARNKYmQIvWq99ibmf5ffJgdhQBF3sRYJLt_XflJ_2lsaiFOxvc45T2fnkMVy2lHFcri7F9f1BRiT_0AcDthxsecGzG8BZ9QvaM6b4Dn0rhjrOq8rsF0m3ZnbPBkkg3LV5EkbHWstMo2fgJPJhJswlGWhqJPJBDecG1nMBC8SMH32X-zVlSM-BLiaghvOGNxyb_RLZJZ3CLczIdQ2JO2UeYkOGCPGzernvkHDMpqQXc-8cmulDdHgCy87qFLy5ttGFgYbnTm92h_ChOGKZixeX0PL0pQY5wXd2xTO7Tg_Ov5E5FoVwIkwOextedVsF9iz_b_mwtCY3LXrvbJTW7zWrwBVsVyAXxT5iu0HyQ3tBVxT2GxS-yM5ApqLozcZCQg9flMyfSgThu82FfzEr0fI5vKw8zo0GdO4GBuVSppM9m6ToG6hlwyHD9g2YTZw9068hyq1_kZQhugJRjgGbpa2gyGqzx16fg0zVoupVIiq5KfvRlAQFeOVVjQwb0BWf25tJUj5tV3O9ge6dbKSXizEca33FJJwJWoXhd7DCREXUU9pBz06NCCf495BGoVbq3oLPDQc2mpcuy0XAPxSwXcc5Ts8DNs7MrxBlYdw81wMXuztIpOY4.XjKlMWl_H40XszToi2VU5g
\ No newline at end of file
clone:
path: github.com/go-openapi/validate
matrix:
GO_VERSION:
- "1.6"
build:
integration:
image: golang:$$GO_VERSION
pull: true
commands:
- go get -u github.com/axw/gocov/gocov
- go get -u gopkg.in/matm/v1/gocov-html
- go get -u github.com/cee-dub/go-junit-report
- go get -u github.com/stretchr/testify/assert
- go get -u gopkg.in/yaml.v2
- go get -u github.com/go-openapi/analysis
- go get -u github.com/go-openapi/errors
- go get -u github.com/go-openapi/loads
- go get -u github.com/go-openapi/strfmt
- go get -u github.com/go-openapi/runtime
- go test -race
- go test -v -cover -coverprofile=coverage.out -covermode=count
notify:
slack:
channel: bots
webhook_url: $$SLACK_URL
username: drone
publish:
coverage:
server: https://coverage.vmware.run
token: $$GITHUB_TOKEN
# threshold: 70
# must_increase: true
when:
matrix:
GO_VERSION: "1.6"
approve_by_comment: true
approve_regex: '^(:shipit:|:\+1:|\+1|LGTM|lgtm|Approved)'
reject_regex: ^[Rr]ejected
reset_on_push: false
reviewers:
members:
- casualjim
- chancez
- frapposelli
- vburenin
- pytlesk4
name: pullapprove
required: 1
language: go
go:
- 1.7
install:
- go get -u github.com/axw/gocov/gocov
- go get -u gopkg.in/matm/v1/gocov-html
- go get -u github.com/cee-dub/go-junit-report
- go get -u github.com/stretchr/testify/assert
- go get -u github.com/kr/pretty
- go get -u gopkg.in/yaml.v2
- go get -u github.com/go-openapi/analysis
- go get -u github.com/go-openapi/errors
- go get -u github.com/go-openapi/loads
- go get -u github.com/go-openapi/strfmt
- go get -u github.com/go-openapi/runtime
script:
- go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
slack:
secure: EmObnQuM9Mw8J9vpFaKKHqSMN4Wsr/A9+v7ewAD5cEhA0T1P4m7MbJMiJOhxUhj/X+BFh2DamW+P2lT8mybj5wg8wnkQ2BteKA8Tawi6f9PRw2NRheO8tAi8o/npLnlmet0kc93mn+oLuqHw36w4+j5mkOl2FghkfGiUVhwrhkCP7KXQN+3TU87e+/HzQumlJ3nsE+6terVxkH3PmaUTsS5ONaODZfuxFpfb7RsoEl3skHf6d+tr+1nViLxxly7558Nc33C+W1mr0qiEvMLZ+kJ/CpGWBJ6CUJM3jm6hNe2eMuIPwEK2hxZob8c7n22VPap4K6a0bBRoydoDXaba+2sD7Ym6ivDO/DVyL44VeBBLyIiIBylDGQdZH+6SoWm90Qe/i7tnY/T5Ao5igT8f3cfQY1c3EsTfqmlDfrhmACBmwSlgkdVBLTprHL63JMY24LWmh4jhxsmMRZhCL4dze8su1w6pLN/pD1pGHtKYCEVbdTmaM3PblNRFf12XB7qosmQsgUndH4Vq3bTbU0s1pKjeDhRyLvFzvR0TBbo0pDLEoF1A/i5GVFWa7yLZNUDudQERRh7qv/xBl2excIaQ1sV4DSVm7bAE9l6Kp+yeHQJW2uN6Y3X8wu9gB9nv9l5HBze7wh8KE6PyWAOLYYqZg9/sAtsv/2GcQqXcKFF1zcA=
# Validation helpers [![Build Status](https://ci.vmware.run/api/badges/go-openapi/validate/status.svg)](https://ci.vmware.run/go-openapi/validate) [![Coverage](https://coverage.vmware.run/badges/go-openapi/validate/coverage.svg)](https://coverage.vmware.run/go-openapi/validate) [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) # Validation helpers [![Build Status](https://travis-ci.org/go-openapi/validate.svg?branch=master)](https://travis-ci.org/go-openapi/validate) [![codecov](https://codecov.io/gh/go-openapi/validate/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/validate) [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io)
[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/validate/master/LICENSE) [![GoDoc](https://godoc.org/github.com/go-openapi/validate?status.svg)](http://godoc.org/github.com/go-openapi/validate) [![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/validate/master/LICENSE) [![GoDoc](https://godoc.org/github.com/go-openapi/validate?status.svg)](http://godoc.org/github.com/go-openapi/validate)
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
package validate package validate
import ( import (
"log"
"reflect" "reflect"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
...@@ -51,7 +52,9 @@ func (f *formatValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -51,7 +52,9 @@ func (f *formatValidator) Applies(source interface{}, kind reflect.Kind) bool {
return false return false
} }
r := doit() r := doit()
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", f.Path, r, source, kind) if Debug {
log.Printf("format validator for %q applies %t for %T (kind: %v)\n", f.Path, r, source, kind)
}
return r return r
} }
......
...@@ -15,8 +15,10 @@ ...@@ -15,8 +15,10 @@
package validate package validate
import ( import (
"log"
"reflect" "reflect"
"regexp" "regexp"
"strings"
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
...@@ -45,10 +47,45 @@ func (o *objectValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -45,10 +47,45 @@ func (o *objectValidator) Applies(source interface{}, kind reflect.Kind) bool {
// there is a problem in the type validator where it will be unhappy about null values // there is a problem in the type validator where it will be unhappy about null values
// so that requires more testing // so that requires more testing
r := reflect.TypeOf(source) == specSchemaType && (kind == reflect.Map || kind == reflect.Struct) r := reflect.TypeOf(source) == specSchemaType && (kind == reflect.Map || kind == reflect.Struct)
//fmt.Printf("object validator for %q applies %t for %T (kind: %v)\n", o.Path, r, source, kind) if Debug {
log.Printf("object validator for %q applies %t for %T (kind: %v)\n", o.Path, r, source, kind)
}
return r return r
} }
func (o *objectValidator) isPropertyName() bool {
p := strings.Split(o.Path, ".")
return p[len(p)-1] == "properties" && p[len(p)-2] != "properties"
}
func (o *objectValidator) checkArrayMustHaveItems(res *Result, val map[string]interface{}) {
if t, typeFound := val["type"]; typeFound {
if tpe, ok := t.(string); ok && tpe == "array" {
if _, itemsKeyFound := val["items"]; !itemsKeyFound {
res.AddErrors(errors.Required("items", o.Path))
}
}
}
}
func (o *objectValidator) checkItemsMustBeTypeArray(res *Result, val map[string]interface{}) {
if !o.isPropertyName() {
if _, itemsKeyFound := val["items"]; itemsKeyFound {
t, typeFound := val["type"]
if typeFound {
if tpe, ok := t.(string); !ok || tpe != "array" {
res.AddErrors(errors.InvalidType(o.Path, o.In, "array", nil))
}
} else {
// there is no type
res.AddErrors(errors.Required("type", o.Path))
}
}
}
}
func (o *objectValidator) precheck(res *Result, val map[string]interface{}) {
o.checkArrayMustHaveItems(res, val)
o.checkItemsMustBeTypeArray(res, val)
}
func (o *objectValidator) Validate(data interface{}) *Result { func (o *objectValidator) Validate(data interface{}) *Result {
val := data.(map[string]interface{}) val := data.(map[string]interface{})
numKeys := int64(len(val)) numKeys := int64(len(val))
...@@ -61,14 +98,8 @@ func (o *objectValidator) Validate(data interface{}) *Result { ...@@ -61,14 +98,8 @@ func (o *objectValidator) Validate(data interface{}) *Result {
} }
res := new(Result) res := new(Result)
if len(o.Required) > 0 {
for _, k := range o.Required { o.precheck(res, val)
if _, ok := val[k]; !ok {
res.AddErrors(errors.Required(o.Path+"."+k, o.In))
continue
}
}
}
if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows { if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows {
for k := range val { for k := range val {
...@@ -99,6 +130,8 @@ func (o *objectValidator) Validate(data interface{}) *Result { ...@@ -99,6 +130,8 @@ func (o *objectValidator) Validate(data interface{}) *Result {
} }
} }
createdFromDefaults := map[string]bool{}
for pName, pSchema := range o.Properties { for pName, pSchema := range o.Properties {
rName := pName rName := pName
if o.Path != "" { if o.Path != "" {
...@@ -106,7 +139,24 @@ func (o *objectValidator) Validate(data interface{}) *Result { ...@@ -106,7 +139,24 @@ func (o *objectValidator) Validate(data interface{}) *Result {
} }
if v, ok := val[pName]; ok { if v, ok := val[pName]; ok {
res.Merge(NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats).Validate(v)) r := NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats).Validate(v)
res.Merge(r)
} else if pSchema.Default != nil {
createdFromDefaults[pName] = true
pName := pName // shaddow
def := pSchema.Default
res.Defaulters = append(res.Defaulters, DefaulterFunc(func() {
val[pName] = def
}))
}
}
if len(o.Required) > 0 {
for _, k := range o.Required {
if _, ok := val[k]; !ok && !createdFromDefaults[k] {
res.AddErrors(errors.Required(o.Path+"."+k, o.In))
continue
}
} }
} }
...@@ -137,9 +187,6 @@ func (o *objectValidator) validatePatternProperty(key string, value interface{}, ...@@ -137,9 +187,6 @@ func (o *objectValidator) validatePatternProperty(key string, value interface{},
res := validator.Validate(value) res := validator.Validate(value)
result.Merge(res) result.Merge(res)
if res.IsValid() {
succeededOnce = true
}
} }
} }
......
...@@ -14,12 +14,32 @@ ...@@ -14,12 +14,32 @@
package validate package validate
import "github.com/go-openapi/errors" import (
"os"
"github.com/go-openapi/errors"
)
var (
// Debug is true when the SWAGGER_DEBUG env var is not empty
Debug = os.Getenv("SWAGGER_DEBUG") != ""
)
type Defaulter interface {
Apply()
}
type DefaulterFunc func()
func (f DefaulterFunc) Apply() {
f()
}
// Result represents a validation result // Result represents a validation result
type Result struct { type Result struct {
Errors []error Errors []error
MatchCount int MatchCount int
Defaulters []Defaulter
} }
// Merge merges this result with the other one, preserving match counts etc // Merge merges this result with the other one, preserving match counts etc
...@@ -29,11 +49,13 @@ func (r *Result) Merge(other *Result) *Result { ...@@ -29,11 +49,13 @@ func (r *Result) Merge(other *Result) *Result {
} }
r.AddErrors(other.Errors...) r.AddErrors(other.Errors...)
r.MatchCount += other.MatchCount r.MatchCount += other.MatchCount
r.Defaulters = append(r.Defaulters, other.Defaulters...)
return r return r
} }
// AddErrors adds errors to this validation result // AddErrors adds errors to this validation result
func (r *Result) AddErrors(errors ...error) { func (r *Result) AddErrors(errors ...error) {
// TODO: filter already existing errors
r.Errors = append(r.Errors, errors...) r.Errors = append(r.Errors, errors...)
} }
...@@ -59,3 +81,9 @@ func (r *Result) AsError() error { ...@@ -59,3 +81,9 @@ func (r *Result) AsError() error {
} }
return errors.CompositeValidationError(r.Errors...) return errors.CompositeValidationError(r.Errors...)
} }
func (r *Result) ApplyDefaults() {
for _, d := range r.Defaulters {
d.Apply()
}
}
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
package validate package validate
import ( import (
"encoding/json"
"log"
"reflect" "reflect"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
...@@ -80,12 +82,16 @@ func (s *SchemaValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -80,12 +82,16 @@ func (s *SchemaValidator) Applies(source interface{}, kind reflect.Kind) bool {
// Validate validates the data against the schema // Validate validates the data against the schema
func (s *SchemaValidator) Validate(data interface{}) *Result { func (s *SchemaValidator) Validate(data interface{}) *Result {
result := new(Result)
if s == nil {
return result
}
if data == nil { if data == nil {
v := s.validators[0].Validate(data) v := s.validators[0].Validate(data)
v.Merge(s.validators[6].Validate(data)) v.Merge(s.validators[6].Validate(data))
return v return v
} }
result := new(Result)
tpe := reflect.TypeOf(data) tpe := reflect.TypeOf(data)
kind := tpe.Kind() kind := tpe.Kind()
...@@ -98,8 +104,35 @@ func (s *SchemaValidator) Validate(data interface{}) *Result { ...@@ -98,8 +104,35 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
d = swag.ToDynamicJSON(data) d = swag.ToDynamicJSON(data)
} }
isnumber := s.Schema.Type.Contains("number") || s.Schema.Type.Contains("integer")
if num, ok := data.(json.Number); ok && isnumber {
if s.Schema.Type.Contains("integer") { // avoid lossy conversion
in, erri := num.Int64()
if erri != nil {
result.AddErrors(erri)
result.Inc()
return result
}
d = in
} else {
nf, errf := num.Float64()
if errf != nil {
result.AddErrors(errf)
result.Inc()
return result
}
d = nf
}
tpe = reflect.TypeOf(d)
kind = tpe.Kind()
}
for _, v := range s.validators { for _, v := range s.validators {
if !v.Applies(s.Schema, kind) { if !v.Applies(s.Schema, kind) {
if Debug {
log.Printf("%T does not apply for %v", v, kind)
}
continue continue
} }
...@@ -119,7 +152,6 @@ func (s *SchemaValidator) commonValidator() valueValidator { ...@@ -119,7 +152,6 @@ func (s *SchemaValidator) commonValidator() valueValidator {
return &basicCommonValidator{ return &basicCommonValidator{
Path: s.Path, Path: s.Path,
In: s.in, In: s.in,
Default: s.Schema.Default,
Enum: s.Schema.Enum, Enum: s.Schema.Enum,
} }
} }
...@@ -155,7 +187,6 @@ func (s *SchemaValidator) stringValidator() valueValidator { ...@@ -155,7 +187,6 @@ func (s *SchemaValidator) stringValidator() valueValidator {
return &stringValidator{ return &stringValidator{
Path: s.Path, Path: s.Path,
In: s.in, In: s.in,
Default: s.Schema.Default,
MaxLength: s.Schema.MaxLength, MaxLength: s.Schema.MaxLength,
MinLength: s.Schema.MinLength, MinLength: s.Schema.MinLength,
Pattern: s.Schema.Pattern, Pattern: s.Schema.Pattern,
...@@ -166,7 +197,6 @@ func (s *SchemaValidator) formatValidator() valueValidator { ...@@ -166,7 +197,6 @@ func (s *SchemaValidator) formatValidator() valueValidator {
return &formatValidator{ return &formatValidator{
Path: s.Path, Path: s.Path,
In: s.in, In: s.in,
//Default: s.Schema.Default,
Format: s.Schema.Format, Format: s.Schema.Format,
KnownFormats: s.KnownFormats, KnownFormats: s.KnownFormats,
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
package validate package validate
import ( import (
"log"
"reflect" "reflect"
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
...@@ -80,12 +81,15 @@ func newSchemaPropsValidator(path string, in string, allOf, oneOf, anyOf []spec. ...@@ -80,12 +81,15 @@ func newSchemaPropsValidator(path string, in string, allOf, oneOf, anyOf []spec.
func (s *schemaPropsValidator) Applies(source interface{}, kind reflect.Kind) bool { func (s *schemaPropsValidator) Applies(source interface{}, kind reflect.Kind) bool {
r := reflect.TypeOf(source) == specSchemaType r := reflect.TypeOf(source) == specSchemaType
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind) if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
}
return r return r
} }
func (s *schemaPropsValidator) Validate(data interface{}) *Result { func (s *schemaPropsValidator) Validate(data interface{}) *Result {
mainResult := new(Result) mainResult := new(Result)
var firstSuccess *Result
if len(s.anyOfValidators) > 0 { if len(s.anyOfValidators) > 0 {
var bestFailures *Result var bestFailures *Result
succeededOnce := false succeededOnce := false
...@@ -94,6 +98,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result { ...@@ -94,6 +98,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if result.IsValid() { if result.IsValid() {
bestFailures = nil bestFailures = nil
succeededOnce = true succeededOnce = true
if firstSuccess == nil {
firstSuccess = result
}
break break
} }
if bestFailures == nil || result.MatchCount > bestFailures.MatchCount { if bestFailures == nil || result.MatchCount > bestFailures.MatchCount {
...@@ -106,11 +113,14 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result { ...@@ -106,11 +113,14 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
} }
if bestFailures != nil { if bestFailures != nil {
mainResult.Merge(bestFailures) mainResult.Merge(bestFailures)
} else if firstSuccess != nil {
mainResult.Merge(firstSuccess)
} }
} }
if len(s.oneOfValidators) > 0 { if len(s.oneOfValidators) > 0 {
var bestFailures *Result var bestFailures *Result
var firstSuccess *Result
validated := 0 validated := 0
for _, oneOfSchema := range s.oneOfValidators { for _, oneOfSchema := range s.oneOfValidators {
...@@ -118,6 +128,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result { ...@@ -118,6 +128,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if result.IsValid() { if result.IsValid() {
validated++ validated++
bestFailures = nil bestFailures = nil
if firstSuccess == nil {
firstSuccess = result
}
continue continue
} }
if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) { if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) {
...@@ -130,6 +143,8 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result { ...@@ -130,6 +143,8 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if bestFailures != nil { if bestFailures != nil {
mainResult.Merge(bestFailures) mainResult.Merge(bestFailures)
} }
} else if firstSuccess != nil {
mainResult.Merge(firstSuccess)
} }
} }
......
...@@ -67,6 +67,9 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result { ...@@ -67,6 +67,9 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
itemsSize = int64(len(s.Items.Schemas)) itemsSize = int64(len(s.Items.Schemas))
for i := int64(0); i < itemsSize; i++ { for i := int64(0); i < itemsSize; i++ {
validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats) validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats)
if val.Len() <= int(i) {
break
}
result.Merge(validator.Validate(val.Index(int(i)).Interface())) result.Merge(validator.Validate(val.Index(int(i)).Interface()))
} }
......
...@@ -19,6 +19,7 @@ import ( ...@@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"log" "log"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/go-openapi/analysis" "github.com/go-openapi/analysis"
...@@ -193,6 +194,13 @@ func (s *SpecValidator) validateDuplicatePropertyNames() *Result { ...@@ -193,6 +194,13 @@ func (s *SpecValidator) validateDuplicatePropertyNames() *Result {
return res return res
} }
func (s *SpecValidator) resolveRef(ref *spec.Ref) (*spec.Schema, error) {
if s.spec.SpecFilePath() != "" {
return spec.ResolveRefWithBase(s.spec.Spec(), ref, &spec.ExpandOptions{RelativeBase: s.spec.SpecFilePath()})
}
return spec.ResolveRef(s.spec.Spec(), ref)
}
func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, knowns map[string]struct{}) []dupProp { func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, knowns map[string]struct{}) []dupProp {
var dups []dupProp var dups []dupProp
...@@ -200,7 +208,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, ...@@ -200,7 +208,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema,
schc := &sch schc := &sch
for schc.Ref.String() != "" { for schc.Ref.String() != "" {
// gather property names // gather property names
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref) reso, err := s.resolveRef(&schc.Ref)
if err != nil { if err != nil {
panic(err) panic(err)
} }
...@@ -236,7 +244,7 @@ func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, kno ...@@ -236,7 +244,7 @@ func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, kno
schn := nm schn := nm
schc := &sch schc := &sch
for schc.Ref.String() != "" { for schc.Ref.String() != "" {
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref) reso, err := s.resolveRef(&schc.Ref)
if err != nil { if err != nil {
panic(err) panic(err)
} }
...@@ -335,15 +343,15 @@ func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID str ...@@ -335,15 +343,15 @@ func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID str
return errors.New(422, "%s for %q is a collection without an element type", prefix, opID) return errors.New(422, "%s for %q is a collection without an element type", prefix, opID)
} }
schemas := schema.Items.Schemas
if schema.Items.Schema != nil { if schema.Items.Schema != nil {
schemas = []spec.Schema{*schema.Items.Schema} schema = *schema.Items.Schema
} if _, err := regexp.Compile(schema.Pattern); err != nil {
for _, sch := range schemas { return errors.New(422, "%s for %q has invalid items pattern: %q", prefix, opID, schema.Pattern)
if err := s.validateSchemaItems(sch, prefix, opID); err != nil {
return err
} }
return s.validateSchemaItems(schema, prefix, opID)
} }
return nil return nil
} }
...@@ -524,8 +532,15 @@ func (s *SpecValidator) validateParameters() *Result { ...@@ -524,8 +532,15 @@ func (s *SpecValidator) validateParameters() *Result {
} }
var fromPath []string var fromPath []string
for _, i := range params { for _, i := range params {
fromPath = append(fromPath, knowns[i]) knownsi := knowns[i]
knowns[i] = "!" iparams := extractPathParams(knownsi)
if len(iparams) > 0 {
fromPath = append(fromPath, iparams...)
for _, iparam := range iparams {
knownsi = strings.Replace(knownsi, iparam, "!", 1)
}
knowns[i] = knownsi
}
} }
knownPath := strings.Join(knowns, "/") knownPath := strings.Join(knowns, "/")
if orig, ok := knownPaths[knownPath]; ok { if orig, ok := knownPaths[knownPath]; ok {
...@@ -544,7 +559,9 @@ func (s *SpecValidator) validateParameters() *Result { ...@@ -544,7 +559,9 @@ func (s *SpecValidator) validateParameters() *Result {
for pr.Ref.String() != "" { for pr.Ref.String() != "" {
obj, _, err := pr.Ref.GetPointer().Get(sw) obj, _, err := pr.Ref.GetPointer().Get(sw)
if err != nil { if err != nil {
if Debug {
log.Println(err) log.Println(err)
}
res.AddErrors(err) res.AddErrors(err)
break PARAMETERS break PARAMETERS
} }
...@@ -575,6 +592,10 @@ func (s *SpecValidator) validateParameters() *Result { ...@@ -575,6 +592,10 @@ func (s *SpecValidator) validateParameters() *Result {
pr = obj.(spec.Parameter) pr = obj.(spec.Parameter)
} }
if _, err := regexp.Compile(pr.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in param %q: %q", op.ID, pr.Name, pr.Pattern))
}
if pr.In == "body" { if pr.In == "body" {
if firstBodyParam != "" { if firstBodyParam != "" {
res.AddErrors(errors.New(422, "operation %q has more than 1 body param (accepted: %q, dropped: %q)", op.ID, firstBodyParam, pr.Name)) res.AddErrors(errors.New(422, "operation %q has more than 1 body param (accepted: %q, dropped: %q)", op.ID, firstBodyParam, pr.Name))
...@@ -595,18 +616,35 @@ func (s *SpecValidator) validateParameters() *Result { ...@@ -595,18 +616,35 @@ func (s *SpecValidator) validateParameters() *Result {
func parsePath(path string) (segments []string, params []int) { func parsePath(path string) (segments []string, params []int) {
for i, p := range strings.Split(path, "/") { for i, p := range strings.Split(path, "/") {
segments = append(segments, p) segments = append(segments, p)
if len(p) > 0 && p[0] == '{' && p[len(p)-1] == '}' { if d0 := strings.Index(p, "{"); d0 >= 0 && d0 < strings.Index(p, "}") {
params = append(params, i) params = append(params, i)
} }
} }
return return
} }
func extractPathParams(segment string) (params []string) {
for {
d0 := strings.IndexByte(segment, '{')
if d0 < 0 {
break
}
d1 := strings.IndexByte(segment[d0:], '}')
if d1 > 0 {
params = append(params, segment[d0:d0+d1+1])
} else {
break
}
segment = segment[d1:]
}
return params
}
func (s *SpecValidator) validateReferencesValid() *Result { func (s *SpecValidator) validateReferencesValid() *Result {
// each reference must point to a valid object // each reference must point to a valid object
res := new(Result) res := new(Result)
for _, r := range s.analyzer.AllRefs() { for _, r := range s.analyzer.AllRefs() {
if !r.IsValidURI() { if !r.IsValidURI(s.spec.SpecFilePath()) {
res.AddErrors(errors.New(404, "invalid ref %q", r.String())) res.AddErrors(errors.New(404, "invalid ref %q", r.String()))
} }
} }
...@@ -698,7 +736,9 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result { ...@@ -698,7 +736,9 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
} }
// check simple paramters first // check simple paramters first
if param.Default != nil && param.Schema == nil { if param.Default != nil && param.Schema == nil {
//fmt.Println(param.Name, "in", param.In, "has a default without a schema") if Debug {
log.Println(param.Name, "in", param.In, "has a default without a schema")
}
// check param valid // check param valid
res.Merge(NewParamValidator(&param, s.KnownFormats).Validate(param.Default)) res.Merge(NewParamValidator(&param, s.KnownFormats).Validate(param.Default))
} }
...@@ -721,9 +761,15 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result { ...@@ -721,9 +761,15 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
if h.Items != nil { if h.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items)) res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items))
} }
if _, err := regexp.Compile(h.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in default header %q: %q", op.ID, nm, h.Pattern))
} }
} }
for _, r := range op.Responses.StatusCodeResponses { if dr.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema("default", "response", dr.Schema))
}
}
for code, r := range op.Responses.StatusCodeResponses {
for nm, h := range r.Headers { for nm, h := range r.Headers {
if h.Default != nil { if h.Default != nil {
res.Merge(NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default)) res.Merge(NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default))
...@@ -731,6 +777,12 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result { ...@@ -731,6 +777,12 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
if h.Items != nil { if h.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items)) res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items))
} }
if _, err := regexp.Compile(h.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in %v's header %q: %q", op.ID, code, nm, h.Pattern))
}
}
if r.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(strconv.Itoa(code), "response", r.Schema))
} }
} }
...@@ -758,6 +810,9 @@ func (s *SpecValidator) validateDefaultValueSchemaAgainstSchema(path, in string, ...@@ -758,6 +810,9 @@ func (s *SpecValidator) validateDefaultValueSchemaAgainstSchema(path, in string,
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.items[%d]", path, i), in, &sch)) res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.items[%d]", path, i), in, &sch))
} }
} }
if _, err := regexp.Compile(schema.Pattern); err != nil {
res.AddErrors(errors.New(422, "%s in %s has invalid pattern: %q", path, in, schema.Pattern))
}
if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil { if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema)) res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema))
} }
...@@ -787,6 +842,9 @@ func (s *SpecValidator) validateDefaultValueItemsAgainstSchema(path, in string, ...@@ -787,6 +842,9 @@ func (s *SpecValidator) validateDefaultValueItemsAgainstSchema(path, in string,
if items.Items != nil { if items.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(path+"[0]", in, root, items.Items)) res.Merge(s.validateDefaultValueItemsAgainstSchema(path+"[0]", in, root, items.Items))
} }
if _, err := regexp.Compile(items.Pattern); err != nil {
res.AddErrors(errors.New(422, "%s in %s has invalid pattern: %q", path, in, items.Pattern))
}
} }
return res return res
} }
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
package validate package validate
import ( import (
"log"
"reflect" "reflect"
"strings" "strings"
...@@ -32,16 +33,6 @@ type typeValidator struct { ...@@ -32,16 +33,6 @@ type typeValidator struct {
Path string Path string
} }
var jsonTypeNames = map[string]struct{}{
"array": struct{}{},
"boolean": struct{}{},
"integer": struct{}{},
"null": struct{}{},
"number": struct{}{},
"object": struct{}{},
"string": struct{}{},
}
func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) { func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) {
switch data.(type) { switch data.(type) {
case []byte: case []byte:
...@@ -121,7 +112,9 @@ func (t *typeValidator) SetPath(path string) { ...@@ -121,7 +112,9 @@ func (t *typeValidator) SetPath(path string) {
func (t *typeValidator) Applies(source interface{}, kind reflect.Kind) bool { func (t *typeValidator) Applies(source interface{}, kind reflect.Kind) bool {
stpe := reflect.TypeOf(source) stpe := reflect.TypeOf(source)
r := (len(t.Type) > 0 || t.Format != "") && (stpe == specSchemaType || stpe == specParameterType || stpe == specHeaderType) r := (len(t.Type) > 0 || t.Format != "") && (stpe == specSchemaType || stpe == specParameterType || stpe == specHeaderType)
//fmt.Printf("type validator for %q applies %t for %T (kind: %v)\n", t.Path, r, source, kind) if Debug {
log.Printf("type validator for %q applies %t for %T (kind: %v)\n", t.Path, r, source, kind)
}
return r return r
} }
...@@ -140,7 +133,9 @@ func (t *typeValidator) Validate(data interface{}) *Result { ...@@ -140,7 +133,9 @@ func (t *typeValidator) Validate(data interface{}) *Result {
kind := val.Kind() kind := val.Kind()
schType, format := t.schemaInfoForType(data) schType, format := t.schemaInfoForType(data)
//fmt.Println("path:", t.Path, "schType:", schType, "format:", format, "expType:", t.Type, "expFmt:", t.Format, "kind:", val.Kind().String()) if Debug {
log.Println("path:", t.Path, "schType:", schType, "format:", format, "expType:", t.Type, "expFmt:", t.Format, "kind:", val.Kind().String())
}
isLowerInt := t.Format == "int64" && format == "int32" isLowerInt := t.Format == "int64" && format == "int32"
isLowerFloat := t.Format == "float64" && format == "float32" isLowerFloat := t.Format == "float64" && format == "float32"
isFloatInt := schType == "number" && swag.IsFloat64AJSONInteger(val.Float()) && t.Type.Contains("integer") isFloatInt := schType == "number" && swag.IsFloat64AJSONInteger(val.Float()) && t.Type.Contains("integer")
......
#!/bin/bash
set -eu -o pipefail
dir=$(git rev-parse --show-toplevel)
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
function finish {
rm -rf "$scratch"
}
trap finish EXIT SIGHUP SIGINT SIGTERM
cd "$scratch"
git clone https://github.com/json-schema-org/JSON-Schema-Test-Suite Suite
cp -r Suite/tests/draft4/* "$dir/fixtures/jsonschema_suite"
cp -a Suite/remotes "$dir/fixtures/jsonschema_suite"
...@@ -16,6 +16,7 @@ package validate ...@@ -16,6 +16,7 @@ package validate
import ( import (
"fmt" "fmt"
"log"
"reflect" "reflect"
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
...@@ -156,10 +157,17 @@ func (b *basicCommonValidator) Applies(source interface{}, kind reflect.Kind) bo ...@@ -156,10 +157,17 @@ func (b *basicCommonValidator) Applies(source interface{}, kind reflect.Kind) bo
func (b *basicCommonValidator) Validate(data interface{}) (res *Result) { func (b *basicCommonValidator) Validate(data interface{}) (res *Result) {
if len(b.Enum) > 0 { if len(b.Enum) > 0 {
for _, enumValue := range b.Enum { for _, enumValue := range b.Enum {
if data != nil && reflect.DeepEqual(enumValue, data) { actualType := reflect.TypeOf(enumValue)
if actualType == nil {
continue
}
expectedValue := reflect.ValueOf(data)
if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
if reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), enumValue) {
return nil return nil
} }
} }
}
return sErr(errors.EnumFail(b.Path, b.In, data, b.Enum)) return sErr(errors.EnumFail(b.Path, b.In, data, b.Enum))
} }
return nil return nil
...@@ -474,10 +482,14 @@ func (n *numberValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -474,10 +482,14 @@ func (n *numberValidator) Applies(source interface{}, kind reflect.Kind) bool {
isInt := kind >= reflect.Int && kind <= reflect.Uint64 isInt := kind >= reflect.Int && kind <= reflect.Uint64
isFloat := kind == reflect.Float32 || kind == reflect.Float64 isFloat := kind == reflect.Float32 || kind == reflect.Float64
r := isInt || isFloat r := isInt || isFloat
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, r, source, kind) if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, r, source, kind)
}
return r return r
} }
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, false, source, kind) if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, false, source, kind)
}
return false return false
} }
...@@ -536,15 +548,22 @@ func (s *stringValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -536,15 +548,22 @@ func (s *stringValidator) Applies(source interface{}, kind reflect.Kind) bool {
switch source.(type) { switch source.(type) {
case *spec.Parameter, *spec.Schema, *spec.Items, *spec.Header: case *spec.Parameter, *spec.Schema, *spec.Items, *spec.Header:
r := kind == reflect.String r := kind == reflect.String
// fmt.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind) if Debug {
log.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
}
return r return r
} }
// fmt.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, false, source, kind) if Debug {
log.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, false, source, kind)
}
return false return false
} }
func (s *stringValidator) Validate(val interface{}) *Result { func (s *stringValidator) Validate(val interface{}) *Result {
data := val.(string) data, ok := val.(string)
if !ok {
return sErr(errors.InvalidType(s.Path, s.In, "string", val))
}
if s.Required && !s.AllowEmptyValue && (s.Default == nil || s.Default == "") { if s.Required && !s.AllowEmptyValue && (s.Default == nil || s.Default == "") {
if err := RequiredString(s.Path, s.In, data); err != nil { if err := RequiredString(s.Path, s.In, data); err != nil {
......
...@@ -196,7 +196,11 @@ func MinimumUint(path, in string, data, min uint64, exclusive bool) *errors.Vali ...@@ -196,7 +196,11 @@ func MinimumUint(path, in string, data, min uint64, exclusive bool) *errors.Vali
// MultipleOf validates if the provided number is a multiple of the factor // MultipleOf validates if the provided number is a multiple of the factor
func MultipleOf(path, in string, data, factor float64) *errors.Validation { func MultipleOf(path, in string, data, factor float64) *errors.Validation {
if !swag.IsFloat64AJSONInteger(data / factor) { mult := data / factor
if factor < 1 {
mult = 1 / factor * data
}
if !swag.IsFloat64AJSONInteger(mult) {
return errors.NotMultipleOf(path, in, factor) return errors.NotMultipleOf(path, in, factor)
} }
return nil return nil
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment