mirror of https://github.com/k3s-io/k3s
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. ```pull/6/head
commit
6e856480c0
|
@ -1263,7 +1263,7 @@
|
|||
},
|
||||
{
|
||||
"ImportPath": "github.com/go-openapi/validate",
|
||||
"Rev": "deaf2c9013bc1a7f4c774662259a506ba874d80f"
|
||||
"Rev": "d509235108fcf6ab4913d2dcb3a2260c0db2108e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/godbus/dbus",
|
||||
|
|
|
@ -83905,6 +83905,10 @@
|
|||
"description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced",
|
||||
"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": {
|
||||
"description": "Validation describes the validation methods for CustomResources",
|
||||
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation"
|
||||
|
@ -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": {
|
||||
"description": "CustomResourceValidation is a list of validation methods for CustomResources.",
|
||||
"properties": {
|
||||
|
|
|
@ -24,7 +24,7 @@ import (
|
|||
apimeta "k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"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/scale"
|
||||
"k8s.io/kubernetes/pkg/controller/podautoscaler"
|
||||
|
|
|
@ -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/status
|
||||
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/test/integration/testserver
|
||||
staging/src/k8s.io/apimachinery/pkg/api/meta
|
||||
|
|
|
@ -304,7 +304,8 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
|
|||
|
||||
// inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed
|
||||
// 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
|
||||
ServiceProxyAllowExternalIPs: {Default: false, PreRelease: utilfeature.Deprecated},
|
||||
|
|
|
@ -30,6 +30,22 @@
|
|||
"ImportPath": "github.com/beorn7/perks/quantile",
|
||||
"Rev": "3ac7bf7a47d159a033b107610db8a1b6575507a4"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/cockroachdb/cmux",
|
||||
"Rev": "112f0506e7743d64a6eb8fedbcff13d9979bbf92"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/bbolt",
|
||||
"Rev": "48ea1b39c25fc1bab3506fbc712ecbaa842c4d2d"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/alarm",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/auth",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/auth/authpb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
|
@ -42,26 +58,214 @@
|
|||
"ImportPath": "github.com/coreos/etcd/clientv3",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/clientv3/concurrency",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/clientv3/namespace",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/clientv3/naming",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/compactor",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/discovery",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/embed",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/error",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/etcdhttp",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v2http",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v2http/httptypes",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3client",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3election",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3election/v3electionpb/gw",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3lock",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3lock/v3lockpb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3lock/v3lockpb/gw",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3rpc",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/auth",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/etcdserverpb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/etcdserverpb/gw",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/membership",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/etcdserver/stats",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/integration",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/lease",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/lease/leasehttp",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/lease/leasepb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/mvcc",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/mvcc/backend",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/mvcc/mvccpb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/adt",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/contention",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/cors",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/cpuutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/crc",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/debugutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/fileutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/httputil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/idutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/ioutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/logutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/monotime",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/netutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/pathutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/pbutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/runtime",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/schedule",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/srv",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/testutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/tlsutil",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
|
@ -74,10 +278,58 @@
|
|||
"ImportPath": "github.com/coreos/etcd/pkg/types",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/pkg/wait",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/proxy/grpcproxy",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/proxy/grpcproxy/adapter",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/proxy/grpcproxy/cache",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/raft",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/raft/raftpb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/rafthttp",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/snap",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/snap/snappb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/store",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/version",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/wal",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/etcd/wal/walpb",
|
||||
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-semver/semver",
|
||||
"Rev": "568e959cd89871e61434c1143528d9162da89ef2"
|
||||
|
@ -86,10 +338,22 @@
|
|||
"ImportPath": "github.com/coreos/go-systemd/daemon",
|
||||
"Rev": "48702e0da86bd25e76cfef347e2adeb434a0d0a6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/go-systemd/journal",
|
||||
"Rev": "48702e0da86bd25e76cfef347e2adeb434a0d0a6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/coreos/pkg/capnslog",
|
||||
"Rev": "fa29b1d70f0beaddd4c7021607cc3c3be8ce94b8"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/davecgh/go-spew/spew",
|
||||
"Rev": "782f4967f2dc4564575ca782fe2d04090b5faca8"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/dgrijalva/jwt-go",
|
||||
"Rev": "01aeca54ebda6e0fbfafd0a524d234159c05ec20"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/elazarl/go-bindata-assetfs",
|
||||
"Rev": "3dcc96556217539f50599357fb481ac0dc7439b9"
|
||||
|
@ -152,7 +416,7 @@
|
|||
},
|
||||
{
|
||||
"ImportPath": "github.com/go-openapi/validate",
|
||||
"Rev": "deaf2c9013bc1a7f4c774662259a506ba874d80f"
|
||||
"Rev": "d509235108fcf6ab4913d2dcb3a2260c0db2108e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gogo/protobuf/proto",
|
||||
|
@ -166,6 +430,14 @@
|
|||
"ImportPath": "github.com/golang/glog",
|
||||
"Rev": "44145f04b68cf362d9c4df2182967c2275eaefed"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/groupcache/lru",
|
||||
"Rev": "02826c3e79038b59d737d3b1c0a1d937f71a4433"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/protobuf/jsonpb",
|
||||
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/protobuf/proto",
|
||||
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
|
||||
|
@ -186,10 +458,18 @@
|
|||
"ImportPath": "github.com/golang/protobuf/ptypes/duration",
|
||||
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/protobuf/ptypes/struct",
|
||||
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/protobuf/ptypes/timestamp",
|
||||
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/btree",
|
||||
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/google/gofuzz",
|
||||
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
|
@ -206,6 +486,22 @@
|
|||
"ImportPath": "github.com/googleapis/gnostic/extensions",
|
||||
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/grpc-ecosystem/go-grpc-prometheus",
|
||||
"Rev": "2500245aa6110c562d17020fb31a2c133d737799"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/grpc-ecosystem/grpc-gateway/runtime",
|
||||
"Rev": "8cc3a55af3bcf171a1c23a90c4df9cf591706104"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/grpc-ecosystem/grpc-gateway/runtime/internal",
|
||||
"Rev": "8cc3a55af3bcf171a1c23a90c4df9cf591706104"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/grpc-ecosystem/grpc-gateway/utilities",
|
||||
"Rev": "8cc3a55af3bcf171a1c23a90c4df9cf591706104"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/hashicorp/golang-lru",
|
||||
"Rev": "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4"
|
||||
|
@ -226,6 +522,10 @@
|
|||
"ImportPath": "github.com/inconshreveable/mousetrap",
|
||||
"Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/jonboulle/clockwork",
|
||||
"Rev": "72f9bd7c4e0c2a40055ab3d0f09654f730cce982"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/json-iterator/go",
|
||||
"Rev": "13f86432b882000a51c6e610c620974462691a97"
|
||||
|
@ -302,6 +602,18 @@
|
|||
"ImportPath": "github.com/ugorji/go/codec",
|
||||
"Rev": "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/xiang90/probing",
|
||||
"Rev": "07dd2e8dfe18522e9c447ba95f2fe95262f63bb2"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/bcrypt",
|
||||
"Rev": "81e90905daefcd6fd217b62423c0908922eadb30"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/blowfish",
|
||||
"Rev": "81e90905daefcd6fd217b62423c0908922eadb30"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/ssh/terminal",
|
||||
"Rev": "81e90905daefcd6fd217b62423c0908922eadb30"
|
||||
|
@ -1034,6 +1346,10 @@
|
|||
"ImportPath": "k8s.io/apiserver/pkg/registry/rest",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/registry/rest/resttest",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/server",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1082,10 +1398,18 @@
|
|||
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/etcdtest",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/metrics",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/testing/testingcert",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/util",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1110,6 +1434,10 @@
|
|||
"ImportPath": "k8s.io/apiserver/pkg/storage/storagebackend/factory",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/storage/testing",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/storage/value",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1158,6 +1486,10 @@
|
|||
"ImportPath": "k8s.io/client-go/discovery",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/dynamic",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/informers",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1546,6 +1878,34 @@
|
|||
"ImportPath": "k8s.io/client-go/rest/watch",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale/scheme",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale/scheme/appsint",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale/scheme/appsv1beta1",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale/scheme/appsv1beta2",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale/scheme/autoscalingv1",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale/scheme/extensionsint",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale/scheme/extensionsv1beta1",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/testing",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1646,6 +2006,10 @@
|
|||
"ImportPath": "k8s.io/apimachinery/pkg/api/meta",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery/pkg/api/resource",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery/pkg/api/testing/fuzzer",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1666,6 +2030,10 @@
|
|||
"ImportPath": "k8s.io/apimachinery/pkg/apimachinery/registered",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/internalversion",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1710,10 +2078,18 @@
|
|||
"ImportPath": "k8s.io/apimachinery/pkg/types",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery/pkg/util/diff",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery/pkg/util/errors",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery/pkg/util/intstr",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apimachinery/pkg/util/json",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1778,6 +2154,10 @@
|
|||
"ImportPath": "k8s.io/apiserver/pkg/registry/generic/registry",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/registry/generic/testing",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/registry/rest",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1802,6 +2182,10 @@
|
|||
"ImportPath": "k8s.io/apiserver/pkg/storage/errors",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/testing",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/storage/names",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1814,6 +2198,10 @@
|
|||
"ImportPath": "k8s.io/apiserver/pkg/util/feature",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/util/feature/testing",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/apiserver/pkg/util/logs",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
@ -1834,6 +2222,14 @@
|
|||
"ImportPath": "k8s.io/client-go/rest",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/scale/scheme/autoscalingv1",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
{
|
||||
"ImportPath": "k8s.io/client-go/testing",
|
||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
|
|
@ -16,7 +16,9 @@ limitations under the License.
|
|||
|
||||
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
|
||||
type CustomResourceDefinitionSpec struct {
|
||||
|
@ -30,6 +32,8 @@ type CustomResourceDefinitionSpec struct {
|
|||
Scope ResourceScope
|
||||
// Validation describes the validation methods for CustomResources
|
||||
Validation *CustomResourceValidation
|
||||
// Subresources describes the subresources for CustomResources
|
||||
Subresources *CustomResourceSubresources
|
||||
}
|
||||
|
||||
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
|
||||
|
@ -146,3 +150,41 @@ type CustomResourceValidation struct {
|
|||
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
|
||||
OpenAPIV3Schema *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
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -107,6 +107,12 @@ message CustomResourceDefinitionSpec {
|
|||
// Validation describes the validation methods for CustomResources
|
||||
// +optional
|
||||
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
|
||||
|
@ -119,6 +125,48 @@ message CustomResourceDefinitionStatus {
|
|||
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.
|
||||
message CustomResourceValidation {
|
||||
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
|
||||
|
|
|
@ -16,7 +16,9 @@ limitations under the License.
|
|||
|
||||
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
|
||||
type CustomResourceDefinitionSpec struct {
|
||||
|
@ -31,6 +33,11 @@ type CustomResourceDefinitionSpec struct {
|
|||
// Validation describes the validation methods for CustomResources
|
||||
// +optional
|
||||
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
|
||||
|
@ -147,3 +154,41 @@ type CustomResourceValidation struct {
|
|||
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
|
||||
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 {
|
|||
Convert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomResourceDefinitionSpec,
|
||||
Convert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_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_apiextensions_CustomResourceValidation_To_v1beta1_CustomResourceValidation,
|
||||
Convert_v1beta1_ExternalDocumentation_To_apiextensions_ExternalDocumentation,
|
||||
|
@ -211,6 +217,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes
|
|||
} else {
|
||||
out.Validation = nil
|
||||
}
|
||||
out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -235,6 +242,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes
|
|||
} else {
|
||||
out.Validation = nil
|
||||
}
|
||||
out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -269,6 +277,70 @@ func Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResou
|
|||
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 {
|
||||
if in.OpenAPIV3Schema != nil {
|
||||
in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema
|
||||
|
|
|
@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
|
|||
(*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
|
||||
}
|
||||
|
||||
|
@ -175,6 +184,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
|
|||
return out
|
||||
}
|
||||
|
||||
// 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) {
|
||||
*out = *in
|
||||
|
|
|
@ -18,6 +18,7 @@ package validation
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
|
@ -107,7 +108,13 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
|
|||
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, fldPath.Child("validation"))...)
|
||||
} 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
|
||||
|
@ -182,9 +189,27 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
|
|||
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{}
|
||||
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.
|
||||
|
@ -326,3 +351,64 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
|
|||
|
||||
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
|
|||
(*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
|
||||
}
|
||||
|
||||
|
@ -175,6 +184,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
|
|||
return out
|
||||
}
|
||||
|
||||
// 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) {
|
||||
*out = *in
|
||||
|
|
|
@ -16,7 +16,11 @@ go_library(
|
|||
],
|
||||
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver",
|
||||
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/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/install:go_default_library",
|
||||
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_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/controller/finalizer: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/customresourcedefinition:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
|
||||
|
@ -60,7 +65,10 @@ go_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/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/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/util/workqueue:go_default_library",
|
||||
],
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
|
||||
"github.com/golang/glog"
|
||||
|
||||
autoscaling "k8s.io/api/autoscaling/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
@ -117,6 +118,26 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
|
|||
Verbs: verbs,
|
||||
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 {
|
||||
|
|
|
@ -25,6 +25,9 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/validate"
|
||||
"github.com/golang/glog"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
|
@ -35,6 +38,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/versioning"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
@ -47,7 +51,10 @@ import (
|
|||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/storage/storagebackend"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/scale"
|
||||
"k8s.io/client-go/scale/scheme/autoscalingv1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
|
@ -55,6 +62,7 @@ import (
|
|||
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
|
||||
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
|
||||
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
|
||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
|
||||
)
|
||||
|
||||
|
@ -87,8 +95,11 @@ type crdInfo struct {
|
|||
spec *apiextensions.CustomResourceDefinitionSpec
|
||||
acceptedNames *apiextensions.CustomResourceDefinitionNames
|
||||
|
||||
storage *customresource.REST
|
||||
requestScope handlers.RequestScope
|
||||
storage customresource.CustomResourceStorage
|
||||
|
||||
requestScope handlers.RequestScope
|
||||
scaleRequestScope handlers.RequestScope
|
||||
statusRequestScope handlers.RequestScope
|
||||
}
|
||||
|
||||
// crdStorageMap goes from customresourcedefinition to its storage
|
||||
|
@ -172,10 +183,6 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
r.delegate.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
if len(requestInfo.Subresource) > 0 {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating)
|
||||
|
||||
|
@ -185,61 +192,126 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
storage := crdInfo.storage
|
||||
requestScope := crdInfo.requestScope
|
||||
minRequestTimeout := 1 * time.Minute
|
||||
|
||||
verb := strings.ToUpper(requestInfo.Verb)
|
||||
resource := requestInfo.Resource
|
||||
subresource := requestInfo.Subresource
|
||||
scope := metrics.CleanScope(requestInfo)
|
||||
supportedTypes := []string{
|
||||
string(types.JSONPatchType),
|
||||
string(types.MergePatchType),
|
||||
}
|
||||
|
||||
var handler http.HandlerFunc
|
||||
switch {
|
||||
case subresource == "status" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil:
|
||||
handler = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
|
||||
case subresource == "scale" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil:
|
||||
handler = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
|
||||
case len(subresource) == 0:
|
||||
handler = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
|
||||
default:
|
||||
http.Error(w, "the server could not find the requested resource", http.StatusNotFound)
|
||||
}
|
||||
|
||||
if handler != nil {
|
||||
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
|
||||
handler(w, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||
requestScope := crdInfo.requestScope
|
||||
storage := crdInfo.storage.CustomResource
|
||||
minRequestTimeout := 1 * time.Minute
|
||||
|
||||
switch requestInfo.Verb {
|
||||
case "get":
|
||||
handler = handlers.GetResource(storage, storage, requestScope)
|
||||
return handlers.GetResource(storage, storage, requestScope)
|
||||
case "list":
|
||||
forceWatch := false
|
||||
handler = handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
|
||||
return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
|
||||
case "watch":
|
||||
forceWatch := true
|
||||
handler = handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
|
||||
return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
|
||||
case "create":
|
||||
if terminating {
|
||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
handler = handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
|
||||
return handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
|
||||
case "update":
|
||||
if terminating {
|
||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
handler = handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
|
||||
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
|
||||
case "patch":
|
||||
if terminating {
|
||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
supportedTypes := []string{
|
||||
string(types.JSONPatchType),
|
||||
string(types.MergePatchType),
|
||||
}
|
||||
handler = handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
|
||||
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
|
||||
case "delete":
|
||||
allowsOptions := true
|
||||
handler = handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
|
||||
return handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
|
||||
case "deletecollection":
|
||||
checkBody := true
|
||||
handler = handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
|
||||
return handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||
requestScope := crdInfo.statusRequestScope
|
||||
storage := crdInfo.storage.Status
|
||||
|
||||
switch requestInfo.Verb {
|
||||
case "get":
|
||||
return handlers.GetResource(storage, nil, requestScope)
|
||||
case "update":
|
||||
if terminating {
|
||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
|
||||
case "patch":
|
||||
if terminating {
|
||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *crdHandler) serveScale(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
|
||||
requestScope := crdInfo.scaleRequestScope
|
||||
storage := crdInfo.storage.Scale
|
||||
|
||||
switch requestInfo.Verb {
|
||||
case "get":
|
||||
return handlers.GetResource(storage, nil, requestScope)
|
||||
case "update":
|
||||
if terminating {
|
||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
|
||||
case "patch":
|
||||
if terminating {
|
||||
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
|
||||
default:
|
||||
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
|
||||
handler(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) {
|
||||
|
@ -265,7 +337,8 @@ func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{})
|
|||
// as it is used without locking elsewhere.
|
||||
storageMap2 := storageMap.clone()
|
||||
if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok {
|
||||
oldInfo.storage.DestroyFunc()
|
||||
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
|
||||
oldInfo.storage.CustomResource.DestroyFunc()
|
||||
delete(storageMap2, types.UID(oldCRD.UID))
|
||||
}
|
||||
|
||||
|
@ -297,7 +370,8 @@ func (r *crdHandler) removeDeadStorage() {
|
|||
}
|
||||
if !found {
|
||||
glog.V(4).Infof("Removing dead CRD storage for %v", s.requestScope.Resource)
|
||||
s.storage.DestroyFunc()
|
||||
// destroy only the main storage. Those for the subresources share cacher and etcd clients.
|
||||
s.storage.CustomResource.DestroyFunc()
|
||||
delete(storageMap2, uid)
|
||||
}
|
||||
}
|
||||
|
@ -311,7 +385,7 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info.storage, nil
|
||||
return info.storage.CustomResource, nil
|
||||
}
|
||||
|
||||
func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) {
|
||||
|
@ -340,9 +414,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||
parameterCodec := runtime.NewParameterCodec(parameterScheme)
|
||||
|
||||
kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.Kind}
|
||||
typer := unstructuredObjectTyper{
|
||||
delegate: parameterScheme,
|
||||
unstructuredTyper: discovery.NewUnstructuredObjectTyper(nil),
|
||||
typer := UnstructuredObjectTyper{
|
||||
Delegate: parameterScheme,
|
||||
UnstructuredTyper: discovery.NewUnstructuredObjectTyper(nil),
|
||||
}
|
||||
creator := unstructuredCreator{}
|
||||
|
||||
|
@ -351,7 +425,29 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||
return nil, err
|
||||
}
|
||||
|
||||
storage := customresource.NewREST(
|
||||
var statusSpec *apiextensions.CustomResourceSubresourceStatus
|
||||
var statusValidator *validate.SchemaValidator
|
||||
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
|
||||
statusSpec = crd.Spec.Subresources.Status
|
||||
|
||||
// for the status subresource, validate only against the status schema
|
||||
if crd.Spec.Validation != nil && crd.Spec.Validation.OpenAPIV3Schema != nil && crd.Spec.Validation.OpenAPIV3Schema.Properties != nil {
|
||||
if statusSchema, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["status"]; ok {
|
||||
openapiSchema := &spec.Schema{}
|
||||
if err := apiservervalidation.ConvertJSONSchemaProps(&statusSchema, openapiSchema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
statusValidator = validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var scaleSpec *apiextensions.CustomResourceSubresourceScale
|
||||
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
|
||||
scaleSpec = crd.Spec.Subresources.Scale
|
||||
}
|
||||
|
||||
customResourceStorage := customresource.NewStorage(
|
||||
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural},
|
||||
schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.ListKind},
|
||||
customresource.NewStrategy(
|
||||
|
@ -359,6 +455,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||
crd.Spec.Scope == apiextensions.NamespaceScoped,
|
||||
kind,
|
||||
validator,
|
||||
statusValidator,
|
||||
statusSpec,
|
||||
scaleSpec,
|
||||
),
|
||||
r.restOptionsGetter,
|
||||
)
|
||||
|
@ -373,12 +472,15 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||
|
||||
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
|
||||
|
||||
var ctxFn handlers.ContextFunc
|
||||
ctxFn = func(req *http.Request) apirequest.Context {
|
||||
ret, _ := r.requestContextMapper.Get(req)
|
||||
return ret
|
||||
}
|
||||
|
||||
requestScope := handlers.RequestScope{
|
||||
Namer: handlers.ContextBasedNaming{
|
||||
GetContext: func(req *http.Request) apirequest.Context {
|
||||
ret, _ := r.requestContextMapper.Get(req)
|
||||
return ret
|
||||
},
|
||||
GetContext: ctxFn,
|
||||
SelfLinker: meta.NewAccessor(),
|
||||
ClusterScoped: clusterScoped,
|
||||
SelfLinkPathPrefix: selfLinkPrefix,
|
||||
|
@ -400,9 +502,8 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||
Typer: typer,
|
||||
UnsafeConvertor: unstructured.UnstructuredObjectConverter{},
|
||||
|
||||
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural},
|
||||
Kind: kind,
|
||||
Subresource: "",
|
||||
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural},
|
||||
Kind: kind,
|
||||
|
||||
MetaGroupVersion: metav1.SchemeGroupVersion,
|
||||
}
|
||||
|
@ -411,8 +512,33 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
|
|||
spec: &crd.Spec,
|
||||
acceptedNames: &crd.Status.AcceptedNames,
|
||||
|
||||
storage: storage,
|
||||
requestScope: requestScope,
|
||||
storage: customResourceStorage,
|
||||
requestScope: requestScope,
|
||||
scaleRequestScope: requestScope, // shallow copy
|
||||
statusRequestScope: requestScope, // shallow copy
|
||||
}
|
||||
|
||||
// override scaleSpec subresource values
|
||||
scaleConverter := scale.NewScaleConverter()
|
||||
ret.scaleRequestScope.Subresource = "scale"
|
||||
ret.scaleRequestScope.Serializer = serializer.NewCodecFactory(scaleConverter.Scheme())
|
||||
ret.scaleRequestScope.Kind = autoscalingv1.SchemeGroupVersion.WithKind("Scale")
|
||||
ret.scaleRequestScope.Namer = handlers.ContextBasedNaming{
|
||||
GetContext: ctxFn,
|
||||
SelfLinker: meta.NewAccessor(),
|
||||
ClusterScoped: clusterScoped,
|
||||
SelfLinkPathPrefix: selfLinkPrefix,
|
||||
SelfLinkPathSuffix: "/scale",
|
||||
}
|
||||
|
||||
// override status subresource values
|
||||
ret.statusRequestScope.Subresource = "status"
|
||||
ret.statusRequestScope.Namer = handlers.ContextBasedNaming{
|
||||
GetContext: ctxFn,
|
||||
SelfLinker: meta.NewAccessor(),
|
||||
ClusterScoped: clusterScoped,
|
||||
SelfLinkPathPrefix: selfLinkPrefix,
|
||||
SelfLinkPathSuffix: "/status",
|
||||
}
|
||||
|
||||
// Copy because we cannot write to storageMap without a race
|
||||
|
@ -477,21 +603,21 @@ func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decod
|
|||
return versioning.NewDefaultingCodecForScheme(Scheme, nil, decoder, nil, gv)
|
||||
}
|
||||
|
||||
type unstructuredObjectTyper struct {
|
||||
delegate runtime.ObjectTyper
|
||||
unstructuredTyper runtime.ObjectTyper
|
||||
type UnstructuredObjectTyper struct {
|
||||
Delegate runtime.ObjectTyper
|
||||
UnstructuredTyper runtime.ObjectTyper
|
||||
}
|
||||
|
||||
func (t unstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
|
||||
func (t UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
|
||||
// Delegate for things other than Unstructured.
|
||||
if _, ok := obj.(runtime.Unstructured); !ok {
|
||||
return t.delegate.ObjectKinds(obj)
|
||||
return t.Delegate.ObjectKinds(obj)
|
||||
}
|
||||
return t.unstructuredTyper.ObjectKinds(obj)
|
||||
return t.UnstructuredTyper.ObjectKinds(obj)
|
||||
}
|
||||
|
||||
func (t unstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
|
||||
return t.delegate.Recognizes(gvk) || t.unstructuredTyper.Recognizes(gvk)
|
||||
func (t UnstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
|
||||
return t.Delegate.Recognizes(gvk) || t.UnstructuredTyper.Recognizes(gvk)
|
||||
}
|
||||
|
||||
type unstructuredCreator struct{}
|
||||
|
|
|
@ -29,7 +29,7 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
|
|||
// Convert CRD schema to openapi schema
|
||||
openapiSchema := &spec.Schema{}
|
||||
if customResourceValidation != nil {
|
||||
if err := convertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
|
||||
if err := ConvertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,10 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
|
|||
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
|
||||
// CustomResource is a JSON data structure.
|
||||
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
|
||||
if validator == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := validator.Validate(customResource)
|
||||
if result.AsError() != nil {
|
||||
return result.AsError()
|
||||
|
@ -46,7 +50,8 @@ func ValidateCustomResource(customResource interface{}, validator *validate.Sche
|
|||
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 {
|
||||
return nil
|
||||
}
|
||||
|
@ -99,7 +104,7 @@ func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema)
|
|||
if in.Not != nil {
|
||||
in, out := &in.Not, &out.Not
|
||||
*out = new(spec.Schema)
|
||||
if err := convertJSONSchemaProps(*in, *out); err != nil {
|
||||
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -176,7 +181,7 @@ func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]s
|
|||
if in != nil {
|
||||
for _, jsonSchemaProps := range *in {
|
||||
schema := spec.Schema{}
|
||||
if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
|
||||
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
|
||||
return err
|
||||
}
|
||||
*out = append(*out, schema)
|
||||
|
@ -190,7 +195,7 @@ func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (m
|
|||
if len(in) != 0 {
|
||||
for k, jsonSchemaProps := range in {
|
||||
schema := spec.Schema{}
|
||||
if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
|
||||
if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out[k] = schema
|
||||
|
@ -203,7 +208,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
|
|||
if in.Schema != nil {
|
||||
in, out := &in.Schema, &out.Schema
|
||||
*out = new(spec.Schema)
|
||||
if err := convertJSONSchemaProps(*in, *out); err != nil {
|
||||
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -211,7 +216,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
|
|||
in, out := &in.JSONSchemas, &out.Schemas
|
||||
*out = make([]spec.Schema, len(*in))
|
||||
for i := range *in {
|
||||
if err := convertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
|
||||
if err := ConvertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -224,7 +229,7 @@ func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *
|
|||
if in.Schema != nil {
|
||||
in, out := &in.Schema, &out.Schema
|
||||
*out = new(spec.Schema)
|
||||
if err := convertJSONSchemaProps(*in, *out); err != nil {
|
||||
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -236,7 +241,7 @@ func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStri
|
|||
if in.Schema != nil {
|
||||
in, out := &in.Schema, &out.Schema
|
||||
*out = new(spec.Schema)
|
||||
if err := convertJSONSchemaProps(*in, *out); err != nil {
|
||||
if err := ConvertJSONSchemaProps(*in, *out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ func TestRoundTrip(t *testing.T) {
|
|||
|
||||
// internal -> go-openapi
|
||||
openAPITypes := &spec.Schema{}
|
||||
if err := convertJSONSchemaProps(internal, openAPITypes); err != nil {
|
||||
if err := ConvertJSONSchemaProps(internal, openAPITypes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,12 @@ const (
|
|||
//
|
||||
// CustomResourceValidation is a list of validation methods for CustomResources
|
||||
CustomResourceValidation utilfeature.Feature = "CustomResourceValidation"
|
||||
|
||||
// owner: @sttts, @nikhita
|
||||
// alpha: v1.10
|
||||
//
|
||||
// CustomResourceSubresources defines the subresources for CustomResources
|
||||
CustomResourceSubresources utilfeature.Feature = "CustomResourceSubresources"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -43,5 +49,6 @@ func init() {
|
|||
// To add a new feature, define a key for it above and add it here. The features will be
|
||||
// available throughout Kubernetes binaries.
|
||||
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"])
|
|||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"etcd.go",
|
||||
"registry.go",
|
||||
"status_strategy.go",
|
||||
"strategy.go",
|
||||
"validator.go",
|
||||
],
|
||||
importpath = "k8s.io/apiextensions-apiserver/pkg/registry/customresource",
|
||||
deps = [
|
||||
"//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/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/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/unstructured:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
|
||||
|
@ -24,11 +34,14 @@ go_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/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/registry/generic: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/names:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -44,3 +57,27 @@ filegroup(
|
|||
srcs = [":package-srcs"],
|
||||
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.
|
|||
package customresource
|
||||
|
||||
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/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
)
|
||||
|
||||
// CustomResourceStorage includes dummy storage for CustomResources, and their Status and Scale subresources.
|
||||
type CustomResourceStorage struct {
|
||||
CustomResource *REST
|
||||
Status *StatusREST
|
||||
Scale *ScaleREST
|
||||
}
|
||||
|
||||
func NewStorage(resource schema.GroupResource, 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
|
||||
type REST struct {
|
||||
*genericregistry.Store
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// newREST returns a RESTStorage object that will work against API services.
|
||||
func newREST(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST) {
|
||||
store := &genericregistry.Store{
|
||||
NewFunc: func() runtime.Object { return &unstructured.Unstructured{} },
|
||||
NewListFunc: func() runtime.Object {
|
||||
|
@ -50,5 +94,161 @@ func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, st
|
|||
if err := store.CompleteWithOptions(options); err != nil {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,380 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package customresource_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/diff"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
registrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
etcdtesting "k8s.io/apiserver/pkg/storage/etcd/testing"
|
||||
"k8s.io/client-go/discovery"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver"
|
||||
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
|
||||
)
|
||||
|
||||
func newStorage(t *testing.T) (customresource.CustomResourceStorage, *etcdtesting.EtcdTestServer) {
|
||||
server, etcdStorage := etcdtesting.NewUnsecuredEtcd3TestClientServer(t)
|
||||
etcdStorage.Codec = unstructuredJsonCodec{}
|
||||
restOptions := generic.RESTOptions{StorageConfig: etcdStorage, Decorator: generic.UndecoratedStorage, DeleteCollectionWorkers: 1, ResourcePrefix: "noxus"}
|
||||
|
||||
parameterScheme := runtime.NewScheme()
|
||||
parameterScheme.AddUnversionedTypes(schema.GroupVersion{Group: "mygroup.example.com", Version: "v1beta1"},
|
||||
&metav1.ListOptions{},
|
||||
&metav1.ExportOptions{},
|
||||
&metav1.GetOptions{},
|
||||
&metav1.DeleteOptions{},
|
||||
)
|
||||
|
||||
typer := apiserver.UnstructuredObjectTyper{
|
||||
Delegate: parameterScheme,
|
||||
UnstructuredTyper: discovery.NewUnstructuredObjectTyper(nil),
|
||||
}
|
||||
|
||||
kind := schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "Noxu"}
|
||||
|
||||
labelSelectorPath := ".status.labelSelector"
|
||||
scale := &apiextensions.CustomResourceSubresourceScale{
|
||||
SpecReplicasPath: ".spec.replicas",
|
||||
StatusReplicasPath: ".status.replicas",
|
||||
LabelSelectorPath: &labelSelectorPath,
|
||||
}
|
||||
|
||||
status := &apiextensions.CustomResourceSubresourceStatus{}
|
||||
|
||||
storage := customresource.NewStorage(
|
||||
schema.GroupResource{Group: "mygroup.example.com", Resource: "noxus"},
|
||||
schema.GroupVersionKind{Group: "mygroup.example.com", Version: "v1beta1", Kind: "NoxuItemList"},
|
||||
customresource.NewStrategy(
|
||||
typer,
|
||||
true,
|
||||
kind,
|
||||
nil,
|
||||
nil,
|
||||
status,
|
||||
scale,
|
||||
),
|
||||
restOptions,
|
||||
)
|
||||
|
||||
return storage, server
|
||||
}
|
||||
|
||||
// createCustomResource is a helper function that returns a CustomResource with the updated resource version.
|
||||
func createCustomResource(storage *customresource.REST, cr unstructured.Unstructured, t *testing.T) (unstructured.Unstructured, error) {
|
||||
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), cr.GetNamespace())
|
||||
obj, err := storage.Create(ctx, &cr, rest.ValidateAllObjectFunc, false)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create CustomResource, %v", err)
|
||||
}
|
||||
newCR := obj.(*unstructured.Unstructured)
|
||||
return *newCR, nil
|
||||
}
|
||||
|
||||
func validNewCustomResource() *unstructured.Unstructured {
|
||||
return &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "mygroup.example.com/v1beta1",
|
||||
"kind": "Noxu",
|
||||
"metadata": map[string]interface{}{
|
||||
"namespace": "default",
|
||||
"name": "foo",
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"replicas": int64(7),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var validCustomResource = *validNewCustomResource()
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
test := registrytest.New(t, storage.CustomResource.Store)
|
||||
cr := validNewCustomResource()
|
||||
cr.SetNamespace("")
|
||||
test.TestCreate(
|
||||
cr,
|
||||
)
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
test := registrytest.New(t, storage.CustomResource.Store)
|
||||
test.TestGet(validNewCustomResource())
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
test := registrytest.New(t, storage.CustomResource.Store)
|
||||
test.TestList(validNewCustomResource())
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
test := registrytest.New(t, storage.CustomResource.Store)
|
||||
test.TestDelete(validNewCustomResource())
|
||||
}
|
||||
|
||||
func TestStatusUpdate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
|
||||
key := "/noxus/" + metav1.NamespaceDefault + "/foo"
|
||||
validCustomResource := validNewCustomResource()
|
||||
if err := storage.CustomResource.Storage.Create(ctx, key, validCustomResource, nil, 0); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
gottenObj, err := storage.CustomResource.Get(ctx, "foo", &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
update := gottenObj.(*unstructured.Unstructured)
|
||||
updateContent := update.Object
|
||||
updateContent["status"] = map[string]interface{}{
|
||||
"replicas": int64(7),
|
||||
}
|
||||
|
||||
if _, _, err := storage.Status.Update(ctx, update.GetName(), rest.DefaultUpdatedObjectInfo(update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
obj, err := storage.CustomResource.Get(ctx, "foo", &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
cr, ok := obj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
t.Fatal("unexpected error: custom resource should be of type Unstructured")
|
||||
}
|
||||
content := cr.UnstructuredContent()
|
||||
|
||||
spec := content["spec"].(map[string]interface{})
|
||||
status := content["status"].(map[string]interface{})
|
||||
|
||||
if spec["replicas"].(int64) != 7 {
|
||||
t.Errorf("we expected .spec.replicas to not be updated but it was updated to %v", spec["replicas"].(int64))
|
||||
}
|
||||
if status["replicas"].(int64) != 7 {
|
||||
t.Errorf("we expected .status.replicas to be updated to %d but it was %v", 7, status["replicas"].(int64))
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaleGet(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
|
||||
name := "foo"
|
||||
|
||||
var cr unstructured.Unstructured
|
||||
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
|
||||
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
|
||||
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0); err != nil {
|
||||
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
|
||||
}
|
||||
|
||||
want := &autoscalingv1.Scale{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: cr.GetName(),
|
||||
Namespace: metav1.NamespaceDefault,
|
||||
UID: cr.GetUID(),
|
||||
ResourceVersion: cr.GetResourceVersion(),
|
||||
CreationTimestamp: cr.GetCreationTimestamp(),
|
||||
},
|
||||
Spec: autoscalingv1.ScaleSpec{
|
||||
Replicas: int32(7),
|
||||
},
|
||||
}
|
||||
|
||||
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("error fetching scale for %s: %v", name, err)
|
||||
}
|
||||
|
||||
got := obj.(*autoscalingv1.Scale)
|
||||
if !apiequality.Semantic.DeepEqual(got, want) {
|
||||
t.Errorf("unexpected scale: %s", diff.ObjectDiff(got, want))
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaleGetWithoutSpecReplicas(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
|
||||
name := "foo"
|
||||
|
||||
var cr unstructured.Unstructured
|
||||
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
|
||||
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
|
||||
withoutSpecReplicas := validCustomResource.DeepCopy()
|
||||
unstructured.RemoveNestedField(withoutSpecReplicas.Object, "spec", "replicas")
|
||||
if err := storage.CustomResource.Storage.Create(ctx, key, withoutSpecReplicas, &cr, 0); err != nil {
|
||||
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, withoutSpecReplicas, err)
|
||||
}
|
||||
|
||||
_, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("error expected for %s", name)
|
||||
}
|
||||
if expected := `the spec replicas field ".spec.replicas" does not exist`; !strings.Contains(err.Error(), expected) {
|
||||
t.Fatalf("expected error string %q, got: %v", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaleUpdate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
|
||||
name := "foo"
|
||||
|
||||
var cr unstructured.Unstructured
|
||||
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
|
||||
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
|
||||
if err := storage.CustomResource.Storage.Create(ctx, key, &validCustomResource, &cr, 0); err != nil {
|
||||
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, validCustomResource, err)
|
||||
}
|
||||
|
||||
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("error fetching scale for %s: %v", name, err)
|
||||
}
|
||||
scale, ok := obj.(*autoscalingv1.Scale)
|
||||
if !ok {
|
||||
t.Fatalf("%v is not of the type autoscalingv1.Scale", scale)
|
||||
}
|
||||
|
||||
replicas := 12
|
||||
update := autoscalingv1.Scale{
|
||||
ObjectMeta: scale.ObjectMeta,
|
||||
Spec: autoscalingv1.ScaleSpec{
|
||||
Replicas: int32(replicas),
|
||||
},
|
||||
}
|
||||
|
||||
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc); err != nil {
|
||||
t.Fatalf("error updating scale %v: %v", update, err)
|
||||
}
|
||||
|
||||
obj, err = storage.Scale.Get(ctx, name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("error fetching scale for %s: %v", name, err)
|
||||
}
|
||||
scale = obj.(*autoscalingv1.Scale)
|
||||
if scale.Spec.Replicas != int32(replicas) {
|
||||
t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
|
||||
}
|
||||
|
||||
update.ResourceVersion = scale.ResourceVersion
|
||||
update.Spec.Replicas = 15
|
||||
|
||||
if _, _, err = storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc); err != nil && !errors.IsConflict(err) {
|
||||
t.Fatalf("unexpected error, expecting an update conflict but got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaleUpdateWithoutSpecReplicas(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.CustomResource.Store.DestroyFunc()
|
||||
|
||||
name := "foo"
|
||||
|
||||
var cr unstructured.Unstructured
|
||||
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
|
||||
key := "/noxus/" + metav1.NamespaceDefault + "/" + name
|
||||
withoutSpecReplicas := validCustomResource.DeepCopy()
|
||||
unstructured.RemoveNestedField(withoutSpecReplicas.Object, "spec", "replicas")
|
||||
if err := storage.CustomResource.Storage.Create(ctx, key, withoutSpecReplicas, &cr, 0); err != nil {
|
||||
t.Fatalf("error setting new custom resource (key: %s) %v: %v", key, withoutSpecReplicas, err)
|
||||
}
|
||||
|
||||
replicas := 12
|
||||
update := autoscalingv1.Scale{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
ResourceVersion: cr.GetResourceVersion(),
|
||||
},
|
||||
Spec: autoscalingv1.ScaleSpec{
|
||||
Replicas: int32(replicas),
|
||||
},
|
||||
}
|
||||
|
||||
if _, _, err := storage.Scale.Update(ctx, update.Name, rest.DefaultUpdatedObjectInfo(&update), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc); err != nil {
|
||||
t.Fatalf("error updating scale %v: %v", update, err)
|
||||
}
|
||||
|
||||
obj, err := storage.Scale.Get(ctx, name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("error fetching scale for %s: %v", name, err)
|
||||
}
|
||||
scale := obj.(*autoscalingv1.Scale)
|
||||
if scale.Spec.Replicas != int32(replicas) {
|
||||
t.Errorf("wrong replicas count: expected: %d got: %d", replicas, scale.Spec.Replicas)
|
||||
}
|
||||
}
|
||||
|
||||
type unstructuredJsonCodec struct{}
|
||||
|
||||
func (c unstructuredJsonCodec) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
obj := into.(*unstructured.Unstructured)
|
||||
err := obj.UnmarshalJSON(data)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
gvk := obj.GroupVersionKind()
|
||||
return obj, &gvk, nil
|
||||
}
|
||||
|
||||
func (c unstructuredJsonCodec) Encode(obj runtime.Object, w io.Writer) error {
|
||||
u := obj.(*unstructured.Unstructured)
|
||||
bs, err := u.MarshalJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write(bs)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package customresource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
)
|
||||
|
||||
// Registry is an interface for things that know how to store CustomResources.
|
||||
type Registry interface {
|
||||
ListCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*unstructured.UnstructuredList, error)
|
||||
WatchCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error)
|
||||
GetCustomResource(ctx genericapirequest.Context, customResourceID string, options *metav1.GetOptions) (*unstructured.Unstructured, error)
|
||||
CreateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc) (*unstructured.Unstructured, error)
|
||||
UpdateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (*unstructured.Unstructured, error)
|
||||
DeleteCustomResource(ctx genericapirequest.Context, customResourceID string) error
|
||||
}
|
||||
|
||||
// storage puts strong typing around storage calls
|
||||
type storage struct {
|
||||
rest.StandardStorage
|
||||
}
|
||||
|
||||
// NewRegistry returns a new Registry interface for the given Storage. Any mismatched
|
||||
// types will panic.
|
||||
func NewRegistry(s rest.StandardStorage) Registry {
|
||||
return &storage{s}
|
||||
}
|
||||
|
||||
func (s *storage) ListCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*unstructured.UnstructuredList, error) {
|
||||
if options != nil && options.FieldSelector != nil && !options.FieldSelector.Empty() {
|
||||
return nil, fmt.Errorf("field selector not supported yet")
|
||||
}
|
||||
obj, err := s.List(ctx, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*unstructured.UnstructuredList), err
|
||||
}
|
||||
|
||||
func (s *storage) WatchCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
|
||||
return s.Watch(ctx, options)
|
||||
}
|
||||
|
||||
func (s *storage) GetCustomResource(ctx genericapirequest.Context, customResourceID string, options *metav1.GetOptions) (*unstructured.Unstructured, error) {
|
||||
obj, err := s.Get(ctx, customResourceID, options)
|
||||
customResource, ok := obj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("custom resource must be of type Unstructured")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
apiVersion := customResource.GetAPIVersion()
|
||||
groupVersion := strings.Split(apiVersion, "/")
|
||||
group := groupVersion[0]
|
||||
return nil, errors.NewNotFound(schema.GroupResource{Group: group, Resource: "scale"}, customResourceID)
|
||||
}
|
||||
return customResource, nil
|
||||
}
|
||||
|
||||
func (s *storage) CreateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc) (*unstructured.Unstructured, error) {
|
||||
obj, err := s.Create(ctx, customResource, rest.ValidateAllObjectFunc, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*unstructured.Unstructured), nil
|
||||
}
|
||||
|
||||
func (s *storage) UpdateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (*unstructured.Unstructured, error) {
|
||||
obj, _, err := s.Update(ctx, customResource.GetName(), rest.DefaultUpdatedObjectInfo(customResource), createValidation, updateValidation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return obj.(*unstructured.Unstructured), nil
|
||||
}
|
||||
|
||||
func (s *storage) DeleteCustomResource(ctx genericapirequest.Context, customResourceID string) error {
|
||||
_, _, err := s.Delete(ctx, customResourceID, nil)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package customresource
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
)
|
||||
|
||||
type statusStrategy struct {
|
||||
customResourceStrategy
|
||||
}
|
||||
|
||||
func NewStatusStrategy(strategy customResourceStrategy) statusStrategy {
|
||||
return statusStrategy{strategy}
|
||||
}
|
||||
|
||||
func (a statusStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
|
||||
newCustomResourceObject := obj.(*unstructured.Unstructured)
|
||||
oldCustomResourceObject := old.(*unstructured.Unstructured)
|
||||
|
||||
newCustomResource := newCustomResourceObject.UnstructuredContent()
|
||||
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
|
||||
|
||||
// update is not allowed to set spec and metadata
|
||||
_, ok1 := newCustomResource["spec"]
|
||||
_, ok2 := oldCustomResource["spec"]
|
||||
switch {
|
||||
case ok2:
|
||||
newCustomResource["spec"] = oldCustomResource["spec"]
|
||||
case ok1:
|
||||
delete(newCustomResource, "spec")
|
||||
}
|
||||
|
||||
newCustomResourceObject.SetAnnotations(oldCustomResourceObject.GetAnnotations())
|
||||
newCustomResourceObject.SetFinalizers(oldCustomResourceObject.GetFinalizers())
|
||||
newCustomResourceObject.SetGeneration(oldCustomResourceObject.GetGeneration())
|
||||
newCustomResourceObject.SetLabels(oldCustomResourceObject.GetLabels())
|
||||
newCustomResourceObject.SetOwnerReferences(oldCustomResourceObject.GetOwnerReferences())
|
||||
newCustomResourceObject.SetSelfLink(oldCustomResourceObject.GetSelfLink())
|
||||
}
|
||||
|
||||
// ValidateUpdate is the default update validation for an end user updating status.
|
||||
func (a statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||
return a.customResourceStrategy.validator.ValidateStatusUpdate(ctx, obj, old, a.scale)
|
||||
}
|
|
@ -17,12 +17,10 @@ limitations under the License.
|
|||
package customresource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-openapi/validate"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
|
@ -31,63 +29,129 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/storage"
|
||||
apiserverstorage "k8s.io/apiserver/pkg/storage"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
)
|
||||
|
||||
type customResourceDefinitionStorageStrategy struct {
|
||||
// customResourceStrategy implements behavior for CustomResources.
|
||||
type customResourceStrategy struct {
|
||||
runtime.ObjectTyper
|
||||
names.NameGenerator
|
||||
|
||||
namespaceScoped bool
|
||||
validator customResourceValidator
|
||||
status *apiextensions.CustomResourceSubresourceStatus
|
||||
scale *apiextensions.CustomResourceSubresourceScale
|
||||
}
|
||||
|
||||
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, validator *validate.SchemaValidator) customResourceDefinitionStorageStrategy {
|
||||
return customResourceDefinitionStorageStrategy{
|
||||
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator *validate.SchemaValidator, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
|
||||
return customResourceStrategy{
|
||||
ObjectTyper: typer,
|
||||
NameGenerator: names.SimpleNameGenerator,
|
||||
namespaceScoped: namespaceScoped,
|
||||
status: status,
|
||||
scale: scale,
|
||||
validator: customResourceValidator{
|
||||
namespaceScoped: namespaceScoped,
|
||||
kind: kind,
|
||||
validator: validator,
|
||||
namespaceScoped: namespaceScoped,
|
||||
kind: kind,
|
||||
schemaValidator: schemaValidator,
|
||||
statusSchemaValidator: statusSchemaValidator,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a customResourceDefinitionStorageStrategy) NamespaceScoped() bool {
|
||||
func (a customResourceStrategy) NamespaceScoped() bool {
|
||||
return a.namespaceScoped
|
||||
}
|
||||
|
||||
func (customResourceDefinitionStorageStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
|
||||
// PrepareForCreate clears the status of a CustomResource before creation.
|
||||
func (a customResourceStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
|
||||
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && a.status != nil {
|
||||
customResourceObject := obj.(*unstructured.Unstructured)
|
||||
customResource := customResourceObject.UnstructuredContent()
|
||||
|
||||
// create cannot set status
|
||||
if _, ok := customResource["status"]; ok {
|
||||
delete(customResource, "status")
|
||||
}
|
||||
}
|
||||
|
||||
accessor, _ := meta.Accessor(obj)
|
||||
accessor.SetGeneration(1)
|
||||
}
|
||||
|
||||
func (customResourceDefinitionStorageStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
|
||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
||||
func (a customResourceStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) || a.status == nil {
|
||||
return
|
||||
}
|
||||
|
||||
newCustomResourceObject := obj.(*unstructured.Unstructured)
|
||||
oldCustomResourceObject := old.(*unstructured.Unstructured)
|
||||
|
||||
newCustomResource := newCustomResourceObject.UnstructuredContent()
|
||||
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
|
||||
|
||||
// update is not allowed to set status
|
||||
_, ok1 := newCustomResource["status"]
|
||||
_, ok2 := oldCustomResource["status"]
|
||||
switch {
|
||||
case ok2:
|
||||
newCustomResource["status"] = oldCustomResource["status"]
|
||||
case ok1:
|
||||
delete(newCustomResource, "status")
|
||||
}
|
||||
|
||||
// Any changes to the spec increment the generation number, any changes to the
|
||||
// status should reflect the generation number of the corresponding object. We push
|
||||
// the burden of managing the status onto the clients because we can't (in general)
|
||||
// know here what version of spec the writer of the status has seen. It may seem like
|
||||
// we can at first -- since obj contains spec -- but in the future we will probably make
|
||||
// status its own object, and even if we don't, writes may be the result of a
|
||||
// read-update-write loop, so the contents of spec may not actually be the spec that
|
||||
// the CustomResource has *seen*.
|
||||
newSpec, ok1 := newCustomResource["spec"]
|
||||
oldSpec, ok2 := oldCustomResource["spec"]
|
||||
|
||||
// spec is changed, created or deleted
|
||||
if (ok1 && ok2 && !apiequality.Semantic.DeepEqual(oldSpec, newSpec)) || (ok1 && !ok2) || (!ok1 && ok2) {
|
||||
oldAccessor, _ := meta.Accessor(oldCustomResourceObject)
|
||||
newAccessor, _ := meta.Accessor(newCustomResourceObject)
|
||||
newAccessor.SetGeneration(oldAccessor.GetGeneration() + 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (a customResourceDefinitionStorageStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
|
||||
return a.validator.Validate(ctx, obj)
|
||||
// Validate validates a new CustomResource.
|
||||
func (a customResourceStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
|
||||
return a.validator.Validate(ctx, obj, a.scale)
|
||||
}
|
||||
|
||||
func (customResourceDefinitionStorageStrategy) AllowCreateOnUpdate() bool {
|
||||
// Canonicalize normalizes the object after validation.
|
||||
func (customResourceStrategy) Canonicalize(obj runtime.Object) {
|
||||
}
|
||||
|
||||
// AllowCreateOnUpdate is false for CustomResources; this means a POST is
|
||||
// needed to create one.
|
||||
func (customResourceStrategy) AllowCreateOnUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (customResourceDefinitionStorageStrategy) AllowUnconditionalUpdate() bool {
|
||||
// AllowUnconditionalUpdate is the default update policy for CustomResource objects.
|
||||
func (customResourceStrategy) AllowUnconditionalUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (customResourceDefinitionStorageStrategy) Canonicalize(obj runtime.Object) {
|
||||
// ValidateUpdate is the default update validation for an end user updating status.
|
||||
func (a customResourceStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||
return a.validator.ValidateUpdate(ctx, obj, old, a.scale)
|
||||
}
|
||||
|
||||
func (a customResourceDefinitionStorageStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||
return a.validator.ValidateUpdate(ctx, obj, old)
|
||||
}
|
||||
|
||||
func (a customResourceDefinitionStorageStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
||||
// GetAttrs returns labels and fields of a given object for filtering purposes.
|
||||
func (a customResourceStrategy) GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, nil, false, err
|
||||
|
@ -108,80 +172,13 @@ func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields.
|
|||
}
|
||||
}
|
||||
|
||||
func (a customResourceDefinitionStorageStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) storage.SelectionPredicate {
|
||||
return storage.SelectionPredicate{
|
||||
// MatchCustomResourceDefinitionStorage is the filter used by the generic etcd backend to route
|
||||
// watch events from etcd to clients of the apiserver only interested in specific
|
||||
// labels/fields.
|
||||
func (a customResourceStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) apiserverstorage.SelectionPredicate {
|
||||
return apiserverstorage.SelectionPredicate{
|
||||
Label: label,
|
||||
Field: field,
|
||||
GetAttrs: a.GetAttrs,
|
||||
}
|
||||
}
|
||||
|
||||
type customResourceValidator struct {
|
||||
namespaceScoped bool
|
||||
kind schema.GroupVersionKind
|
||||
validator *validate.SchemaValidator
|
||||
}
|
||||
|
||||
func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||
}
|
||||
typeAccessor, err := meta.TypeAccessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
|
||||
}
|
||||
if typeAccessor.GetKind() != a.kind.Kind {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
|
||||
}
|
||||
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
|
||||
}
|
||||
|
||||
customResourceObject, ok := obj.(*unstructured.Unstructured)
|
||||
// this will never happen.
|
||||
if !ok {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
|
||||
}
|
||||
|
||||
customResource := customResourceObject.UnstructuredContent()
|
||||
if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
|
||||
}
|
||||
|
||||
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
||||
}
|
||||
|
||||
func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||
objAccessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||
}
|
||||
oldAccessor, err := meta.Accessor(old)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||
}
|
||||
typeAccessor, err := meta.TypeAccessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
|
||||
}
|
||||
if typeAccessor.GetKind() != a.kind.Kind {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
|
||||
}
|
||||
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
|
||||
}
|
||||
|
||||
customResourceObject, ok := obj.(*unstructured.Unstructured)
|
||||
// this will never happen.
|
||||
if !ok {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
|
||||
}
|
||||
|
||||
customResource := customResourceObject.UnstructuredContent()
|
||||
if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
|
||||
}
|
||||
|
||||
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package customresource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/validate"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation"
|
||||
)
|
||||
|
||||
type customResourceValidator struct {
|
||||
namespaceScoped bool
|
||||
kind schema.GroupVersionKind
|
||||
schemaValidator *validate.SchemaValidator
|
||||
statusSchemaValidator *validate.SchemaValidator
|
||||
}
|
||||
|
||||
func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||
}
|
||||
typeAccessor, err := meta.TypeAccessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
|
||||
}
|
||||
if typeAccessor.GetKind() != a.kind.Kind {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
|
||||
}
|
||||
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
|
||||
}
|
||||
|
||||
customResourceObject, ok := obj.(*unstructured.Unstructured)
|
||||
// this will never happen.
|
||||
if !ok {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
|
||||
}
|
||||
customResource := customResourceObject.UnstructuredContent()
|
||||
|
||||
if err = apiservervalidation.ValidateCustomResource(customResource, a.schemaValidator); err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
|
||||
}
|
||||
|
||||
if scale != nil {
|
||||
// validate specReplicas
|
||||
specReplicasPath := strings.TrimPrefix(scale.SpecReplicasPath, ".") // ignore leading period
|
||||
specReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(specReplicasPath, ".")...)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, err.Error())}
|
||||
}
|
||||
if specReplicas < 0 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, "should be a non-negative integer")}
|
||||
}
|
||||
if specReplicas > math.MaxInt32 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
|
||||
}
|
||||
|
||||
// validate statusReplicas
|
||||
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
|
||||
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
|
||||
}
|
||||
if statusReplicas < 0 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
|
||||
}
|
||||
if statusReplicas > math.MaxInt32 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
|
||||
}
|
||||
|
||||
// validate labelSelector
|
||||
if scale.LabelSelectorPath != nil {
|
||||
labelSelectorPath := strings.TrimPrefix(*scale.LabelSelectorPath, ".") // ignore leading period
|
||||
labelSelector, _, err := unstructured.NestedString(customResource, strings.Split(labelSelectorPath, ".")...)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
||||
}
|
||||
|
||||
func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
|
||||
objAccessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||
}
|
||||
oldAccessor, err := meta.Accessor(old)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||
}
|
||||
typeAccessor, err := meta.TypeAccessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
|
||||
}
|
||||
if typeAccessor.GetKind() != a.kind.Kind {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
|
||||
}
|
||||
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
|
||||
}
|
||||
|
||||
customResourceObject, ok := obj.(*unstructured.Unstructured)
|
||||
// this will never happen.
|
||||
if !ok {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
|
||||
}
|
||||
customResource := customResourceObject.UnstructuredContent()
|
||||
|
||||
if err = apiservervalidation.ValidateCustomResource(customResource, a.schemaValidator); err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
|
||||
}
|
||||
|
||||
if scale != nil {
|
||||
// validate specReplicas
|
||||
specReplicasPath := strings.TrimPrefix(scale.SpecReplicasPath, ".") // ignore leading period
|
||||
specReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(specReplicasPath, ".")...)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, err.Error())}
|
||||
}
|
||||
if specReplicas < 0 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, "should be a non-negative integer")}
|
||||
}
|
||||
if specReplicas > math.MaxInt32 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.SpecReplicasPath), specReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
|
||||
}
|
||||
|
||||
// validate statusReplicas
|
||||
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
|
||||
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
|
||||
}
|
||||
if statusReplicas < 0 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
|
||||
}
|
||||
if statusReplicas > math.MaxInt32 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
|
||||
}
|
||||
|
||||
// validate labelSelector
|
||||
if scale.LabelSelectorPath != nil {
|
||||
labelSelectorPath := strings.TrimPrefix(*scale.LabelSelectorPath, ".") // ignore leading period
|
||||
labelSelector, _, err := unstructured.NestedString(customResource, strings.Split(labelSelectorPath, ".")...)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
|
||||
}
|
||||
|
||||
func (a customResourceValidator) ValidateStatusUpdate(ctx genericapirequest.Context, obj, old runtime.Object, scale *apiextensions.CustomResourceSubresourceScale) field.ErrorList {
|
||||
objAccessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||
}
|
||||
oldAccessor, err := meta.Accessor(old)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
|
||||
}
|
||||
typeAccessor, err := meta.TypeAccessor(obj)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
|
||||
}
|
||||
if typeAccessor.GetKind() != a.kind.Kind {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
|
||||
}
|
||||
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
|
||||
}
|
||||
|
||||
customResourceObject, ok := obj.(*unstructured.Unstructured)
|
||||
// this will never happen.
|
||||
if !ok {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
|
||||
}
|
||||
customResource := customResourceObject.UnstructuredContent()
|
||||
|
||||
// validate only the status
|
||||
customResourceStatus := customResource["status"]
|
||||
if err = apiservervalidation.ValidateCustomResource(customResourceStatus, a.statusSchemaValidator); err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath("status"), customResourceStatus, err.Error())}
|
||||
}
|
||||
|
||||
if scale != nil {
|
||||
// validate statusReplicas
|
||||
statusReplicasPath := strings.TrimPrefix(scale.StatusReplicasPath, ".") // ignore leading period
|
||||
statusReplicas, _, err := unstructured.NestedInt64(customResource, strings.Split(statusReplicasPath, ".")...)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, err.Error())}
|
||||
}
|
||||
if statusReplicas < 0 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, "should be a non-negative integer")}
|
||||
}
|
||||
if statusReplicas > math.MaxInt32 {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(scale.StatusReplicasPath), statusReplicas, fmt.Sprintf("should be less than or equal to %v", math.MaxInt32))}
|
||||
}
|
||||
|
||||
// validate labelSelector
|
||||
if scale.LabelSelectorPath != nil {
|
||||
labelSelectorPath := strings.TrimPrefix(*scale.LabelSelectorPath, ".") // ignore leading period
|
||||
labelSelector, _, err := unstructured.NestedString(customResource, strings.Split(labelSelectorPath, ".")...)
|
||||
if err != nil {
|
||||
return field.ErrorList{field.Invalid(field.NewPath(*scale.LabelSelectorPath), labelSelector, err.Error())}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
|
||||
}
|
|
@ -35,6 +35,7 @@ import (
|
|||
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
|
||||
)
|
||||
|
||||
// strategy implements behavior for CustomResources.
|
||||
type strategy struct {
|
||||
runtime.ObjectTyper
|
||||
names.NameGenerator
|
||||
|
@ -48,6 +49,7 @@ func (strategy) NamespaceScoped() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// PrepareForCreate clears the status of a CustomResourceDefinition before creation.
|
||||
func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
|
||||
crd := obj.(*apiextensions.CustomResourceDefinition)
|
||||
crd.Status = apiextensions.CustomResourceDefinitionStatus{}
|
||||
|
@ -57,8 +59,12 @@ func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Obje
|
|||
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
|
||||
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) {
|
||||
newCRD := obj.(*apiextensions.CustomResourceDefinition)
|
||||
oldCRD := old.(*apiextensions.CustomResourceDefinition)
|
||||
|
@ -80,23 +86,33 @@ func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime
|
|||
newCRD.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 {
|
||||
return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition))
|
||||
}
|
||||
|
||||
// AllowCreateOnUpdate is false for CustomResourceDefinition; this means a POST is
|
||||
// needed to create one.
|
||||
func (strategy) AllowCreateOnUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// AllowUnconditionalUpdate is the default update policy for CustomResourceDefinition objects.
|
||||
func (strategy) AllowUnconditionalUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Canonicalize normalizes the object after validation.
|
||||
func (strategy) Canonicalize(obj runtime.Object) {
|
||||
}
|
||||
|
||||
// ValidateUpdate is the default update validation for an end user updating status.
|
||||
func (strategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
|
||||
return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
|
||||
}
|
||||
|
@ -143,10 +159,11 @@ func (statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old run
|
|||
return validation.ValidateUpdateCustomResourceDefinitionStatus(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
|
||||
}
|
||||
|
||||
// GetAttrs returns labels and fields of a given object for filtering purposes.
|
||||
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
|
||||
apiserver, ok := obj.(*apiextensions.CustomResourceDefinition)
|
||||
if !ok {
|
||||
return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition.")
|
||||
return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition")
|
||||
}
|
||||
return labels.Set(apiserver.ObjectMeta.Labels), CustomResourceDefinitionToSelectableFields(apiserver), apiserver.Initializers != nil, nil
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ go_test(
|
|||
"basic_test.go",
|
||||
"finalization_test.go",
|
||||
"registration_test.go",
|
||||
"subresources_test.go",
|
||||
"validation_test.go",
|
||||
"yaml_test.go",
|
||||
],
|
||||
|
@ -19,17 +20,23 @@ go_test(
|
|||
"//vendor/github.com/coreos/etcd/clientv3:go_default_library",
|
||||
"//vendor/github.com/ghodss/yaml: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/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/apimachinery/pkg/api/errors: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/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/types: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/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",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -35,7 +35,7 @@ import (
|
|||
)
|
||||
|
||||
func TestServerUp(t *testing.T) {
|
||||
stopCh, _, _, err := testserver.StartDefaultServer()
|
||||
stopCh, _, _, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func TestServerUp(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNamespaceScopedCRUD(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ func TestNamespaceScopedCRUD(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestClusterScopedCRUD(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -347,7 +347,7 @@ func TestDiscovery(t *testing.T) {
|
|||
group := "mygroup.example.com"
|
||||
version := "v1beta1"
|
||||
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -391,7 +391,7 @@ func TestDiscovery(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestNoNamespaceReject(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -430,7 +430,7 @@ func TestNoNamespaceReject(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSameNameDiffNamespace(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -450,7 +450,7 @@ func TestSameNameDiffNamespace(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSelfLink(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -503,7 +503,7 @@ func TestSelfLink(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPreserveInt(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -548,7 +548,7 @@ func TestPreserveInt(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPatch(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -622,7 +622,7 @@ func TestPatch(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCrossNamespaceListWatch(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -758,7 +758,7 @@ func checkNamespacesWatchHelper(t *testing.T, ns string, namespacedwatch watch.I
|
|||
}
|
||||
|
||||
func TestNameConflict(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import (
|
|||
)
|
||||
|
||||
func TestFinalization(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
require.NoError(t, err)
|
||||
defer close(stopCh)
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
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/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
|
@ -73,8 +74,22 @@ func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, defi
|
|||
}, 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) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -198,7 +213,7 @@ func TestMultipleResourceInstances(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestMultipleRegistration(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -254,7 +269,7 @@ func TestMultipleRegistration(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeRegistrationAndReRegistration(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -347,12 +362,18 @@ func TestEtcdStorage(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config)
|
||||
stopCh, clientConfig, err := testserver.StartServer(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
apiExtensionClient, err := apiextensionsclientset.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPool := dynamic.NewDynamicClientPool(clientConfig)
|
||||
|
||||
etcdPrefix := getPrefixFromConfig(t, config)
|
||||
|
||||
ns1 := "another-default-is-possible"
|
||||
|
|
|
@ -0,0 +1,787 @@
|
|||
/*
|
||||
Copyright 2018 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
autoscaling "k8s.io/api/autoscaling/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
"k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apiextensions-apiserver/test/integration/testserver"
|
||||
)
|
||||
|
||||
var labelSelectorPath = ".status.labelSelector"
|
||||
|
||||
func NewNoxuSubresourcesCRD(scope apiextensionsv1beta1.ResourceScope) *apiextensionsv1beta1.CustomResourceDefinition {
|
||||
return &apiextensionsv1beta1.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "noxus.mygroup.example.com"},
|
||||
Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
|
||||
Group: "mygroup.example.com",
|
||||
Version: "v1beta1",
|
||||
Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
|
||||
Plural: "noxus",
|
||||
Singular: "nonenglishnoxu",
|
||||
Kind: "WishIHadChosenNoxu",
|
||||
ShortNames: []string{"foo", "bar", "abc", "def"},
|
||||
ListKind: "NoxuItemList",
|
||||
},
|
||||
Scope: scope,
|
||||
Subresources: &apiextensionsv1beta1.CustomResourceSubresources{
|
||||
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
|
||||
Scale: &apiextensionsv1beta1.CustomResourceSubresourceScale{
|
||||
SpecReplicasPath: ".spec.replicas",
|
||||
StatusReplicasPath: ".status.replicas",
|
||||
LabelSelectorPath: &labelSelectorPath,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewNoxuSubresourceInstance(namespace, name string) *unstructured.Unstructured {
|
||||
return &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "mygroup.example.com/v1beta1",
|
||||
"kind": "WishIHadChosenNoxu",
|
||||
"metadata": map[string]interface{}{
|
||||
"namespace": namespace,
|
||||
"name": name,
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"num": int64(10),
|
||||
"replicas": int64(3),
|
||||
},
|
||||
"status": map[string]interface{}{
|
||||
"replicas": int64(7),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusSubresource(t *testing.T) {
|
||||
// enable alpha feature CustomResourceSubresources
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
|
||||
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ns := "not-the-default"
|
||||
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
|
||||
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create noxu instance: %v", err)
|
||||
}
|
||||
|
||||
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// status should not be set after creation
|
||||
if val, ok := gottenNoxuInstance.Object["status"]; ok {
|
||||
t.Fatalf("status should not be set after creation, got %v", val)
|
||||
}
|
||||
|
||||
// .status.num = 20
|
||||
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// .spec.num = 20
|
||||
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// UpdateStatus should not update spec.
|
||||
// Check that .spec.num = 10 and .status.num = 20
|
||||
updatedStatusInstance, err := noxuStatusResourceClient.Update(gottenNoxuInstance)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to update status: %v", err)
|
||||
}
|
||||
|
||||
specNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "spec", "num")
|
||||
if !found || err != nil {
|
||||
t.Fatalf("unable to get .spec.num")
|
||||
}
|
||||
if specNum != int64(10) {
|
||||
t.Fatalf(".spec.num: expected: %v, got: %v", int64(10), specNum)
|
||||
}
|
||||
|
||||
statusNum, found, err := unstructured.NestedInt64(updatedStatusInstance.Object, "status", "num")
|
||||
if !found || err != nil {
|
||||
t.Fatalf("unable to get .status.num")
|
||||
}
|
||||
if statusNum != int64(20) {
|
||||
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
|
||||
}
|
||||
|
||||
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// .status.num = 40
|
||||
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "status", "num")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// .spec.num = 40
|
||||
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(40), "spec", "num")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Update should not update status.
|
||||
// Check that .spec.num = 40 and .status.num = 20
|
||||
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to update instance: %v", err)
|
||||
}
|
||||
|
||||
specNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "spec", "num")
|
||||
if !found || err != nil {
|
||||
t.Fatalf("unable to get .spec.num")
|
||||
}
|
||||
if specNum != int64(40) {
|
||||
t.Fatalf(".spec.num: expected: %v, got: %v", int64(40), specNum)
|
||||
}
|
||||
|
||||
statusNum, found, err = unstructured.NestedInt64(updatedInstance.Object, "status", "num")
|
||||
if !found || err != nil {
|
||||
t.Fatalf("unable to get .status.num")
|
||||
}
|
||||
if statusNum != int64(20) {
|
||||
t.Fatalf(".status.num: expected: %v, got: %v", int64(20), statusNum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaleSubresource(t *testing.T) {
|
||||
// enable alpha feature CustomResourceSubresources
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
|
||||
|
||||
groupResource := schema.GroupResource{
|
||||
Group: "mygroup.example.com",
|
||||
Resource: "noxus",
|
||||
}
|
||||
|
||||
stopCh, config, err := testserver.StartDefaultServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
apiExtensionClient, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPool := dynamic.NewDynamicClientPool(config)
|
||||
|
||||
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||
|
||||
// set invalid json path for specReplicasPath
|
||||
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = "foo,bar"
|
||||
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected non-error: specReplicasPath should be a valid json path under .spec")
|
||||
}
|
||||
|
||||
noxuDefinition.Spec.Subresources.Scale.SpecReplicasPath = ".spec.replicas"
|
||||
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ns := "not-the-default"
|
||||
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
|
||||
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create noxu instance: %v", err)
|
||||
}
|
||||
|
||||
scaleClient, err := testserver.CreateNewScaleClient(noxuDefinition, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// set .status.labelSelector = bar
|
||||
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = unstructured.SetNestedField(gottenNoxuInstance.Object, "bar", "status", "labelSelector")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
_, err = noxuStatusResourceClient.Update(gottenNoxuInstance)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to update status: %v", err)
|
||||
}
|
||||
|
||||
// get the scale object
|
||||
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gottenScale.Spec.Replicas != 3 {
|
||||
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 3, gottenScale.Spec.Replicas)
|
||||
}
|
||||
if gottenScale.Status.Selector != "bar" {
|
||||
t.Fatalf("Scale.Status.Selector: expected: %v, got: %v", "bar", gottenScale.Status.Selector)
|
||||
}
|
||||
|
||||
// check self link
|
||||
expectedSelfLink := "/apis/mygroup.example.com/v1beta1/namespaces/not-the-default/noxus/foo/scale"
|
||||
if gottenScale.GetSelfLink() != expectedSelfLink {
|
||||
t.Fatalf("Scale.Metadata.SelfLink: expected: %v, got: %v", expectedSelfLink, gottenScale.GetSelfLink())
|
||||
}
|
||||
|
||||
// update the scale object
|
||||
// check that spec is updated, but status is not
|
||||
gottenScale.Spec.Replicas = 5
|
||||
gottenScale.Status.Selector = "baz"
|
||||
updatedScale, err := scaleClient.Scales("not-the-default").Update(groupResource, gottenScale)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if updatedScale.Spec.Replicas != 5 {
|
||||
t.Fatalf("replicas: expected: %v, got: %v", 5, updatedScale.Spec.Replicas)
|
||||
}
|
||||
if updatedScale.Status.Selector != "bar" {
|
||||
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", updatedScale.Status.Selector)
|
||||
}
|
||||
|
||||
// check that .spec.replicas = 5, but status is not updated
|
||||
updatedNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
specReplicas, found, err := unstructured.NestedInt64(updatedNoxuInstance.Object, "spec", "replicas")
|
||||
if !found || err != nil {
|
||||
t.Fatalf("unable to get .spec.replicas")
|
||||
}
|
||||
if specReplicas != 5 {
|
||||
t.Fatalf("replicas: expected: %v, got: %v", 5, specReplicas)
|
||||
}
|
||||
statusLabelSelector, found, err := unstructured.NestedString(updatedNoxuInstance.Object, "status", "labelSelector")
|
||||
if !found || err != nil {
|
||||
t.Fatalf("unable to get .status.labelSelector")
|
||||
}
|
||||
if statusLabelSelector != "bar" {
|
||||
t.Fatalf("scale should not update status: expected %v, got: %v", "bar", statusLabelSelector)
|
||||
}
|
||||
|
||||
// validate maximum value
|
||||
// set .spec.replicas = math.MaxInt64
|
||||
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(math.MaxInt64), "spec", "replicas")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
_, err = noxuResourceClient.Update(gottenNoxuInstance)
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected non-error: .spec.replicas should be less than 2147483647")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationSchema(t *testing.T) {
|
||||
// enable alpha feature CustomResourceSubresources
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
|
||||
|
||||
stopCh, config, err := testserver.StartDefaultServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
apiExtensionClient, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPool := dynamic.NewDynamicClientPool(config)
|
||||
|
||||
// fields other than properties in root schema are not allowed
|
||||
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected non-error: if subresources for custom resources are enabled, only properties can be used at the root of the schema")
|
||||
}
|
||||
|
||||
// make sure we are not restricting fields to properties even in subschemas
|
||||
noxuDefinition.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{
|
||||
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||
"spec": {
|
||||
Description: "Validation for spec",
|
||||
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||
"replicas": {
|
||||
Type: "integer",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to created crd %v: %v", noxuDefinition.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOnlyStatus(t *testing.T) {
|
||||
// enable alpha feature CustomResourceSubresources
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
|
||||
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
// UpdateStatus should validate only status
|
||||
// 1. create a crd with max value of .spec.num = 10 and .status.num = 10
|
||||
// 2. create a cr with .spec.num = 10 and .status.num = 10 (valid)
|
||||
// 3. update the crd so that max value of .spec.num = 5 and .status.num = 10
|
||||
// 4. update the status of the cr with .status.num = 5 (spec is invalid)
|
||||
// validation passes becauses spec is not validated
|
||||
|
||||
// max value of spec.num = 10 and status.num = 10
|
||||
schema := &apiextensionsv1beta1.JSONSchemaProps{
|
||||
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||
"spec": {
|
||||
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||
"num": {
|
||||
Type: "integer",
|
||||
Maximum: float64Ptr(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": {
|
||||
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||
"num": {
|
||||
Type: "integer",
|
||||
Maximum: float64Ptr(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||
noxuDefinition.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
|
||||
OpenAPIV3Schema: schema,
|
||||
}
|
||||
|
||||
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ns := "not-the-default"
|
||||
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
|
||||
|
||||
// set .spec.num = 10 and .status.num = 10
|
||||
noxuInstance := NewNoxuSubresourceInstance(ns, "foo")
|
||||
err = unstructured.SetNestedField(noxuInstance.Object, int64(10), "status", "num")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
createdNoxuInstance, err := instantiateCustomResource(t, noxuInstance, noxuResourceClient, noxuDefinition)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create noxu instance: %v", err)
|
||||
}
|
||||
|
||||
gottenCRD, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get("noxus.mygroup.example.com", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// update the crd so that max value of spec.num = 5 and status.num = 10
|
||||
gottenCRD.Spec.Validation.OpenAPIV3Schema = &apiextensionsv1beta1.JSONSchemaProps{
|
||||
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||
"spec": {
|
||||
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||
"num": {
|
||||
Type: "integer",
|
||||
Maximum: float64Ptr(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
"status": {
|
||||
Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
|
||||
"num": {
|
||||
Type: "integer",
|
||||
Maximum: float64Ptr(10),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(gottenCRD); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// update the status with .status.num = 5
|
||||
err = unstructured.SetNestedField(createdNoxuInstance.Object, int64(5), "status", "num")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// cr is updated even though spec is invalid
|
||||
err = wait.Poll(500*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
_, err := noxuStatusResourceClient.Update(createdNoxuInstance)
|
||||
if statusError, isStatus := err.(*apierrors.StatusError); isStatus {
|
||||
if strings.Contains(statusError.Error(), "is invalid") {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubresourcesDiscovery(t *testing.T) {
|
||||
// enable alpha feature CustomResourceSubresources
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
|
||||
|
||||
stopCh, config, err := testserver.StartDefaultServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
apiExtensionClient, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPool := dynamic.NewDynamicClientPool(config)
|
||||
|
||||
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
group := "mygroup.example.com"
|
||||
version := "v1beta1"
|
||||
|
||||
resources, err := apiExtensionClient.Discovery().ServerResourcesForGroupVersion(group + "/" + version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(resources.APIResources) != 3 {
|
||||
t.Fatalf("Expected exactly the resources \"noxus\", \"noxus/status\" and \"noxus/scale\" in group version %v/%v via discovery, got: %v", group, version, resources.APIResources)
|
||||
}
|
||||
|
||||
// check discovery info for status
|
||||
status := resources.APIResources[1]
|
||||
|
||||
if status.Name != "noxus/status" {
|
||||
t.Fatalf("incorrect status via discovery: expected name: %v, got: %v", "noxus/status", status.Name)
|
||||
}
|
||||
|
||||
if status.Namespaced != true {
|
||||
t.Fatalf("incorrect status via discovery: expected namespace: %v, got: %v", true, status.Namespaced)
|
||||
}
|
||||
|
||||
if status.Kind != "WishIHadChosenNoxu" {
|
||||
t.Fatalf("incorrect status via discovery: expected kind: %v, got: %v", "WishIHadChosenNoxu", status.Kind)
|
||||
}
|
||||
|
||||
expectedVerbs := []string{"get", "patch", "update"}
|
||||
sort.Strings(status.Verbs)
|
||||
if !reflect.DeepEqual([]string(status.Verbs), expectedVerbs) {
|
||||
t.Fatalf("incorrect status via discovery: expected: %v, got: %v", expectedVerbs, status.Verbs)
|
||||
}
|
||||
|
||||
// check discovery info for scale
|
||||
scale := resources.APIResources[2]
|
||||
|
||||
if scale.Group != autoscaling.GroupName {
|
||||
t.Fatalf("incorrect scale via discovery: expected group: %v, got: %v", autoscaling.GroupName, scale.Group)
|
||||
}
|
||||
|
||||
if scale.Version != "v1" {
|
||||
t.Fatalf("incorrect scale via discovery: expected version: %v, got %v", "v1", scale.Version)
|
||||
}
|
||||
|
||||
if scale.Name != "noxus/scale" {
|
||||
t.Fatalf("incorrect scale via discovery: expected name: %v, got: %v", "noxus/scale", scale.Name)
|
||||
}
|
||||
|
||||
if scale.Namespaced != true {
|
||||
t.Fatalf("incorrect scale via discovery: expected namespace: %v, got: %v", true, scale.Namespaced)
|
||||
}
|
||||
|
||||
if scale.Kind != "Scale" {
|
||||
t.Fatalf("incorrect scale via discovery: expected kind: %v, got: %v", "Scale", scale.Kind)
|
||||
}
|
||||
|
||||
sort.Strings(scale.Verbs)
|
||||
if !reflect.DeepEqual([]string(scale.Verbs), expectedVerbs) {
|
||||
t.Fatalf("incorrect scale via discovery: expected: %v, got: %v", expectedVerbs, scale.Verbs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneration(t *testing.T) {
|
||||
// enable alpha feature CustomResourceSubresources
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
|
||||
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ns := "not-the-default"
|
||||
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
|
||||
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create noxu instance: %v", err)
|
||||
}
|
||||
|
||||
// .metadata.generation = 1
|
||||
gottenNoxuInstance, err := noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gottenNoxuInstance.GetGeneration() != 1 {
|
||||
t.Fatalf(".metadata.generation should be 1 after creation")
|
||||
}
|
||||
|
||||
// .status.num = 20
|
||||
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "status", "num")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// UpdateStatus does not increment generation
|
||||
updatedStatusInstance, err := noxuStatusResourceClient.Update(gottenNoxuInstance)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to update status: %v", err)
|
||||
}
|
||||
if updatedStatusInstance.GetGeneration() != 1 {
|
||||
t.Fatalf("updating status should not increment .metadata.generation: expected: %v, got: %v", 1, updatedStatusInstance.GetGeneration())
|
||||
}
|
||||
|
||||
gottenNoxuInstance, err = noxuResourceClient.Get("foo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// .spec.num = 20
|
||||
err = unstructured.SetNestedField(gottenNoxuInstance.Object, int64(20), "spec", "num")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Update increments generation
|
||||
updatedInstance, err := noxuResourceClient.Update(gottenNoxuInstance)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to update instance: %v", err)
|
||||
}
|
||||
if updatedInstance.GetGeneration() != 2 {
|
||||
t.Fatalf("updating spec should increment .metadata.generation: expected: %v, got: %v", 2, updatedStatusInstance.GetGeneration())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubresourcePatch(t *testing.T) {
|
||||
// enable alpha feature CustomResourceSubresources
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
|
||||
|
||||
groupResource := schema.GroupResource{
|
||||
Group: "mygroup.example.com",
|
||||
Resource: "noxus",
|
||||
}
|
||||
|
||||
stopCh, config, err := testserver.StartDefaultServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
apiExtensionClient, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPool := dynamic.NewDynamicClientPool(config)
|
||||
|
||||
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.NamespaceScoped)
|
||||
noxuVersionClient, err := testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ns := "not-the-default"
|
||||
noxuResourceClient := NewNamespacedCustomResourceClient(ns, noxuVersionClient, noxuDefinition)
|
||||
noxuStatusResourceClient := NewNamespacedCustomResourceStatusClient(ns, noxuVersionClient, noxuDefinition)
|
||||
noxuScaleResourceClient := NewNamespacedCustomResourceScaleClient(ns, noxuVersionClient, noxuDefinition)
|
||||
_, err = instantiateCustomResource(t, NewNoxuSubresourceInstance(ns, "foo"), noxuResourceClient, noxuDefinition)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create noxu instance: %v", err)
|
||||
}
|
||||
|
||||
scaleClient, err := testserver.CreateNewScaleClient(noxuDefinition, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
patch := []byte(`{"spec": {"num":999}, "status": {"num":999}}`)
|
||||
patchedNoxuInstance, err := noxuStatusResourceClient.Patch("foo", types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// .spec.num should remain 10
|
||||
specNum, found, err := unstructured.NestedInt64(patchedNoxuInstance.Object, "spec", "num")
|
||||
if !found || err != nil {
|
||||
t.Fatalf("unable to get .spec.num")
|
||||
}
|
||||
if specNum != 10 {
|
||||
t.Fatalf(".spec.num: expected: %v, got: %v", 10, specNum)
|
||||
}
|
||||
|
||||
// .status.num should be 999
|
||||
statusNum, found, err := unstructured.NestedInt64(patchedNoxuInstance.Object, "status", "num")
|
||||
if !found || err != nil {
|
||||
t.Fatalf("unable to get .status.num")
|
||||
}
|
||||
if statusNum != 999 {
|
||||
t.Fatalf(".status.num: expected: %v, got: %v", 999, statusNum)
|
||||
}
|
||||
|
||||
// this call waits for the resourceVersion to be reached in the cache before returning.
|
||||
// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
|
||||
// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
|
||||
// and then the updated object shows a conflicting diff, which permanently fails the patch.
|
||||
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
|
||||
// See https://issue.k8s.io/42644
|
||||
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// no-op patch
|
||||
_, err = noxuStatusResourceClient.Patch("foo", types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// empty patch
|
||||
_, err = noxuStatusResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
patch = []byte(`{"spec": {"replicas":7}, "status": {"replicas":7}}`)
|
||||
patchedNoxuInstance, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// this call waits for the resourceVersion to be reached in the cache before returning.
|
||||
// We need to do this because the patch gets its initial object from the storage, and the cache serves that.
|
||||
// If it is out of date, then our initial patch is applied to an old resource version, which conflicts
|
||||
// and then the updated object shows a conflicting diff, which permanently fails the patch.
|
||||
// This gives expected stability in the patch without retrying on an known number of conflicts below in the test.
|
||||
// See https://issue.k8s.io/42644
|
||||
_, err = noxuResourceClient.Get("foo", metav1.GetOptions{ResourceVersion: patchedNoxuInstance.GetResourceVersion()})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Scale.Spec.Replicas = 7 but Scale.Status.Replicas should remain 7
|
||||
gottenScale, err := scaleClient.Scales("not-the-default").Get(groupResource, "foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gottenScale.Spec.Replicas != 7 {
|
||||
t.Fatalf("Scale.Spec.Replicas: expected: %v, got: %v", 7, gottenScale.Spec.Replicas)
|
||||
}
|
||||
if gottenScale.Status.Replicas != 0 {
|
||||
t.Fatalf("Scale.Status.Replicas: expected: %v, got: %v", 0, gottenScale.Spec.Replicas)
|
||||
}
|
||||
|
||||
// no-op patch
|
||||
_, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// empty patch
|
||||
_, err = noxuScaleResourceClient.Patch("foo", types.MergePatchType, []byte(`{}`))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// make sure strategic merge patch is not supported for both status and scale
|
||||
_, err = noxuStatusResourceClient.Patch("foo", types.StrategicMergePatchType, patch)
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
|
||||
}
|
||||
|
||||
_, err = noxuScaleResourceClient.Patch("foo", types.StrategicMergePatchType, patch)
|
||||
if err == nil {
|
||||
t.Fatalf("unexpected non-error: strategic merge patch is not supported for custom resources")
|
||||
}
|
||||
}
|
|
@ -28,7 +28,10 @@ go_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/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/rest:go_default_library",
|
||||
"//vendor/k8s.io/client-go/scale:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -30,7 +30,10 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/scale"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -293,3 +296,34 @@ func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefi
|
|||
func GetCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1beta1.CustomResourceDefinition, error) {
|
||||
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 (
|
|||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
func DefaultServerConfig() (*extensionsapiserver.Config, error) {
|
||||
|
@ -87,11 +88,11 @@ func DefaultServerConfig() (*extensionsapiserver.Config, error) {
|
|||
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{})
|
||||
server, err := config.Complete().New(genericapiserver.EmptyDelegate)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
go func() {
|
||||
err := server.GenericAPIServer.PrepareRun().Run(stopCh)
|
||||
|
@ -123,26 +124,32 @@ func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.I
|
|||
})
|
||||
if err != nil {
|
||||
close(stopCh)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return stopCh, config.GenericConfig.LoopbackClientConfig, nil
|
||||
}
|
||||
|
||||
func StartDefaultServer() (chan struct{}, *rest.Config, error) {
|
||||
config, err := DefaultServerConfig()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return StartServer(config)
|
||||
}
|
||||
|
||||
func StartDefaultServerWithClients() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
|
||||
stopCh, config, err := StartDefaultServer()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
apiExtensionsClient, err := clientset.NewForConfig(server.GenericAPIServer.LoopbackClientConfig)
|
||||
apiExtensionsClient, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
close(stopCh)
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
bytes, _ := apiExtensionsClient.Discovery().RESTClient().Get().AbsPath("/apis/apiextensions.k8s.io/v1beta1").DoRaw()
|
||||
fmt.Print(string(bytes))
|
||||
|
||||
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(server.GenericAPIServer.LoopbackClientConfig), nil
|
||||
}
|
||||
|
||||
func StartDefaultServer() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
|
||||
config, err := DefaultServerConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return StartServer(config)
|
||||
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(config), nil
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import (
|
|||
)
|
||||
|
||||
func TestForProperValidationErrors(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure
|
|||
}
|
||||
|
||||
func TestCustomResourceValidation(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -190,7 +190,7 @@ func TestCustomResourceValidation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCustomResourceUpdateValidation(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ func TestCustomResourceUpdateValidation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCustomResourceValidationErrors(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -324,7 +324,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCRValidationOnCRDUpdate(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -378,7 +378,7 @@ func TestCRValidationOnCRDUpdate(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestForbiddenFieldsInSchema(t *testing.T) {
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer()
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -24,25 +24,32 @@ import (
|
|||
|
||||
"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/apis/meta/v1/unstructured"
|
||||
"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) {
|
||||
config, err := testserver.DefaultServerConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config)
|
||||
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 := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
|
||||
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
|
@ -232,7 +239,7 @@ values:
|
|||
Param("watch", "true").
|
||||
DoRaw()
|
||||
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)
|
||||
if err != nil {
|
||||
|
@ -294,7 +301,7 @@ values:
|
|||
t.Fatal(v, ok, err, string(result))
|
||||
}
|
||||
if obj.GetUID() != uid {
|
||||
t.Fatal("uid changed: %v vs %v", uid, obj.GetUID())
|
||||
t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -346,6 +353,179 @@ values:
|
|||
}
|
||||
}
|
||||
|
||||
func TestYAMLSubresource(t *testing.T) {
|
||||
// enable alpha feature CustomResourceSubresources
|
||||
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
|
||||
|
||||
stopCh, config, err := testserver.StartDefaultServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer close(stopCh)
|
||||
|
||||
apiExtensionClient, err := clientset.NewForConfig(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPool := dynamic.NewDynamicClientPool(config)
|
||||
|
||||
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.ClusterScoped)
|
||||
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
kind := noxuDefinition.Spec.Names.Kind
|
||||
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
|
||||
|
||||
rest := apiExtensionClient.Discovery().RESTClient()
|
||||
|
||||
uid := types.UID("")
|
||||
resourceVersion := ""
|
||||
|
||||
// Create
|
||||
{
|
||||
yamlBody := []byte(fmt.Sprintf(`
|
||||
apiVersion: %s
|
||||
kind: %s
|
||||
metadata:
|
||||
name: mytest
|
||||
spec:
|
||||
replicas: 3`, apiVersion, kind))
|
||||
|
||||
result, err := rest.Post().
|
||||
SetHeader("Accept", "application/yaml").
|
||||
SetHeader("Content-Type", "application/yaml").
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
|
||||
Body(yamlBody).
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
obj, err := decodeYAML(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if obj.GetName() != "mytest" {
|
||||
t.Fatalf("expected mytest, got %s", obj.GetName())
|
||||
}
|
||||
if obj.GetAPIVersion() != apiVersion {
|
||||
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
|
||||
}
|
||||
if obj.GetKind() != kind {
|
||||
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
|
||||
}
|
||||
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
|
||||
t.Fatal(v, ok, err, string(result))
|
||||
}
|
||||
uid = obj.GetUID()
|
||||
resourceVersion = obj.GetResourceVersion()
|
||||
}
|
||||
|
||||
// Get at /status
|
||||
{
|
||||
result, err := rest.Get().
|
||||
SetHeader("Accept", "application/yaml").
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
obj, err := decodeYAML(result)
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
if obj.GetName() != "mytest" {
|
||||
t.Fatalf("expected mytest, got %s", obj.GetName())
|
||||
}
|
||||
if obj.GetAPIVersion() != apiVersion {
|
||||
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
|
||||
}
|
||||
if obj.GetKind() != kind {
|
||||
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
|
||||
}
|
||||
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
|
||||
t.Fatal(v, ok, err, string(result))
|
||||
}
|
||||
}
|
||||
|
||||
// Update at /status
|
||||
{
|
||||
yamlBody := []byte(fmt.Sprintf(`
|
||||
apiVersion: %s
|
||||
kind: %s
|
||||
metadata:
|
||||
name: mytest
|
||||
uid: %s
|
||||
resourceVersion: "%s"
|
||||
spec:
|
||||
replicas: 5
|
||||
status:
|
||||
replicas: 3`, apiVersion, kind, uid, resourceVersion))
|
||||
result, err := rest.Put().
|
||||
SetHeader("Accept", "application/yaml").
|
||||
SetHeader("Content-Type", "application/yaml").
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
|
||||
Body(yamlBody).
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
obj, err := decodeYAML(result)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if obj.GetName() != "mytest" {
|
||||
t.Fatalf("expected mytest, got %s", obj.GetName())
|
||||
}
|
||||
if obj.GetAPIVersion() != apiVersion {
|
||||
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
|
||||
}
|
||||
if obj.GetKind() != kind {
|
||||
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
|
||||
}
|
||||
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
|
||||
t.Fatal(v, ok, err, string(result))
|
||||
}
|
||||
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
|
||||
t.Fatal(v, ok, err, string(result))
|
||||
}
|
||||
if obj.GetUID() != uid {
|
||||
t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
|
||||
}
|
||||
}
|
||||
|
||||
// Get at /scale
|
||||
{
|
||||
result, err := rest.Get().
|
||||
SetHeader("Accept", "application/yaml").
|
||||
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "scale").
|
||||
DoRaw()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
obj, err := decodeYAML(result)
|
||||
if err != nil {
|
||||
t.Fatal(err, string(result))
|
||||
}
|
||||
if obj.GetName() != "mytest" {
|
||||
t.Fatalf("expected mytest, got %s", obj.GetName())
|
||||
}
|
||||
if obj.GetAPIVersion() != "autoscaling/v1" {
|
||||
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
|
||||
}
|
||||
if obj.GetKind() != "Scale" {
|
||||
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
|
||||
}
|
||||
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
|
||||
t.Fatal(v, ok, err, string(result))
|
||||
}
|
||||
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
|
||||
t.Fatal(v, ok, err, string(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeYAML(data []byte) (*unstructured.Unstructured, error) {
|
||||
retval := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
||||
// ensure this isn't JSON
|
||||
|
|
|
@ -280,6 +280,7 @@ func (rc *ResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error
|
|||
Watch()
|
||||
}
|
||||
|
||||
// Patch applies the patch and returns the patched resource.
|
||||
func (rc *ResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) {
|
||||
result := new(unstructured.Unstructured)
|
||||
resourceName, subresourceName := rc.parseResourceSubresourceName()
|
||||
|
|
|
@ -99,7 +99,7 @@ func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) {
|
|||
|
||||
restMapperRes, err := discovery.GetAPIGroupResources(fakeDiscoveryClient)
|
||||
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)
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
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
|
|
@ -1,40 +0,0 @@
|
|||
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"
|
|
@ -1,13 +0,0 @@
|
|||
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
|
|
@ -0,0 +1,22 @@
|
|||
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=
|
|
@ -1,3 +1,3 @@
|
|||
# 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)
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package validate
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
|
@ -51,7 +52,9 @@ func (f *formatValidator) Applies(source interface{}, kind reflect.Kind) bool {
|
|||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
package validate
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/spec"
|
||||
|
@ -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
|
||||
// so that requires more testing
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
val := data.(map[string]interface{})
|
||||
numKeys := int64(len(val))
|
||||
|
@ -61,14 +98,8 @@ func (o *objectValidator) Validate(data interface{}) *Result {
|
|||
}
|
||||
|
||||
res := new(Result)
|
||||
if len(o.Required) > 0 {
|
||||
for _, k := range o.Required {
|
||||
if _, ok := val[k]; !ok {
|
||||
res.AddErrors(errors.Required(o.Path+"."+k, o.In))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
o.precheck(res, val)
|
||||
|
||||
if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows {
|
||||
for k := range val {
|
||||
|
@ -99,6 +130,8 @@ func (o *objectValidator) Validate(data interface{}) *Result {
|
|||
}
|
||||
}
|
||||
|
||||
createdFromDefaults := map[string]bool{}
|
||||
|
||||
for pName, pSchema := range o.Properties {
|
||||
rName := pName
|
||||
if o.Path != "" {
|
||||
|
@ -106,7 +139,24 @@ func (o *objectValidator) Validate(data interface{}) *Result {
|
|||
}
|
||||
|
||||
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{},
|
|||
|
||||
res := validator.Validate(value)
|
||||
result.Merge(res)
|
||||
if res.IsValid() {
|
||||
succeededOnce = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,12 +14,32 @@
|
|||
|
||||
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
|
||||
type Result struct {
|
||||
Errors []error
|
||||
MatchCount int
|
||||
Defaulters []Defaulter
|
||||
}
|
||||
|
||||
// Merge merges this result with the other one, preserving match counts etc
|
||||
|
@ -29,11 +49,13 @@ func (r *Result) Merge(other *Result) *Result {
|
|||
}
|
||||
r.AddErrors(other.Errors...)
|
||||
r.MatchCount += other.MatchCount
|
||||
r.Defaulters = append(r.Defaulters, other.Defaulters...)
|
||||
return r
|
||||
}
|
||||
|
||||
// AddErrors adds errors to this validation result
|
||||
func (r *Result) AddErrors(errors ...error) {
|
||||
// TODO: filter already existing errors
|
||||
r.Errors = append(r.Errors, errors...)
|
||||
}
|
||||
|
||||
|
@ -59,3 +81,9 @@ func (r *Result) AsError() error {
|
|||
}
|
||||
return errors.CompositeValidationError(r.Errors...)
|
||||
}
|
||||
|
||||
func (r *Result) ApplyDefaults() {
|
||||
for _, d := range r.Defaulters {
|
||||
d.Apply()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
package validate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
|
@ -80,12 +82,16 @@ func (s *SchemaValidator) Applies(source interface{}, kind reflect.Kind) bool {
|
|||
|
||||
// Validate validates the data against the schema
|
||||
func (s *SchemaValidator) Validate(data interface{}) *Result {
|
||||
result := new(Result)
|
||||
if s == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
v := s.validators[0].Validate(data)
|
||||
v.Merge(s.validators[6].Validate(data))
|
||||
return v
|
||||
}
|
||||
result := new(Result)
|
||||
|
||||
tpe := reflect.TypeOf(data)
|
||||
kind := tpe.Kind()
|
||||
|
@ -98,8 +104,35 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
|
|||
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 {
|
||||
if !v.Applies(s.Schema, kind) {
|
||||
if Debug {
|
||||
log.Printf("%T does not apply for %v", v, kind)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -117,10 +150,9 @@ func (s *SchemaValidator) typeValidator() valueValidator {
|
|||
|
||||
func (s *SchemaValidator) commonValidator() valueValidator {
|
||||
return &basicCommonValidator{
|
||||
Path: s.Path,
|
||||
In: s.in,
|
||||
Default: s.Schema.Default,
|
||||
Enum: s.Schema.Enum,
|
||||
Path: s.Path,
|
||||
In: s.in,
|
||||
Enum: s.Schema.Enum,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,7 +187,6 @@ func (s *SchemaValidator) stringValidator() valueValidator {
|
|||
return &stringValidator{
|
||||
Path: s.Path,
|
||||
In: s.in,
|
||||
Default: s.Schema.Default,
|
||||
MaxLength: s.Schema.MaxLength,
|
||||
MinLength: s.Schema.MinLength,
|
||||
Pattern: s.Schema.Pattern,
|
||||
|
@ -164,9 +195,8 @@ func (s *SchemaValidator) stringValidator() valueValidator {
|
|||
|
||||
func (s *SchemaValidator) formatValidator() valueValidator {
|
||||
return &formatValidator{
|
||||
Path: s.Path,
|
||||
In: s.in,
|
||||
//Default: s.Schema.Default,
|
||||
Path: s.Path,
|
||||
In: s.in,
|
||||
Format: s.Schema.Format,
|
||||
KnownFormats: s.KnownFormats,
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package validate
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
|
@ -80,12 +81,15 @@ func newSchemaPropsValidator(path string, in string, allOf, oneOf, anyOf []spec.
|
|||
|
||||
func (s *schemaPropsValidator) Applies(source interface{}, kind reflect.Kind) bool {
|
||||
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
|
||||
}
|
||||
|
||||
func (s *schemaPropsValidator) Validate(data interface{}) *Result {
|
||||
mainResult := new(Result)
|
||||
var firstSuccess *Result
|
||||
if len(s.anyOfValidators) > 0 {
|
||||
var bestFailures *Result
|
||||
succeededOnce := false
|
||||
|
@ -94,6 +98,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
|
|||
if result.IsValid() {
|
||||
bestFailures = nil
|
||||
succeededOnce = true
|
||||
if firstSuccess == nil {
|
||||
firstSuccess = result
|
||||
}
|
||||
break
|
||||
}
|
||||
if bestFailures == nil || result.MatchCount > bestFailures.MatchCount {
|
||||
|
@ -106,11 +113,14 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
|
|||
}
|
||||
if bestFailures != nil {
|
||||
mainResult.Merge(bestFailures)
|
||||
} else if firstSuccess != nil {
|
||||
mainResult.Merge(firstSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.oneOfValidators) > 0 {
|
||||
var bestFailures *Result
|
||||
var firstSuccess *Result
|
||||
validated := 0
|
||||
|
||||
for _, oneOfSchema := range s.oneOfValidators {
|
||||
|
@ -118,6 +128,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
|
|||
if result.IsValid() {
|
||||
validated++
|
||||
bestFailures = nil
|
||||
if firstSuccess == nil {
|
||||
firstSuccess = result
|
||||
}
|
||||
continue
|
||||
}
|
||||
if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) {
|
||||
|
@ -130,6 +143,8 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
|
|||
if bestFailures != nil {
|
||||
mainResult.Merge(bestFailures)
|
||||
}
|
||||
} else if firstSuccess != nil {
|
||||
mainResult.Merge(firstSuccess)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,6 +67,9 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
|
|||
itemsSize = int64(len(s.Items.Schemas))
|
||||
for i := int64(0); i < itemsSize; i++ {
|
||||
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()))
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/analysis"
|
||||
|
@ -193,6 +194,13 @@ func (s *SpecValidator) validateDuplicatePropertyNames() *Result {
|
|||
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 {
|
||||
var dups []dupProp
|
||||
|
||||
|
@ -200,7 +208,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema,
|
|||
schc := &sch
|
||||
for schc.Ref.String() != "" {
|
||||
// gather property names
|
||||
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref)
|
||||
reso, err := s.resolveRef(&schc.Ref)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -236,7 +244,7 @@ func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, kno
|
|||
schn := nm
|
||||
schc := &sch
|
||||
for schc.Ref.String() != "" {
|
||||
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref)
|
||||
reso, err := s.resolveRef(&schc.Ref)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
schemas := schema.Items.Schemas
|
||||
if schema.Items.Schema != nil {
|
||||
schemas = []spec.Schema{*schema.Items.Schema}
|
||||
}
|
||||
for _, sch := range schemas {
|
||||
if err := s.validateSchemaItems(sch, prefix, opID); err != nil {
|
||||
return err
|
||||
schema = *schema.Items.Schema
|
||||
if _, err := regexp.Compile(schema.Pattern); err != nil {
|
||||
return errors.New(422, "%s for %q has invalid items pattern: %q", prefix, opID, schema.Pattern)
|
||||
}
|
||||
|
||||
return s.validateSchemaItems(schema, prefix, opID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -524,8 +532,15 @@ func (s *SpecValidator) validateParameters() *Result {
|
|||
}
|
||||
var fromPath []string
|
||||
for _, i := range params {
|
||||
fromPath = append(fromPath, knowns[i])
|
||||
knowns[i] = "!"
|
||||
knownsi := 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, "/")
|
||||
if orig, ok := knownPaths[knownPath]; ok {
|
||||
|
@ -544,7 +559,9 @@ func (s *SpecValidator) validateParameters() *Result {
|
|||
for pr.Ref.String() != "" {
|
||||
obj, _, err := pr.Ref.GetPointer().Get(sw)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
if Debug {
|
||||
log.Println(err)
|
||||
}
|
||||
res.AddErrors(err)
|
||||
break PARAMETERS
|
||||
}
|
||||
|
@ -575,6 +592,10 @@ func (s *SpecValidator) validateParameters() *Result {
|
|||
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 firstBodyParam != "" {
|
||||
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 {
|
|||
func parsePath(path string) (segments []string, params []int) {
|
||||
for i, p := range strings.Split(path, "/") {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// each reference must point to a valid object
|
||||
res := new(Result)
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
@ -698,7 +736,9 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
|
|||
}
|
||||
// check simple paramters first
|
||||
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
|
||||
res.Merge(NewParamValidator(¶m, s.KnownFormats).Validate(param.Default))
|
||||
}
|
||||
|
@ -721,9 +761,15 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
|
|||
if h.Items != nil {
|
||||
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))
|
||||
}
|
||||
}
|
||||
if dr.Schema != nil {
|
||||
res.Merge(s.validateDefaultValueSchemaAgainstSchema("default", "response", dr.Schema))
|
||||
}
|
||||
}
|
||||
for _, r := range op.Responses.StatusCodeResponses {
|
||||
for code, r := range op.Responses.StatusCodeResponses {
|
||||
for nm, h := range r.Headers {
|
||||
if h.Default != nil {
|
||||
res.Merge(NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default))
|
||||
|
@ -731,6 +777,12 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
|
|||
if h.Items != nil {
|
||||
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,
|
|||
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 {
|
||||
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema))
|
||||
}
|
||||
|
@ -787,6 +842,9 @@ func (s *SpecValidator) validateDefaultValueItemsAgainstSchema(path, in string,
|
|||
if items.Items != nil {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
package validate
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
|
@ -32,16 +33,6 @@ type typeValidator struct {
|
|||
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) {
|
||||
switch data.(type) {
|
||||
case []byte:
|
||||
|
@ -121,7 +112,9 @@ func (t *typeValidator) SetPath(path string) {
|
|||
func (t *typeValidator) Applies(source interface{}, kind reflect.Kind) bool {
|
||||
stpe := reflect.TypeOf(source)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -140,7 +133,9 @@ func (t *typeValidator) Validate(data interface{}) *Result {
|
|||
kind := val.Kind()
|
||||
|
||||
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"
|
||||
isLowerFloat := t.Format == "float64" && format == "float32"
|
||||
isFloatInt := schType == "number" && swag.IsFloat64AJSONInteger(val.Float()) && t.Type.Contains("integer")
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
#!/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
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
|
@ -156,8 +157,15 @@ func (b *basicCommonValidator) Applies(source interface{}, kind reflect.Kind) bo
|
|||
func (b *basicCommonValidator) Validate(data interface{}) (res *Result) {
|
||||
if len(b.Enum) > 0 {
|
||||
for _, enumValue := range b.Enum {
|
||||
if data != nil && reflect.DeepEqual(enumValue, data) {
|
||||
return nil
|
||||
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 sErr(errors.EnumFail(b.Path, b.In, data, b.Enum))
|
||||
|
@ -474,10 +482,14 @@ func (n *numberValidator) Applies(source interface{}, kind reflect.Kind) bool {
|
|||
isInt := kind >= reflect.Int && kind <= reflect.Uint64
|
||||
isFloat := kind == reflect.Float32 || kind == reflect.Float64
|
||||
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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -536,15 +548,22 @@ func (s *stringValidator) Applies(source interface{}, kind reflect.Kind) bool {
|
|||
switch source.(type) {
|
||||
case *spec.Parameter, *spec.Schema, *spec.Items, *spec.Header:
|
||||
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
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
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 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
|
|||
|
||||
// MultipleOf validates if the provided number is a multiple of the factor
|
||||
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 nil
|
||||
|
|
Loading…
Reference in New Issue