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

Merge pull request #55168 from nikhita/customresources-subresources

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. apiextensions: add subresources for custom resources Fixes #38113 Fixes #58778 **Related**: - Proposal: https://github.com/kubernetes/community/pull/913 - For custom resources to work with `kubectl scale`: https://github.com/kubernetes/kubernetes/pull/58283 **Add types**: - Add `CustomResourceSubResources` type to CRD. - Fix proto generation for `CustomResourceSubResourceStatus`: https://github.com/kubernetes/kubernetes/pull/55970. - Add feature gate for `CustomResourceSubResources`. - Update CRD strategy: if feature gate is disabled, this feature is dropped (i.e. set to `nil`). - Add validation for `CustomResourceSubResources`: - `SpecReplicasPath` should not be empty and should be a valid json path under `.spec`. If there is no value under the given path in the CustomResource, the `/scale` subresource will return an error on GET. - `StatusReplicasPath` should not be empty and should be a valid json path under `.status`. If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource will default to 0. - If present, `LabelSelectorPath` should be a valid json path. If there is no value under `LabelSelectorPath` in the CustomResource, the status label selector value in the `/scale` subresource will default to the empty string. - `ScaleGroupVersion` should be `autoscaling/v1`. - If `CustomResourceSubResources` is enabled, only `properties` is allowed under the root schema for CRD validation. **Add status and scale subresources**: - Use helper functions from `apimachinery/pkg/apis/meta/v1/unstructured/helpers.go`. - Improve error handling: https://github.com/kubernetes/kubernetes/pull/56563, https://github.com/kubernetes/kubernetes/pull/58215. - Introduce Registry interface for storage. - Update storage: - Introduce `CustomResourceStorage` which acts as storage for the custom resource and its status and scale subresources. Note: storage for status and scale is only enabled when the feature gate is enabled _and_ the respective fields are enabled in the CRD. - Introduce `StatusREST` and its `New()`, `Get()` and `Update()` methods. - Introduce `ScaleREST` and its `New()`, `Get()` and `Update()` methods. - Get and Update use the json paths from the CRD and use it to return an `autoscaling/v1.Scale` object. - Update strategy: - In `PrepareForCreate`, - Clear `.status`. - Set `.metadata.generation` = 1 - In `PrepareForUpdate`, - Do not update `.status`. - If both the old and new objects have `.status` and it is changed, set it back to its old value. - If the old object has a `.status` but the new object doesn't, set it to the old value. - If old object did not have a `.status` but the new object does, delete it. - Increment generation if spec changes i.e. in the following cases: - If both the old and new objects had `.spec` and it changed. - If the old object did not have `.spec` but the new object does. - If the old object had a `.spec` but the new object doesn't. - In `Validate` and `ValidateUpdate`, - ensure that values at `specReplicasPath` and `statusReplicasPath` are >=0 and < maxInt32. - make sure there are no errors in getting the value at all the paths. - Introduce `statusStrategy` with its methods. - In `PrepareForUpdate`: - Do not update `.spec`. - If both the old and new objects have `.spec` and it is changed, set it back to its old value. - If the old object has a `.spec` but the new object doesn't, set it to the old value. - If old object did not have a `.spec` but the new object does, delete it. - Do not update `.metadata`. - In `ValidateStatusUpdate`: - For CRD validation, validate only under `.status`. - Validate value at `statusReplicasPath` as above. If `labelSelectorPath` is a path under `.status`, then validate it as well. - Plug into the custom resource handler: - Store all three storage - customResource, status and scale in `crdInfo`. - Use the storage as per the subresource in the request. - Use the validator as per the subresource (for status, only use the schema for `status`, if present). - Serve the endpoint as per the subresource - see `serveResource`, `serveStatus` and `serveScale`. - Update discovery by adding the `/status` and `/scale` resources, if enabled. **Add tests**: - Add unit tests in `etcd_test.go`. - Add integration tests. - In `subresources_test.go`, use the [polymporphic scale client](https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/client-go/scale) to get and update `Scale`. - Add a test to check everything works fine with yaml in `yaml_test.go`. **Release note**: ```release-note `/status` and `/scale` subresources are added for custom resources. ```
parents fd1527a9 55ce3ded
...@@ -1263,7 +1263,7 @@ ...@@ -1263,7 +1263,7 @@
}, },
{ {
"ImportPath": "github.com/go-openapi/validate", "ImportPath": "github.com/go-openapi/validate",
"Rev": "deaf2c9013bc1a7f4c774662259a506ba874d80f" "Rev": "d509235108fcf6ab4913d2dcb3a2260c0db2108e"
}, },
{ {
"ImportPath": "github.com/godbus/dbus", "ImportPath": "github.com/godbus/dbus",
......
...@@ -83905,6 +83905,10 @@ ...@@ -83905,6 +83905,10 @@
"description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced", "description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced",
"type": "string" "type": "string"
}, },
"subresources": {
"description": "Subresources describes the subresources for CustomResources This field is alpha-level and should only be sent to servers that enable subresources via the CustomResourceSubresources feature gate.",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources"
},
"validation": { "validation": {
"description": "Validation describes the validation methods for CustomResources", "description": "Validation describes the validation methods for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation" "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation"
...@@ -83935,6 +83939,43 @@ ...@@ -83935,6 +83939,43 @@
} }
} }
}, },
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceScale": {
"description": "CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.",
"required": [
"specReplicasPath",
"statusReplicasPath"
],
"properties": {
"labelSelectorPath": {
"description": "LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector. Only JSON paths without the array notation are allowed. Must be a JSON Path under .status. Must be set to work with HPA. If there is no value under the given path in the CustomResource, the status label selector value in the /scale subresource will default to the empty string.",
"type": "string"
},
"specReplicasPath": {
"description": "SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas. Only JSON paths without the array notation are allowed. Must be a JSON Path under .spec. If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.",
"type": "string"
},
"statusReplicasPath": {
"description": "StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas. Only JSON paths without the array notation are allowed. Must be a JSON Path under .status. If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource will default to 0.",
"type": "string"
}
}
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceStatus": {
"description": "CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources. Status is represented by the `.status` JSON path inside of a CustomResource. When set, * exposes a /status subresource for the custom resource * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza"
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources": {
"description": "CustomResourceSubresources defines the status and scale subresources for CustomResources.",
"properties": {
"scale": {
"description": "Scale denotes the scale subresource for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceScale"
},
"status": {
"description": "Status denotes the status subresource for CustomResources",
"$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceStatus"
}
}
},
"io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation": { "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation": {
"description": "CustomResourceValidation is a list of validation methods for CustomResources.", "description": "CustomResourceValidation is a list of validation methods for CustomResources.",
"properties": { "properties": {
...@@ -24,7 +24,7 @@ import ( ...@@ -24,7 +24,7 @@ import (
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
discocache "k8s.io/client-go/discovery/cached" // Saturday Night Fever discocache "k8s.io/client-go/discovery/cached"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/scale" "k8s.io/client-go/scale"
"k8s.io/kubernetes/pkg/controller/podautoscaler" "k8s.io/kubernetes/pkg/controller/podautoscaler"
......
...@@ -477,7 +477,6 @@ staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server ...@@ -477,7 +477,6 @@ staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server
staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer
staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status
staging/src/k8s.io/apiextensions-apiserver/pkg/features staging/src/k8s.io/apiextensions-apiserver/pkg/features
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource
staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition
staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver staging/src/k8s.io/apiextensions-apiserver/test/integration/testserver
staging/src/k8s.io/apimachinery/pkg/api/meta staging/src/k8s.io/apimachinery/pkg/api/meta
......
...@@ -304,7 +304,8 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS ...@@ -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 // inherited features from apiextensions-apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side: // unintentionally on either side:
apiextensionsfeatures.CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta}, apiextensionsfeatures.CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
apiextensionsfeatures.CustomResourceSubresources: {Default: false, PreRelease: utilfeature.Alpha},
// features that enable backwards compatibility but are scheduled to be removed // features that enable backwards compatibility but are scheduled to be removed
ServiceProxyAllowExternalIPs: {Default: false, PreRelease: utilfeature.Deprecated}, ServiceProxyAllowExternalIPs: {Default: false, PreRelease: utilfeature.Deprecated},
......
...@@ -31,6 +31,22 @@ ...@@ -31,6 +31,22 @@
"Rev": "3ac7bf7a47d159a033b107610db8a1b6575507a4" "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", "ImportPath": "github.com/coreos/etcd/auth/authpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
}, },
...@@ -43,26 +59,214 @@ ...@@ -43,26 +59,214 @@
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "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", "ImportPath": "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
}, },
{ {
"ImportPath": "github.com/coreos/etcd/etcdserver/auth",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/etcdserver/etcdserverpb", "ImportPath": "github.com/coreos/etcd/etcdserver/etcdserverpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "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", "ImportPath": "github.com/coreos/etcd/mvcc/mvccpb",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "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", "ImportPath": "github.com/coreos/etcd/pkg/pathutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "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", "ImportPath": "github.com/coreos/etcd/pkg/srv",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
}, },
{ {
"ImportPath": "github.com/coreos/etcd/pkg/testutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
},
{
"ImportPath": "github.com/coreos/etcd/pkg/tlsutil", "ImportPath": "github.com/coreos/etcd/pkg/tlsutil",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada"
}, },
...@@ -75,10 +279,58 @@ ...@@ -75,10 +279,58 @@
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "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", "ImportPath": "github.com/coreos/etcd/version",
"Rev": "95a726a27e09030f9ccbd9982a1508f5a6d25ada" "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", "ImportPath": "github.com/coreos/go-semver/semver",
"Rev": "568e959cd89871e61434c1143528d9162da89ef2" "Rev": "568e959cd89871e61434c1143528d9162da89ef2"
}, },
...@@ -87,10 +339,22 @@ ...@@ -87,10 +339,22 @@
"Rev": "48702e0da86bd25e76cfef347e2adeb434a0d0a6" "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", "ImportPath": "github.com/davecgh/go-spew/spew",
"Rev": "782f4967f2dc4564575ca782fe2d04090b5faca8" "Rev": "782f4967f2dc4564575ca782fe2d04090b5faca8"
}, },
{ {
"ImportPath": "github.com/dgrijalva/jwt-go",
"Rev": "01aeca54ebda6e0fbfafd0a524d234159c05ec20"
},
{
"ImportPath": "github.com/elazarl/go-bindata-assetfs", "ImportPath": "github.com/elazarl/go-bindata-assetfs",
"Rev": "3dcc96556217539f50599357fb481ac0dc7439b9" "Rev": "3dcc96556217539f50599357fb481ac0dc7439b9"
}, },
...@@ -152,7 +416,7 @@ ...@@ -152,7 +416,7 @@
}, },
{ {
"ImportPath": "github.com/go-openapi/validate", "ImportPath": "github.com/go-openapi/validate",
"Rev": "deaf2c9013bc1a7f4c774662259a506ba874d80f" "Rev": "d509235108fcf6ab4913d2dcb3a2260c0db2108e"
}, },
{ {
"ImportPath": "github.com/gogo/protobuf/proto", "ImportPath": "github.com/gogo/protobuf/proto",
...@@ -167,6 +431,14 @@ ...@@ -167,6 +431,14 @@
"Rev": "44145f04b68cf362d9c4df2182967c2275eaefed" "Rev": "44145f04b68cf362d9c4df2182967c2275eaefed"
}, },
{ {
"ImportPath": "github.com/golang/groupcache/lru",
"Rev": "02826c3e79038b59d737d3b1c0a1d937f71a4433"
},
{
"ImportPath": "github.com/golang/protobuf/jsonpb",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/proto", "ImportPath": "github.com/golang/protobuf/proto",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9" "Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
}, },
...@@ -187,10 +459,18 @@ ...@@ -187,10 +459,18 @@
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9" "Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
}, },
{ {
"ImportPath": "github.com/golang/protobuf/ptypes/struct",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
},
{
"ImportPath": "github.com/golang/protobuf/ptypes/timestamp", "ImportPath": "github.com/golang/protobuf/ptypes/timestamp",
"Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9" "Rev": "1643683e1b54a9e88ad26d98f81400c8c9d9f4f9"
}, },
{ {
"ImportPath": "github.com/google/btree",
"Rev": "7d79101e329e5a3adf994758c578dab82b90c017"
},
{
"ImportPath": "github.com/google/gofuzz", "ImportPath": "github.com/google/gofuzz",
"Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c" "Rev": "44d81051d367757e1c7c6a5a86423ece9afcf63c"
}, },
...@@ -207,6 +487,22 @@ ...@@ -207,6 +487,22 @@
"Rev": "0c5108395e2debce0d731cf0287ddf7242066aba" "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", "ImportPath": "github.com/hashicorp/golang-lru",
"Rev": "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4" "Rev": "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4"
}, },
...@@ -227,6 +523,10 @@ ...@@ -227,6 +523,10 @@
"Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" "Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
}, },
{ {
"ImportPath": "github.com/jonboulle/clockwork",
"Rev": "72f9bd7c4e0c2a40055ab3d0f09654f730cce982"
},
{
"ImportPath": "github.com/json-iterator/go", "ImportPath": "github.com/json-iterator/go",
"Rev": "13f86432b882000a51c6e610c620974462691a97" "Rev": "13f86432b882000a51c6e610c620974462691a97"
}, },
...@@ -303,6 +603,18 @@ ...@@ -303,6 +603,18 @@
"Rev": "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74" "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", "ImportPath": "golang.org/x/crypto/ssh/terminal",
"Rev": "81e90905daefcd6fd217b62423c0908922eadb30" "Rev": "81e90905daefcd6fd217b62423c0908922eadb30"
}, },
...@@ -1035,6 +1347,10 @@ ...@@ -1035,6 +1347,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apiserver/pkg/registry/rest/resttest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/server", "ImportPath": "k8s.io/apiserver/pkg/server",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1083,10 +1399,18 @@ ...@@ -1083,10 +1399,18 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/etcdtest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/metrics", "ImportPath": "k8s.io/apiserver/pkg/storage/etcd/metrics",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/testing/testingcert",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/util", "ImportPath": "k8s.io/apiserver/pkg/storage/etcd/util",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1111,6 +1435,10 @@ ...@@ -1111,6 +1435,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apiserver/pkg/storage/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/value", "ImportPath": "k8s.io/apiserver/pkg/storage/value",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1159,6 +1487,10 @@ ...@@ -1159,6 +1487,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/client-go/dynamic",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/client-go/informers", "ImportPath": "k8s.io/client-go/informers",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1547,6 +1879,34 @@ ...@@ -1547,6 +1879,34 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "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", "ImportPath": "k8s.io/client-go/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1647,6 +2007,10 @@ ...@@ -1647,6 +2007,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apimachinery/pkg/api/resource",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/api/testing/fuzzer", "ImportPath": "k8s.io/apimachinery/pkg/api/testing/fuzzer",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1667,6 +2031,10 @@ ...@@ -1667,6 +2031,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/internalversion",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1", "ImportPath": "k8s.io/apimachinery/pkg/apis/meta/v1",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1711,10 +2079,18 @@ ...@@ -1711,10 +2079,18 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apimachinery/pkg/util/diff",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/errors", "ImportPath": "k8s.io/apimachinery/pkg/util/errors",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apimachinery/pkg/util/intstr",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apimachinery/pkg/util/json", "ImportPath": "k8s.io/apimachinery/pkg/util/json",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1779,6 +2155,10 @@ ...@@ -1779,6 +2155,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apiserver/pkg/registry/generic/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/registry/rest", "ImportPath": "k8s.io/apiserver/pkg/registry/rest",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1803,6 +2183,10 @@ ...@@ -1803,6 +2183,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apiserver/pkg/storage/etcd/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/storage/names", "ImportPath": "k8s.io/apiserver/pkg/storage/names",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1815,6 +2199,10 @@ ...@@ -1815,6 +2199,10 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
{ {
"ImportPath": "k8s.io/apiserver/pkg/util/feature/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"ImportPath": "k8s.io/apiserver/pkg/util/logs", "ImportPath": "k8s.io/apiserver/pkg/util/logs",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
...@@ -1835,6 +2223,14 @@ ...@@ -1835,6 +2223,14 @@
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "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", "ImportPath": "k8s.io/client-go/testing",
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}, },
......
...@@ -16,7 +16,9 @@ limitations under the License. ...@@ -16,7 +16,9 @@ limitations under the License.
package apiextensions package apiextensions
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CustomResourceDefinitionSpec describes how a user wants their resource to appear // CustomResourceDefinitionSpec describes how a user wants their resource to appear
type CustomResourceDefinitionSpec struct { type CustomResourceDefinitionSpec struct {
...@@ -30,6 +32,8 @@ type CustomResourceDefinitionSpec struct { ...@@ -30,6 +32,8 @@ type CustomResourceDefinitionSpec struct {
Scope ResourceScope Scope ResourceScope
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
Validation *CustomResourceValidation Validation *CustomResourceValidation
// Subresources describes the subresources for CustomResources
Subresources *CustomResourceSubresources
} }
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition // CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
...@@ -146,3 +150,41 @@ type CustomResourceValidation struct { ...@@ -146,3 +150,41 @@ type CustomResourceValidation struct {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against. // OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
OpenAPIV3Schema *JSONSchemaProps OpenAPIV3Schema *JSONSchemaProps
} }
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
type CustomResourceSubresources struct {
// Status denotes the status subresource for CustomResources
Status *CustomResourceSubresourceStatus
// Scale denotes the scale subresource for CustomResources
Scale *CustomResourceSubresourceScale
}
// CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources.
// Status is represented by the `.status` JSON path inside of a CustomResource. When set,
// * exposes a /status subresource for the custom resource
// * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza
// * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza
type CustomResourceSubresourceStatus struct{}
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
type CustomResourceSubresourceScale struct {
// SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .spec.
// If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.
SpecReplicasPath string
// StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource
// will default to 0.
StatusReplicasPath string
// LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// Must be set to work with HPA.
// If there is no value under the given path in the CustomResource, the status label selector value in the /scale
// subresource will default to the empty string.
// +optional
LabelSelectorPath *string
}
...@@ -31,6 +31,9 @@ limitations under the License. ...@@ -31,6 +31,9 @@ limitations under the License.
CustomResourceDefinitionNames CustomResourceDefinitionNames
CustomResourceDefinitionSpec CustomResourceDefinitionSpec
CustomResourceDefinitionStatus CustomResourceDefinitionStatus
CustomResourceSubresourceScale
CustomResourceSubresourceStatus
CustomResourceSubresources
CustomResourceValidation CustomResourceValidation
ExternalDocumentation ExternalDocumentation
JSON JSON
...@@ -99,36 +102,54 @@ func (*CustomResourceDefinitionStatus) Descriptor() ([]byte, []int) { ...@@ -99,36 +102,54 @@ func (*CustomResourceDefinitionStatus) Descriptor() ([]byte, []int) {
return fileDescriptorGenerated, []int{5} return fileDescriptorGenerated, []int{5}
} }
func (m *CustomResourceSubresourceScale) Reset() { *m = CustomResourceSubresourceScale{} }
func (*CustomResourceSubresourceScale) ProtoMessage() {}
func (*CustomResourceSubresourceScale) Descriptor() ([]byte, []int) {
return fileDescriptorGenerated, []int{6}
}
func (m *CustomResourceSubresourceStatus) Reset() { *m = CustomResourceSubresourceStatus{} }
func (*CustomResourceSubresourceStatus) ProtoMessage() {}
func (*CustomResourceSubresourceStatus) Descriptor() ([]byte, []int) {
return fileDescriptorGenerated, []int{7}
}
func (m *CustomResourceSubresources) Reset() { *m = CustomResourceSubresources{} }
func (*CustomResourceSubresources) ProtoMessage() {}
func (*CustomResourceSubresources) Descriptor() ([]byte, []int) {
return fileDescriptorGenerated, []int{8}
}
func (m *CustomResourceValidation) Reset() { *m = CustomResourceValidation{} } func (m *CustomResourceValidation) Reset() { *m = CustomResourceValidation{} }
func (*CustomResourceValidation) ProtoMessage() {} func (*CustomResourceValidation) ProtoMessage() {}
func (*CustomResourceValidation) Descriptor() ([]byte, []int) { func (*CustomResourceValidation) Descriptor() ([]byte, []int) {
return fileDescriptorGenerated, []int{6} return fileDescriptorGenerated, []int{9}
} }
func (m *ExternalDocumentation) Reset() { *m = ExternalDocumentation{} } func (m *ExternalDocumentation) Reset() { *m = ExternalDocumentation{} }
func (*ExternalDocumentation) ProtoMessage() {} func (*ExternalDocumentation) ProtoMessage() {}
func (*ExternalDocumentation) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{7} } func (*ExternalDocumentation) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{10} }
func (m *JSON) Reset() { *m = JSON{} } func (m *JSON) Reset() { *m = JSON{} }
func (*JSON) ProtoMessage() {} func (*JSON) ProtoMessage() {}
func (*JSON) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{8} } func (*JSON) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{11} }
func (m *JSONSchemaProps) Reset() { *m = JSONSchemaProps{} } func (m *JSONSchemaProps) Reset() { *m = JSONSchemaProps{} }
func (*JSONSchemaProps) ProtoMessage() {} func (*JSONSchemaProps) ProtoMessage() {}
func (*JSONSchemaProps) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{9} } func (*JSONSchemaProps) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{12} }
func (m *JSONSchemaPropsOrArray) Reset() { *m = JSONSchemaPropsOrArray{} } func (m *JSONSchemaPropsOrArray) Reset() { *m = JSONSchemaPropsOrArray{} }
func (*JSONSchemaPropsOrArray) ProtoMessage() {} func (*JSONSchemaPropsOrArray) ProtoMessage() {}
func (*JSONSchemaPropsOrArray) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{10} } func (*JSONSchemaPropsOrArray) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{13} }
func (m *JSONSchemaPropsOrBool) Reset() { *m = JSONSchemaPropsOrBool{} } func (m *JSONSchemaPropsOrBool) Reset() { *m = JSONSchemaPropsOrBool{} }
func (*JSONSchemaPropsOrBool) ProtoMessage() {} func (*JSONSchemaPropsOrBool) ProtoMessage() {}
func (*JSONSchemaPropsOrBool) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{11} } func (*JSONSchemaPropsOrBool) Descriptor() ([]byte, []int) { return fileDescriptorGenerated, []int{14} }
func (m *JSONSchemaPropsOrStringArray) Reset() { *m = JSONSchemaPropsOrStringArray{} } func (m *JSONSchemaPropsOrStringArray) Reset() { *m = JSONSchemaPropsOrStringArray{} }
func (*JSONSchemaPropsOrStringArray) ProtoMessage() {} func (*JSONSchemaPropsOrStringArray) ProtoMessage() {}
func (*JSONSchemaPropsOrStringArray) Descriptor() ([]byte, []int) { func (*JSONSchemaPropsOrStringArray) Descriptor() ([]byte, []int) {
return fileDescriptorGenerated, []int{12} return fileDescriptorGenerated, []int{15}
} }
func init() { func init() {
...@@ -138,6 +159,9 @@ func init() { ...@@ -138,6 +159,9 @@ func init() {
proto.RegisterType((*CustomResourceDefinitionNames)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionNames") proto.RegisterType((*CustomResourceDefinitionNames)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionNames")
proto.RegisterType((*CustomResourceDefinitionSpec)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionSpec") proto.RegisterType((*CustomResourceDefinitionSpec)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionSpec")
proto.RegisterType((*CustomResourceDefinitionStatus)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionStatus") proto.RegisterType((*CustomResourceDefinitionStatus)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionStatus")
proto.RegisterType((*CustomResourceSubresourceScale)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceScale")
proto.RegisterType((*CustomResourceSubresourceStatus)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresourceStatus")
proto.RegisterType((*CustomResourceSubresources)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceSubresources")
proto.RegisterType((*CustomResourceValidation)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation") proto.RegisterType((*CustomResourceValidation)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation")
proto.RegisterType((*ExternalDocumentation)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.ExternalDocumentation") proto.RegisterType((*ExternalDocumentation)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.ExternalDocumentation")
proto.RegisterType((*JSON)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.JSON") proto.RegisterType((*JSON)(nil), "k8s.io.apiextensions_apiserver.pkg.apis.apiextensions.v1beta1.JSON")
...@@ -362,6 +386,16 @@ func (m *CustomResourceDefinitionSpec) MarshalTo(dAtA []byte) (int, error) { ...@@ -362,6 +386,16 @@ func (m *CustomResourceDefinitionSpec) MarshalTo(dAtA []byte) (int, error) {
} }
i += n7 i += n7
} }
if m.Subresources != nil {
dAtA[i] = 0x32
i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Subresources.Size()))
n8, err := m.Subresources.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n8
}
return i, nil return i, nil
} }
...@@ -395,11 +429,99 @@ func (m *CustomResourceDefinitionStatus) MarshalTo(dAtA []byte) (int, error) { ...@@ -395,11 +429,99 @@ func (m *CustomResourceDefinitionStatus) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.AcceptedNames.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.AcceptedNames.Size()))
n8, err := m.AcceptedNames.MarshalTo(dAtA[i:]) n9, err := m.AcceptedNames.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n8 i += n9
return i, nil
}
func (m *CustomResourceSubresourceScale) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *CustomResourceSubresourceScale) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
dAtA[i] = 0xa
i++
i = encodeVarintGenerated(dAtA, i, uint64(len(m.SpecReplicasPath)))
i += copy(dAtA[i:], m.SpecReplicasPath)
dAtA[i] = 0x12
i++
i = encodeVarintGenerated(dAtA, i, uint64(len(m.StatusReplicasPath)))
i += copy(dAtA[i:], m.StatusReplicasPath)
if m.LabelSelectorPath != nil {
dAtA[i] = 0x1a
i++
i = encodeVarintGenerated(dAtA, i, uint64(len(*m.LabelSelectorPath)))
i += copy(dAtA[i:], *m.LabelSelectorPath)
}
return i, nil
}
func (m *CustomResourceSubresourceStatus) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *CustomResourceSubresourceStatus) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
return i, nil
}
func (m *CustomResourceSubresources) Marshal() (dAtA []byte, err error) {
size := m.Size()
dAtA = make([]byte, size)
n, err := m.MarshalTo(dAtA)
if err != nil {
return nil, err
}
return dAtA[:n], nil
}
func (m *CustomResourceSubresources) MarshalTo(dAtA []byte) (int, error) {
var i int
_ = i
var l int
_ = l
if m.Status != nil {
dAtA[i] = 0xa
i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Status.Size()))
n10, err := m.Status.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n10
}
if m.Scale != nil {
dAtA[i] = 0x12
i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Scale.Size()))
n11, err := m.Scale.MarshalTo(dAtA[i:])
if err != nil {
return 0, err
}
i += n11
}
return i, nil return i, nil
} }
...@@ -422,11 +544,11 @@ func (m *CustomResourceValidation) MarshalTo(dAtA []byte) (int, error) { ...@@ -422,11 +544,11 @@ func (m *CustomResourceValidation) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0xa dAtA[i] = 0xa
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.OpenAPIV3Schema.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.OpenAPIV3Schema.Size()))
n9, err := m.OpenAPIV3Schema.MarshalTo(dAtA[i:]) n12, err := m.OpenAPIV3Schema.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n9 i += n12
} }
return i, nil return i, nil
} }
...@@ -530,11 +652,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -530,11 +652,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x42 dAtA[i] = 0x42
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Default.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Default.Size()))
n10, err := m.Default.MarshalTo(dAtA[i:]) n13, err := m.Default.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n10 i += n13
} }
if m.Maximum != nil { if m.Maximum != nil {
dAtA[i] = 0x49 dAtA[i] = 0x49
...@@ -658,11 +780,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -658,11 +780,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x1 dAtA[i] = 0x1
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Items.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Items.Size()))
n11, err := m.Items.MarshalTo(dAtA[i:]) n14, err := m.Items.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n11 i += n14
} }
if len(m.AllOf) > 0 { if len(m.AllOf) > 0 {
for _, msg := range m.AllOf { for _, msg := range m.AllOf {
...@@ -712,11 +834,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -712,11 +834,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x1 dAtA[i] = 0x1
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Not.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Not.Size()))
n12, err := m.Not.MarshalTo(dAtA[i:]) n15, err := m.Not.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n12 i += n15
} }
if len(m.Properties) > 0 { if len(m.Properties) > 0 {
keysForProperties := make([]string, 0, len(m.Properties)) keysForProperties := make([]string, 0, len(m.Properties))
...@@ -744,11 +866,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -744,11 +866,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64((&v).Size())) i = encodeVarintGenerated(dAtA, i, uint64((&v).Size()))
n13, err := (&v).MarshalTo(dAtA[i:]) n16, err := (&v).MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n13 i += n16
} }
} }
if m.AdditionalProperties != nil { if m.AdditionalProperties != nil {
...@@ -757,11 +879,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -757,11 +879,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x1 dAtA[i] = 0x1
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.AdditionalProperties.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.AdditionalProperties.Size()))
n14, err := m.AdditionalProperties.MarshalTo(dAtA[i:]) n17, err := m.AdditionalProperties.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n14 i += n17
} }
if len(m.PatternProperties) > 0 { if len(m.PatternProperties) > 0 {
keysForPatternProperties := make([]string, 0, len(m.PatternProperties)) keysForPatternProperties := make([]string, 0, len(m.PatternProperties))
...@@ -789,11 +911,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -789,11 +911,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64((&v).Size())) i = encodeVarintGenerated(dAtA, i, uint64((&v).Size()))
n15, err := (&v).MarshalTo(dAtA[i:]) n18, err := (&v).MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n15 i += n18
} }
} }
if len(m.Dependencies) > 0 { if len(m.Dependencies) > 0 {
...@@ -822,11 +944,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -822,11 +944,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64((&v).Size())) i = encodeVarintGenerated(dAtA, i, uint64((&v).Size()))
n16, err := (&v).MarshalTo(dAtA[i:]) n19, err := (&v).MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n16 i += n19
} }
} }
if m.AdditionalItems != nil { if m.AdditionalItems != nil {
...@@ -835,11 +957,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -835,11 +957,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x2 dAtA[i] = 0x2
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.AdditionalItems.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.AdditionalItems.Size()))
n17, err := m.AdditionalItems.MarshalTo(dAtA[i:]) n20, err := m.AdditionalItems.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n17 i += n20
} }
if len(m.Definitions) > 0 { if len(m.Definitions) > 0 {
keysForDefinitions := make([]string, 0, len(m.Definitions)) keysForDefinitions := make([]string, 0, len(m.Definitions))
...@@ -867,11 +989,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -867,11 +989,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64((&v).Size())) i = encodeVarintGenerated(dAtA, i, uint64((&v).Size()))
n18, err := (&v).MarshalTo(dAtA[i:]) n21, err := (&v).MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n18 i += n21
} }
} }
if m.ExternalDocs != nil { if m.ExternalDocs != nil {
...@@ -880,11 +1002,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -880,11 +1002,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x2 dAtA[i] = 0x2
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.ExternalDocs.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.ExternalDocs.Size()))
n19, err := m.ExternalDocs.MarshalTo(dAtA[i:]) n22, err := m.ExternalDocs.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n19 i += n22
} }
if m.Example != nil { if m.Example != nil {
dAtA[i] = 0xa2 dAtA[i] = 0xa2
...@@ -892,11 +1014,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { ...@@ -892,11 +1014,11 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x2 dAtA[i] = 0x2
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Example.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Example.Size()))
n20, err := m.Example.MarshalTo(dAtA[i:]) n23, err := m.Example.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n20 i += n23
} }
return i, nil return i, nil
} }
...@@ -920,11 +1042,11 @@ func (m *JSONSchemaPropsOrArray) MarshalTo(dAtA []byte) (int, error) { ...@@ -920,11 +1042,11 @@ func (m *JSONSchemaPropsOrArray) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0xa dAtA[i] = 0xa
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size()))
n21, err := m.Schema.MarshalTo(dAtA[i:]) n24, err := m.Schema.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n21 i += n24
} }
if len(m.JSONSchemas) > 0 { if len(m.JSONSchemas) > 0 {
for _, msg := range m.JSONSchemas { for _, msg := range m.JSONSchemas {
...@@ -968,11 +1090,11 @@ func (m *JSONSchemaPropsOrBool) MarshalTo(dAtA []byte) (int, error) { ...@@ -968,11 +1090,11 @@ func (m *JSONSchemaPropsOrBool) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0x12 dAtA[i] = 0x12
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size()))
n22, err := m.Schema.MarshalTo(dAtA[i:]) n25, err := m.Schema.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n22 i += n25
} }
return i, nil return i, nil
} }
...@@ -996,11 +1118,11 @@ func (m *JSONSchemaPropsOrStringArray) MarshalTo(dAtA []byte) (int, error) { ...@@ -996,11 +1118,11 @@ func (m *JSONSchemaPropsOrStringArray) MarshalTo(dAtA []byte) (int, error) {
dAtA[i] = 0xa dAtA[i] = 0xa
i++ i++
i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size())) i = encodeVarintGenerated(dAtA, i, uint64(m.Schema.Size()))
n23, err := m.Schema.MarshalTo(dAtA[i:]) n26, err := m.Schema.MarshalTo(dAtA[i:])
if err != nil { if err != nil {
return 0, err return 0, err
} }
i += n23 i += n26
} }
if len(m.Property) > 0 { if len(m.Property) > 0 {
for _, s := range m.Property { for _, s := range m.Property {
...@@ -1124,6 +1246,10 @@ func (m *CustomResourceDefinitionSpec) Size() (n int) { ...@@ -1124,6 +1246,10 @@ func (m *CustomResourceDefinitionSpec) Size() (n int) {
l = m.Validation.Size() l = m.Validation.Size()
n += 1 + l + sovGenerated(uint64(l)) n += 1 + l + sovGenerated(uint64(l))
} }
if m.Subresources != nil {
l = m.Subresources.Size()
n += 1 + l + sovGenerated(uint64(l))
}
return n return n
} }
...@@ -1141,6 +1267,40 @@ func (m *CustomResourceDefinitionStatus) Size() (n int) { ...@@ -1141,6 +1267,40 @@ func (m *CustomResourceDefinitionStatus) Size() (n int) {
return n return n
} }
func (m *CustomResourceSubresourceScale) Size() (n int) {
var l int
_ = l
l = len(m.SpecReplicasPath)
n += 1 + l + sovGenerated(uint64(l))
l = len(m.StatusReplicasPath)
n += 1 + l + sovGenerated(uint64(l))
if m.LabelSelectorPath != nil {
l = len(*m.LabelSelectorPath)
n += 1 + l + sovGenerated(uint64(l))
}
return n
}
func (m *CustomResourceSubresourceStatus) Size() (n int) {
var l int
_ = l
return n
}
func (m *CustomResourceSubresources) Size() (n int) {
var l int
_ = l
if m.Status != nil {
l = m.Status.Size()
n += 1 + l + sovGenerated(uint64(l))
}
if m.Scale != nil {
l = m.Scale.Size()
n += 1 + l + sovGenerated(uint64(l))
}
return n
}
func (m *CustomResourceValidation) Size() (n int) { func (m *CustomResourceValidation) Size() (n int) {
var l int var l int
_ = l _ = l
...@@ -1436,6 +1596,7 @@ func (this *CustomResourceDefinitionSpec) String() string { ...@@ -1436,6 +1596,7 @@ func (this *CustomResourceDefinitionSpec) String() string {
`Names:` + strings.Replace(strings.Replace(this.Names.String(), "CustomResourceDefinitionNames", "CustomResourceDefinitionNames", 1), `&`, ``, 1) + `,`, `Names:` + strings.Replace(strings.Replace(this.Names.String(), "CustomResourceDefinitionNames", "CustomResourceDefinitionNames", 1), `&`, ``, 1) + `,`,
`Scope:` + fmt.Sprintf("%v", this.Scope) + `,`, `Scope:` + fmt.Sprintf("%v", this.Scope) + `,`,
`Validation:` + strings.Replace(fmt.Sprintf("%v", this.Validation), "CustomResourceValidation", "CustomResourceValidation", 1) + `,`, `Validation:` + strings.Replace(fmt.Sprintf("%v", this.Validation), "CustomResourceValidation", "CustomResourceValidation", 1) + `,`,
`Subresources:` + strings.Replace(fmt.Sprintf("%v", this.Subresources), "CustomResourceSubresources", "CustomResourceSubresources", 1) + `,`,
`}`, `}`,
}, "") }, "")
return s return s
...@@ -1451,6 +1612,38 @@ func (this *CustomResourceDefinitionStatus) String() string { ...@@ -1451,6 +1612,38 @@ func (this *CustomResourceDefinitionStatus) String() string {
}, "") }, "")
return s return s
} }
func (this *CustomResourceSubresourceScale) String() string {
if this == nil {
return "nil"
}
s := strings.Join([]string{`&CustomResourceSubresourceScale{`,
`SpecReplicasPath:` + fmt.Sprintf("%v", this.SpecReplicasPath) + `,`,
`StatusReplicasPath:` + fmt.Sprintf("%v", this.StatusReplicasPath) + `,`,
`LabelSelectorPath:` + valueToStringGenerated(this.LabelSelectorPath) + `,`,
`}`,
}, "")
return s
}
func (this *CustomResourceSubresourceStatus) String() string {
if this == nil {
return "nil"
}
s := strings.Join([]string{`&CustomResourceSubresourceStatus{`,
`}`,
}, "")
return s
}
func (this *CustomResourceSubresources) String() string {
if this == nil {
return "nil"
}
s := strings.Join([]string{`&CustomResourceSubresources{`,
`Status:` + strings.Replace(fmt.Sprintf("%v", this.Status), "CustomResourceSubresourceStatus", "CustomResourceSubresourceStatus", 1) + `,`,
`Scale:` + strings.Replace(fmt.Sprintf("%v", this.Scale), "CustomResourceSubresourceScale", "CustomResourceSubresourceScale", 1) + `,`,
`}`,
}, "")
return s
}
func (this *CustomResourceValidation) String() string { func (this *CustomResourceValidation) String() string {
if this == nil { if this == nil {
return "nil" return "nil"
...@@ -2429,6 +2622,39 @@ func (m *CustomResourceDefinitionSpec) Unmarshal(dAtA []byte) error { ...@@ -2429,6 +2622,39 @@ func (m *CustomResourceDefinitionSpec) Unmarshal(dAtA []byte) error {
return err return err
} }
iNdEx = postIndex iNdEx = postIndex
case 6:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Subresources", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Subresources == nil {
m.Subresources = &CustomResourceSubresources{}
}
if err := m.Subresources.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default: default:
iNdEx = preIndex iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:]) skippy, err := skipGenerated(dAtA[iNdEx:])
...@@ -2561,6 +2787,310 @@ func (m *CustomResourceDefinitionStatus) Unmarshal(dAtA []byte) error { ...@@ -2561,6 +2787,310 @@ func (m *CustomResourceDefinitionStatus) Unmarshal(dAtA []byte) error {
} }
return nil return nil
} }
func (m *CustomResourceSubresourceScale) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: CustomResourceSubresourceScale: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: CustomResourceSubresourceScale: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SpecReplicasPath", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.SpecReplicasPath = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field StatusReplicasPath", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.StatusReplicasPath = string(dAtA[iNdEx:postIndex])
iNdEx = postIndex
case 3:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field LabelSelectorPath", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
s := string(dAtA[iNdEx:postIndex])
m.LabelSelectorPath = &s
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthGenerated
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *CustomResourceSubresourceStatus) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: CustomResourceSubresourceStatus: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: CustomResourceSubresourceStatus: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
default:
iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthGenerated
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *CustomResourceSubresources) Unmarshal(dAtA []byte) error {
l := len(dAtA)
iNdEx := 0
for iNdEx < l {
preIndex := iNdEx
var wire uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
wire |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
fieldNum := int32(wire >> 3)
wireType := int(wire & 0x7)
if wireType == 4 {
return fmt.Errorf("proto: CustomResourceSubresources: wiretype end group for non-group")
}
if fieldNum <= 0 {
return fmt.Errorf("proto: CustomResourceSubresources: illegal tag %d (wire type %d)", fieldNum, wire)
}
switch fieldNum {
case 1:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Status", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Status == nil {
m.Status = &CustomResourceSubresourceStatus{}
}
if err := m.Status.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
case 2:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field Scale", wireType)
}
var msglen int
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowGenerated
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
msglen |= (int(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
if msglen < 0 {
return ErrInvalidLengthGenerated
}
postIndex := iNdEx + msglen
if postIndex > l {
return io.ErrUnexpectedEOF
}
if m.Scale == nil {
m.Scale = &CustomResourceSubresourceScale{}
}
if err := m.Scale.Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
return err
}
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipGenerated(dAtA[iNdEx:])
if err != nil {
return err
}
if skippy < 0 {
return ErrInvalidLengthGenerated
}
if (iNdEx + skippy) > l {
return io.ErrUnexpectedEOF
}
iNdEx += skippy
}
}
if iNdEx > l {
return io.ErrUnexpectedEOF
}
return nil
}
func (m *CustomResourceValidation) Unmarshal(dAtA []byte) error { func (m *CustomResourceValidation) Unmarshal(dAtA []byte) error {
l := len(dAtA) l := len(dAtA)
iNdEx := 0 iNdEx := 0
...@@ -4660,125 +5190,136 @@ func init() { ...@@ -4660,125 +5190,136 @@ func init() {
} }
var fileDescriptorGenerated = []byte{ var fileDescriptorGenerated = []byte{
// 1917 bytes of a gzipped FileDescriptorProto // 2081 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x58, 0xdb, 0x6f, 0x5b, 0x49, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x58, 0xdb, 0x6f, 0x5b, 0x49,
0x19, 0xcf, 0xd8, 0xb9, 0x4e, 0x92, 0x4d, 0x32, 0x6d, 0xca, 0x69, 0x68, 0xed, 0xd4, 0xcb, 0xae, 0x19, 0xcf, 0xd8, 0xb9, 0x4e, 0x92, 0x4d, 0x32, 0x6d, 0xca, 0x69, 0x68, 0xed, 0xc4, 0xcb, 0xae,
0x02, 0x6c, 0x6d, 0xba, 0x17, 0x76, 0x41, 0xe2, 0x21, 0x6e, 0x02, 0x2a, 0x34, 0x4d, 0x35, 0x69, 0x02, 0x6c, 0x6d, 0xba, 0x17, 0x76, 0x41, 0xe2, 0x21, 0x6e, 0x02, 0xea, 0x92, 0x34, 0xd1, 0xb8,
0x8b, 0xc4, 0x2e, 0xec, 0x4e, 0x7c, 0xc6, 0xce, 0x34, 0xe7, 0xcc, 0x39, 0x3d, 0x33, 0xc7, 0x8d, 0x2d, 0x82, 0xbd, 0x4e, 0x8e, 0xc7, 0xce, 0x34, 0xe7, 0xd6, 0x33, 0x73, 0xdc, 0x44, 0x02, 0xc4,
0x25, 0x90, 0x40, 0x68, 0x85, 0x84, 0x04, 0x42, 0xd0, 0x17, 0x24, 0x9e, 0x78, 0x44, 0x08, 0x24, 0x45, 0x2b, 0x24, 0x24, 0x2e, 0x82, 0xbe, 0x20, 0xf1, 0x02, 0x8f, 0x08, 0xc1, 0x03, 0x3c, 0xf2,
0xe0, 0x91, 0x3f, 0xa0, 0x8f, 0x2b, 0xf1, 0xb2, 0x4f, 0x16, 0x35, 0xff, 0x02, 0xbc, 0xe4, 0x09, 0xc8, 0x43, 0x1f, 0x57, 0xe2, 0x65, 0x9f, 0x2c, 0x6a, 0xfe, 0x05, 0x10, 0x52, 0x9e, 0x56, 0x73,
0xcd, 0xe5, 0xdc, 0xec, 0x64, 0xb7, 0xd2, 0xda, 0xdb, 0x37, 0x9f, 0xef, 0xf6, 0xfb, 0xcd, 0x37, 0x39, 0x37, 0x3b, 0xde, 0xad, 0xb4, 0x76, 0xfb, 0xe6, 0xf3, 0xdd, 0x7e, 0xbf, 0xf9, 0xe6, 0x9b,
0xdf, 0x7c, 0xf3, 0x8d, 0x61, 0xfb, 0xf8, 0x1d, 0x51, 0x67, 0x41, 0xe3, 0x38, 0x3e, 0xa4, 0x11, 0x6f, 0xbe, 0x31, 0x6c, 0x1d, 0xbf, 0xc1, 0xab, 0xcc, 0xaf, 0x1d, 0x47, 0x87, 0x34, 0xf4, 0xa8,
0xa7, 0x92, 0x8a, 0x46, 0x97, 0x72, 0x37, 0x88, 0x1a, 0x56, 0x41, 0x42, 0x46, 0x4f, 0x24, 0xe5, 0xa0, 0xbc, 0xd6, 0xa1, 0x5e, 0xd3, 0x0f, 0x6b, 0x46, 0x41, 0x02, 0x46, 0x4f, 0x04, 0xf5, 0x38,
0x82, 0x05, 0x5c, 0x5c, 0x27, 0x21, 0x13, 0x34, 0xea, 0xd2, 0xa8, 0x11, 0x1e, 0x77, 0x94, 0x4e, 0xf3, 0x3d, 0x7e, 0x8d, 0x04, 0x8c, 0xd3, 0xb0, 0x43, 0xc3, 0x5a, 0x70, 0xdc, 0x96, 0x3a, 0x9e,
0x14, 0x0d, 0x1a, 0xdd, 0x1b, 0x87, 0x54, 0x92, 0x1b, 0x8d, 0x0e, 0xe5, 0x34, 0x22, 0x92, 0xba, 0x37, 0xa8, 0x75, 0xae, 0x1f, 0x52, 0x41, 0xae, 0xd7, 0xda, 0xd4, 0xa3, 0x21, 0x11, 0xb4, 0x59,
0xf5, 0x30, 0x0a, 0x64, 0x80, 0xbe, 0x65, 0xc2, 0xd5, 0x0b, 0xd6, 0xef, 0xa7, 0xe1, 0xea, 0xe1, 0x0d, 0x42, 0x5f, 0xf8, 0xe8, 0x1b, 0x3a, 0x5c, 0x35, 0x67, 0xfd, 0x5e, 0x12, 0xae, 0x1a, 0x1c,
0x71, 0x47, 0xe9, 0x44, 0xd1, 0xa0, 0x6e, 0xc3, 0x6d, 0x5c, 0xef, 0x30, 0x79, 0x14, 0x1f, 0xd6, 0xb7, 0xa5, 0x8e, 0xe7, 0x0d, 0xaa, 0x26, 0xdc, 0xda, 0xb5, 0x36, 0x13, 0x47, 0xd1, 0x61, 0xd5,
0x5b, 0x81, 0xdf, 0xe8, 0x04, 0x9d, 0xa0, 0xa1, 0xa3, 0x1e, 0xc6, 0x6d, 0xfd, 0xa5, 0x3f, 0xf4, 0xf6, 0xdd, 0x5a, 0xdb, 0x6f, 0xfb, 0x35, 0x15, 0xf5, 0x30, 0x6a, 0xa9, 0x2f, 0xf5, 0xa1, 0x7e,
0x2f, 0x83, 0xb6, 0xf1, 0x66, 0x46, 0xde, 0x27, 0xad, 0x23, 0xc6, 0x69, 0xd4, 0xcb, 0x18, 0xfb, 0x69, 0xb4, 0xb5, 0x57, 0x53, 0xf2, 0x2e, 0xb1, 0x8f, 0x98, 0x47, 0xc3, 0xd3, 0x94, 0xb1, 0x4b,
0x54, 0x92, 0x46, 0x77, 0x84, 0xe3, 0x46, 0xe3, 0x3c, 0xaf, 0x28, 0xe6, 0x92, 0xf9, 0x74, 0xc4, 0x05, 0xa9, 0x75, 0x06, 0x38, 0xae, 0xd5, 0x86, 0x79, 0x85, 0x91, 0x27, 0x98, 0x4b, 0x07, 0x1c,
0xe1, 0xeb, 0x9f, 0xe6, 0x20, 0x5a, 0x47, 0xd4, 0x27, 0x23, 0x7e, 0x6f, 0x9c, 0xe7, 0x17, 0x4b, 0xbe, 0xfa, 0x69, 0x0e, 0xdc, 0x3e, 0xa2, 0x2e, 0x19, 0xf0, 0x7b, 0x65, 0x98, 0x5f, 0x24, 0x98,
0xe6, 0x35, 0x18, 0x97, 0x42, 0x46, 0xc3, 0x4e, 0xb5, 0x9f, 0x97, 0xa1, 0x73, 0x33, 0x16, 0x32, 0x53, 0x63, 0x9e, 0xe0, 0x22, 0xec, 0x77, 0xaa, 0xfc, 0xb4, 0x08, 0xad, 0x1b, 0x11, 0x17, 0xbe,
0xf0, 0x31, 0x15, 0x41, 0x1c, 0xb5, 0xe8, 0x0e, 0x6d, 0x33, 0xce, 0x24, 0x0b, 0x38, 0xfa, 0x00, 0x8b, 0x29, 0xf7, 0xa3, 0xd0, 0xa6, 0xdb, 0xb4, 0xc5, 0x3c, 0x26, 0x98, 0xef, 0xa1, 0xf7, 0xe1,
0xce, 0xab, 0x55, 0xb9, 0x44, 0x12, 0x07, 0x6c, 0x82, 0xad, 0xc5, 0xd7, 0xbf, 0x56, 0xcf, 0x32, 0xac, 0x5c, 0x55, 0x93, 0x08, 0x62, 0x81, 0x75, 0xb0, 0x39, 0xff, 0xf2, 0x57, 0xaa, 0x69, 0xc6,
0x9e, 0x82, 0x64, 0x69, 0x56, 0xd6, 0xf5, 0xee, 0x8d, 0xfa, 0xfe, 0xe1, 0x43, 0xda, 0x92, 0x7b, 0x13, 0x90, 0x34, 0xcd, 0xd2, 0xba, 0xda, 0xb9, 0x5e, 0xdd, 0x3f, 0xbc, 0x47, 0x6d, 0xb1, 0x47,
0x54, 0x92, 0x26, 0x7a, 0xda, 0xaf, 0x4e, 0x0d, 0xfa, 0x55, 0x98, 0xc9, 0x70, 0x1a, 0x15, 0xfd, 0x05, 0xa9, 0xa3, 0x47, 0xdd, 0xf2, 0x44, 0xaf, 0x5b, 0x86, 0xa9, 0x0c, 0x27, 0x51, 0xd1, 0x0f,
0x04, 0x4e, 0x8b, 0x90, 0xb6, 0x9c, 0x92, 0x8e, 0xfe, 0x6e, 0xfd, 0x33, 0xed, 0x67, 0xfd, 0xbc, 0xe0, 0x24, 0x0f, 0xa8, 0x6d, 0x15, 0x54, 0xf4, 0xb7, 0xaa, 0x9f, 0x69, 0x3f, 0xab, 0xc3, 0x16,
0x85, 0x1c, 0x84, 0xb4, 0xd5, 0x5c, 0xb2, 0x44, 0xa6, 0xd5, 0x17, 0xd6, 0xb0, 0xe8, 0x43, 0x00, 0xd2, 0x08, 0xa8, 0x5d, 0x5f, 0x30, 0x44, 0x26, 0xe5, 0x17, 0x56, 0xb0, 0xe8, 0x03, 0x00, 0xa7,
0x67, 0x85, 0x24, 0x32, 0x16, 0x4e, 0x59, 0x33, 0xf8, 0xe1, 0xa4, 0x18, 0x68, 0x90, 0xe6, 0x4b, 0xb9, 0x20, 0x22, 0xe2, 0x56, 0x51, 0x31, 0x78, 0x67, 0x5c, 0x0c, 0x14, 0x48, 0xfd, 0x39, 0xc3,
0x96, 0xc3, 0xac, 0xf9, 0xc6, 0x16, 0xbc, 0xf6, 0xdf, 0x12, 0xbc, 0x76, 0x9e, 0xeb, 0xcd, 0x80, 0x61, 0x5a, 0x7f, 0x63, 0x03, 0x5e, 0xf9, 0x6f, 0x01, 0x6e, 0x0c, 0x73, 0xbd, 0xe1, 0x7b, 0x4d,
0xbb, 0x66, 0x3b, 0x6e, 0xc1, 0x69, 0xd9, 0x0b, 0xa9, 0xde, 0x8a, 0x85, 0xe6, 0x5b, 0xc9, 0x7a, 0xbd, 0x1d, 0x37, 0xe1, 0xa4, 0x38, 0x0d, 0xa8, 0xda, 0x8a, 0xb9, 0xfa, 0x6b, 0xf1, 0x7a, 0x6e,
0xee, 0xf5, 0x42, 0x7a, 0xda, 0xaf, 0xbe, 0xf2, 0xa9, 0x01, 0x94, 0x21, 0xd6, 0x21, 0xd0, 0x37, 0x9f, 0x06, 0xf4, 0xac, 0x5b, 0x7e, 0xe1, 0x53, 0x03, 0x48, 0x43, 0xac, 0x42, 0xa0, 0xaf, 0x25,
0xd2, 0x75, 0x97, 0x74, 0xb0, 0x6b, 0x45, 0x62, 0xa7, 0xfd, 0xea, 0x4a, 0xea, 0x56, 0xe4, 0x8a, 0xeb, 0x2e, 0xa8, 0x60, 0x1b, 0x79, 0x62, 0x67, 0xdd, 0xf2, 0x52, 0xe2, 0x96, 0xe7, 0x8a, 0x3a,
0xba, 0x10, 0x79, 0x44, 0xc8, 0x7b, 0x11, 0xe1, 0xc2, 0x84, 0x65, 0x3e, 0xb5, 0xe9, 0xfb, 0xca, 0x10, 0x39, 0x84, 0x8b, 0xdb, 0x21, 0xf1, 0xb8, 0x0e, 0xcb, 0x5c, 0x6a, 0xd2, 0xf7, 0xa5, 0x27,
0xf3, 0x95, 0x87, 0xf2, 0x68, 0x6e, 0x58, 0x48, 0x74, 0x7b, 0x24, 0x1a, 0x3e, 0x03, 0x01, 0xbd, 0x2b, 0x0f, 0xe9, 0x51, 0x5f, 0x33, 0x90, 0x68, 0x77, 0x20, 0x1a, 0x3e, 0x07, 0x01, 0xbd, 0x08,
0x0a, 0x67, 0x23, 0x4a, 0x44, 0xc0, 0x9d, 0x69, 0x4d, 0x39, 0xcd, 0x25, 0xd6, 0x52, 0x6c, 0xb5, 0xa7, 0x43, 0x4a, 0xb8, 0xef, 0x59, 0x93, 0x8a, 0x72, 0x92, 0x4b, 0xac, 0xa4, 0xd8, 0x68, 0xd1,
0xe8, 0xcb, 0x70, 0xce, 0xa7, 0x42, 0x90, 0x0e, 0x75, 0x66, 0xb4, 0xe1, 0x8a, 0x35, 0x9c, 0xdb, 0x17, 0xe1, 0x8c, 0x4b, 0x39, 0x27, 0x6d, 0x6a, 0x4d, 0x29, 0xc3, 0x25, 0x63, 0x38, 0xb3, 0xa7,
0x33, 0x62, 0x9c, 0xe8, 0x6b, 0xa7, 0x00, 0x5e, 0x39, 0x2f, 0x6b, 0xb7, 0x99, 0x90, 0xe8, 0xbd, 0xc5, 0x38, 0xd6, 0x57, 0xce, 0x00, 0xbc, 0x32, 0x2c, 0x6b, 0xbb, 0x8c, 0x0b, 0xf4, 0xf6, 0xc0,
0x91, 0x03, 0x50, 0x7f, 0xbe, 0x15, 0x2a, 0x6f, 0x5d, 0xfe, 0xab, 0x16, 0x7c, 0x3e, 0x91, 0xe4, 0x01, 0xa8, 0x3e, 0xd9, 0x0a, 0xa5, 0xb7, 0x2a, 0xff, 0x65, 0x03, 0x3e, 0x1b, 0x4b, 0x32, 0xc5,
0x8a, 0xff, 0xc7, 0x70, 0x86, 0x49, 0xea, 0xab, 0x3d, 0x28, 0x6f, 0x2d, 0xbe, 0xfe, 0xfd, 0x09, 0xff, 0x7d, 0x38, 0xc5, 0x04, 0x75, 0xe5, 0x1e, 0x14, 0x37, 0xe7, 0x5f, 0xfe, 0xce, 0x98, 0x6a,
0xd5, 0x5e, 0x73, 0xd9, 0x72, 0x98, 0xb9, 0xa5, 0xd0, 0xb0, 0x01, 0xad, 0xfd, 0x0f, 0xc0, 0xab, 0xaf, 0xbe, 0x68, 0x38, 0x4c, 0xdd, 0x94, 0x68, 0x58, 0x83, 0x56, 0xfe, 0x07, 0xe0, 0xd5, 0x61,
0xe7, 0xb9, 0xdc, 0x21, 0x3e, 0x15, 0x2a, 0xe3, 0xa1, 0x17, 0x47, 0xc4, 0xb3, 0x15, 0x97, 0x66, 0x2e, 0xb7, 0x88, 0x4b, 0xb9, 0xcc, 0x78, 0xe0, 0x44, 0x21, 0x71, 0x4c, 0xc5, 0x25, 0x19, 0x3f,
0xfc, 0xae, 0x96, 0x62, 0xab, 0x45, 0xaf, 0xc1, 0x79, 0xc1, 0x78, 0x27, 0xf6, 0x48, 0x64, 0xcb, 0x50, 0x52, 0x6c, 0xb4, 0xe8, 0x25, 0x38, 0xcb, 0x99, 0xd7, 0x8e, 0x1c, 0x12, 0x9a, 0x72, 0x4a,
0x29, 0x5d, 0xf5, 0x81, 0x95, 0xe3, 0xd4, 0x02, 0xd5, 0x21, 0x14, 0x47, 0x41, 0x24, 0x35, 0x86, 0x56, 0xdd, 0x30, 0x72, 0x9c, 0x58, 0xa0, 0x2a, 0x84, 0xfc, 0xc8, 0x0f, 0x85, 0xc2, 0xb0, 0x8a,
0x53, 0xde, 0x2c, 0xab, 0xc8, 0xaa, 0x41, 0x1c, 0xa4, 0x52, 0x9c, 0xb3, 0x40, 0x9b, 0x70, 0xfa, 0xeb, 0x45, 0x19, 0x59, 0x36, 0x88, 0x46, 0x22, 0xc5, 0x19, 0x0b, 0xb4, 0x0e, 0x27, 0x8f, 0x99,
0x98, 0x71, 0xd7, 0xee, 0x7a, 0x7a, 0x8a, 0xbf, 0xc7, 0xb8, 0x8b, 0xb5, 0x46, 0xe1, 0x7b, 0x4c, 0xd7, 0x34, 0xbb, 0x9e, 0x9c, 0xe2, 0x6f, 0x33, 0xaf, 0x89, 0x95, 0x46, 0xe2, 0x3b, 0x8c, 0x0b,
0x48, 0x25, 0xb1, 0x5b, 0x5e, 0xc8, 0xba, 0xb6, 0x4c, 0x2d, 0x6a, 0x7f, 0x2b, 0x9f, 0xbf, 0xe9, 0x29, 0x31, 0x5b, 0x9e, 0xcb, 0xba, 0xb2, 0x4c, 0x2c, 0x2a, 0xff, 0x9c, 0x1c, 0xbe, 0xe9, 0xb2,
0xaa, 0x35, 0xa0, 0x97, 0xe1, 0x4c, 0x27, 0x0a, 0xe2, 0xd0, 0xae, 0x3a, 0xcd, 0xde, 0x77, 0x94, 0x35, 0xa0, 0xe7, 0xe1, 0x54, 0x3b, 0xf4, 0xa3, 0xc0, 0xac, 0x3a, 0xc9, 0xde, 0xb7, 0xa4, 0x10,
0x10, 0x1b, 0x9d, 0xaa, 0xb2, 0x2e, 0x8d, 0xd4, 0x06, 0xd8, 0x25, 0xa7, 0x55, 0xf6, 0xc0, 0x88, 0x6b, 0x9d, 0xac, 0xb2, 0x0e, 0x0d, 0xe5, 0x06, 0x98, 0x25, 0x27, 0x55, 0x76, 0x57, 0x8b, 0x71,
0x71, 0xa2, 0x47, 0x3f, 0x03, 0x70, 0x86, 0xdb, 0xc5, 0xaa, 0x12, 0x7a, 0x6f, 0x42, 0xfb, 0xac, 0xac, 0x47, 0x3f, 0x06, 0x70, 0xca, 0x33, 0x8b, 0x95, 0x25, 0xf4, 0xf6, 0x98, 0xf6, 0x59, 0xa5,
0xd3, 0x95, 0xd1, 0x35, 0x99, 0x34, 0xc8, 0xe8, 0x4d, 0x38, 0x23, 0x5a, 0x41, 0x48, 0x6d, 0x16, 0x2b, 0xa5, 0xab, 0x33, 0xa9, 0x91, 0xd1, 0xab, 0x70, 0x8a, 0xdb, 0x7e, 0x40, 0x4d, 0x16, 0x4b,
0x2b, 0x89, 0xd1, 0x81, 0x12, 0x9e, 0xf6, 0xab, 0xcb, 0x49, 0x38, 0x2d, 0xc0, 0xc6, 0x18, 0xfd, 0xb1, 0x51, 0x43, 0x0a, 0xcf, 0xba, 0xe5, 0xc5, 0x38, 0x9c, 0x12, 0x60, 0x6d, 0x8c, 0x7e, 0x06,
0x02, 0x40, 0xd8, 0x25, 0x1e, 0x73, 0x89, 0x8a, 0xaf, 0x73, 0x3b, 0xee, 0x32, 0x7d, 0x90, 0x86, 0x20, 0xec, 0x10, 0x87, 0x35, 0x89, 0x8c, 0xaf, 0x72, 0x3b, 0xea, 0x32, 0xbd, 0x9b, 0x84, 0xd7,
0x37, 0x45, 0x90, 0x7d, 0xe3, 0x1c, 0x74, 0xed, 0x5f, 0x25, 0x58, 0xf9, 0xe4, 0xde, 0x8a, 0x9e, 0x45, 0x90, 0x7e, 0xe3, 0x0c, 0x34, 0xfa, 0x15, 0x80, 0x0b, 0x3c, 0x3a, 0x0c, 0x8d, 0x17, 0xb7,
0x00, 0x08, 0x5b, 0x49, 0xcf, 0x12, 0x0e, 0xd0, 0x67, 0xea, 0x83, 0x09, 0xe5, 0x3a, 0x6d, 0x8e, 0xa6, 0x15, 0x97, 0xef, 0x8e, 0x94, 0x4b, 0x23, 0x03, 0x50, 0x5f, 0xee, 0x75, 0xcb, 0x0b, 0x59,
0xd9, 0xfd, 0x96, 0x8a, 0x04, 0xce, 0xf1, 0x40, 0xbf, 0x07, 0x70, 0x99, 0xb4, 0x5a, 0x34, 0x94, 0x09, 0xce, 0x11, 0xa8, 0xfc, 0xab, 0x00, 0x4b, 0x9f, 0xdc, 0xed, 0xd1, 0x43, 0x00, 0xa1, 0x1d,
0xd4, 0x35, 0x25, 0x5f, 0xfa, 0x1c, 0xaa, 0x60, 0xdd, 0xb2, 0x5a, 0xde, 0xce, 0x43, 0xe3, 0x22, 0x77, 0x51, 0x6e, 0x01, 0x75, 0xca, 0xdf, 0x1f, 0xd3, 0xee, 0x27, 0xed, 0x3a, 0xbd, 0x71, 0x13,
0x93, 0xda, 0x9f, 0xc0, 0xf0, 0xe5, 0x9f, 0xa5, 0x1f, 0xfd, 0x0a, 0xc0, 0x95, 0x20, 0xa4, 0x7c, 0x11, 0xc7, 0x19, 0x1e, 0xe8, 0x77, 0x00, 0x2e, 0x12, 0xdb, 0xa6, 0x81, 0xa0, 0x4d, 0x7d, 0x08,
0xfb, 0xee, 0xad, 0x07, 0x6f, 0x1c, 0xe8, 0x91, 0xc3, 0xf6, 0xc0, 0x3b, 0x9f, 0x91, 0xfa, 0x77, 0x0b, 0x4f, 0xa1, 0x2e, 0x57, 0x0d, 0xab, 0xc5, 0xad, 0x2c, 0x34, 0xce, 0x33, 0xa9, 0xfc, 0x1f,
0x0f, 0xf6, 0xef, 0x98, 0x80, 0x77, 0xa3, 0x20, 0x14, 0xcd, 0x0b, 0x83, 0x7e, 0x75, 0x65, 0xbf, 0xf4, 0x67, 0x35, 0xb3, 0x05, 0x0d, 0x9b, 0x38, 0x14, 0x6d, 0xc3, 0x65, 0x79, 0x77, 0x63, 0x1a,
0x08, 0x85, 0x87, 0xb1, 0x6b, 0x3e, 0x5c, 0xdf, 0x3d, 0x91, 0x34, 0xe2, 0xc4, 0xdb, 0x09, 0x5a, 0x38, 0xcc, 0x26, 0xfc, 0x80, 0x88, 0x23, 0x73, 0x52, 0x2d, 0x03, 0xb1, 0xdc, 0xe8, 0xd3, 0xe3,
0xb1, 0x4f, 0xb9, 0x34, 0x44, 0xdf, 0x82, 0x8b, 0x2e, 0x15, 0xad, 0x88, 0x85, 0xba, 0x4a, 0xcd, 0x01, 0x0f, 0xf4, 0x26, 0x44, 0xfa, 0x3e, 0xcb, 0xc5, 0xd1, 0x47, 0x39, 0xb9, 0x99, 0x1a, 0x03,
0xa9, 0xbd, 0x60, 0x13, 0xb0, 0xb8, 0x93, 0xa9, 0x70, 0xde, 0x0e, 0x5d, 0x85, 0xe5, 0x38, 0xf2, 0x16, 0xf8, 0x1c, 0x2f, 0x74, 0x03, 0xae, 0x38, 0xe4, 0x90, 0x3a, 0x0d, 0xea, 0x50, 0x5b, 0xf8,
0xec, 0xe9, 0x5d, 0xb4, 0xe6, 0xe5, 0xfb, 0xf8, 0x36, 0x56, 0xf2, 0xda, 0x35, 0x38, 0xad, 0x78, 0xa1, 0x0a, 0x55, 0x54, 0xa1, 0x56, 0x7b, 0xdd, 0xf2, 0xca, 0x6e, 0xbf, 0x12, 0x0f, 0xda, 0x57,
0xa2, 0xcb, 0xb0, 0x1c, 0x91, 0xc7, 0x3a, 0xea, 0x52, 0x73, 0x4e, 0x99, 0x60, 0xf2, 0x18, 0x2b, 0x36, 0x60, 0x79, 0xf8, 0xc2, 0xf5, 0x94, 0xf0, 0x87, 0x02, 0x5c, 0x1b, 0x5e, 0xb1, 0xe8, 0x27,
0x59, 0xed, 0xcf, 0x57, 0xe0, 0xca, 0xd0, 0x5a, 0xd0, 0x06, 0x2c, 0x31, 0xd7, 0x72, 0x80, 0x36, 0xe9, 0x30, 0xa3, 0xef, 0xaa, 0x77, 0xc7, 0x75, 0x3a, 0xcc, 0x34, 0x03, 0x07, 0x27, 0x19, 0xf4,
0x68, 0xe9, 0xd6, 0x0e, 0x2e, 0x31, 0x17, 0xbd, 0x0d, 0x67, 0xcd, 0xe8, 0x66, 0x41, 0xab, 0xe9, 0x43, 0xd9, 0x68, 0x88, 0x43, 0x4d, 0x4d, 0xbd, 0x33, 0x36, 0x0a, 0x12, 0xa4, 0x3e, 0xa7, 0x7b,
0xa5, 0xab, 0xa5, 0xea, 0x18, 0x66, 0xe1, 0x14, 0x11, 0x6b, 0xae, 0x39, 0xd0, 0xb6, 0x6e, 0x1f, 0x18, 0x71, 0x54, 0xcb, 0x22, 0x0e, 0xad, 0xfc, 0x09, 0xf4, 0xcf, 0xb3, 0x69, 0x47, 0x41, 0xbf,
0x0b, 0x96, 0x03, 0x6d, 0x63, 0x25, 0x1b, 0x5e, 0xfc, 0xf4, 0x73, 0x2e, 0x7e, 0xd3, 0x8e, 0x12, 0x00, 0x70, 0xc9, 0x0f, 0xa8, 0xb7, 0x75, 0x70, 0xf3, 0xee, 0x2b, 0x0d, 0x35, 0x45, 0x9b, 0x54,
0x33, 0xc5, 0xa6, 0x9a, 0x9b, 0x10, 0x5e, 0x85, 0xb3, 0xed, 0x20, 0xf2, 0x89, 0x74, 0x66, 0x8b, 0xdd, 0xfa, 0x8c, 0x3c, 0xdf, 0x6c, 0xec, 0xdf, 0xd2, 0x01, 0x0f, 0x42, 0x3f, 0xe0, 0xf5, 0x0b,
0xcd, 0xff, 0xdb, 0x5a, 0x8a, 0xad, 0x56, 0x75, 0x4b, 0xc9, 0xa4, 0x47, 0x9d, 0xb9, 0x62, 0xb7, 0xbd, 0x6e, 0x79, 0x69, 0x3f, 0x0f, 0x85, 0xfb, 0xb1, 0x2b, 0x2e, 0x5c, 0xdd, 0x39, 0x11, 0x34,
0xbc, 0xa7, 0x84, 0xd8, 0xe8, 0xd0, 0x43, 0x38, 0xe7, 0xd2, 0x36, 0x89, 0x3d, 0xe9, 0xcc, 0xeb, 0xf4, 0x88, 0xb3, 0xed, 0xdb, 0x91, 0x4b, 0x3d, 0xa1, 0x89, 0xbe, 0x06, 0xe7, 0x9b, 0x94, 0xdb,
0x12, 0xba, 0x39, 0x86, 0x12, 0x6a, 0x2e, 0xaa, 0x76, 0xbb, 0x63, 0xe2, 0xe2, 0x04, 0x00, 0xbd, 0x21, 0x0b, 0x54, 0xe3, 0xd5, 0xe5, 0x7d, 0xc1, 0x94, 0xe5, 0xfc, 0x76, 0xaa, 0xc2, 0x59, 0x3b,
0x02, 0xe7, 0x7c, 0x72, 0xc2, 0xfc, 0xd8, 0x77, 0x16, 0x36, 0xc1, 0x16, 0x30, 0x66, 0x7b, 0x46, 0x74, 0x15, 0x16, 0xa3, 0xd0, 0x31, 0x55, 0x3c, 0x6f, 0xcc, 0x8b, 0x77, 0xf0, 0x2e, 0x96, 0xf2,
0x84, 0x13, 0x1d, 0xda, 0x81, 0xab, 0xf4, 0xa4, 0xe5, 0xc5, 0x82, 0x75, 0xa9, 0x55, 0x3a, 0x70, 0xca, 0x06, 0x9c, 0x94, 0x3c, 0xd1, 0x65, 0x58, 0x0c, 0xc9, 0x03, 0x15, 0x75, 0xa1, 0x3e, 0x23,
0x13, 0x6c, 0xcd, 0x37, 0x1d, 0xbb, 0x84, 0xd5, 0xdd, 0x21, 0x3d, 0x1e, 0xf1, 0xd0, 0x60, 0x8c, 0x4d, 0x30, 0x79, 0x80, 0xa5, 0xac, 0xf2, 0xe7, 0x2b, 0x70, 0xa9, 0x6f, 0x2d, 0x68, 0x0d, 0x16,
0x6b, 0xe7, 0xc5, 0x1c, 0x98, 0x11, 0xe1, 0x44, 0x57, 0x04, 0xb3, 0xf6, 0x4b, 0xe7, 0x81, 0x59, 0x58, 0xd3, 0x70, 0x80, 0x26, 0x68, 0xe1, 0xe6, 0x36, 0x2e, 0xb0, 0x26, 0x7a, 0x1d, 0x4e, 0xeb,
0xe7, 0x11, 0x0f, 0xf4, 0x55, 0xb8, 0xe0, 0x93, 0x93, 0xdb, 0x94, 0x77, 0xe4, 0x91, 0xb3, 0xbc, 0xd7, 0x88, 0x01, 0x2d, 0x27, 0x73, 0xa4, 0x92, 0xca, 0x9b, 0x25, 0x0d, 0x27, 0x89, 0x18, 0x73,
0x09, 0xb6, 0xca, 0xcd, 0xe5, 0x41, 0xbf, 0xba, 0xb0, 0x97, 0x08, 0x71, 0xa6, 0xd7, 0xc6, 0x8c, 0xc5, 0x81, 0xb6, 0xcc, 0x29, 0xd1, 0x1c, 0x68, 0x0b, 0x4b, 0x59, 0xff, 0xe2, 0x27, 0x9f, 0x70,
0x5b, 0xe3, 0x97, 0x72, 0xc6, 0x89, 0x10, 0x67, 0x7a, 0x75, 0x9b, 0x85, 0x44, 0xaa, 0xc3, 0xe5, 0xf1, 0xeb, 0x66, 0x3a, 0x9e, 0xca, 0xcf, 0x09, 0x99, 0xa1, 0xf7, 0x45, 0x38, 0xdd, 0xf2, 0x43,
0xac, 0x14, 0x6f, 0xb3, 0xbb, 0x46, 0x8c, 0x13, 0x3d, 0xda, 0x82, 0xf3, 0x3e, 0x39, 0xd1, 0x93, 0x97, 0x08, 0x75, 0x7b, 0x64, 0xe6, 0x99, 0x6f, 0x2a, 0x29, 0x36, 0x5a, 0x39, 0x00, 0x08, 0x26,
0x84, 0xb3, 0xaa, 0xc3, 0x2e, 0xa9, 0x8b, 0x76, 0xcf, 0xca, 0x70, 0xaa, 0xd5, 0x96, 0x8c, 0x1b, 0x1c, 0x6a, 0xcd, 0xe4, 0x07, 0x80, 0xdb, 0x52, 0x88, 0xb5, 0x0e, 0xdd, 0x83, 0x33, 0x4d, 0xda,
0xcb, 0xb5, 0x9c, 0xa5, 0x95, 0xe1, 0x54, 0xab, 0x8a, 0x38, 0xe6, 0xec, 0x51, 0x4c, 0x8d, 0x31, 0x22, 0x91, 0x23, 0xac, 0x59, 0x55, 0x42, 0x37, 0x46, 0x50, 0x42, 0xf5, 0x79, 0x39, 0x41, 0x6c,
0xd2, 0x99, 0x49, 0x8b, 0xf8, 0x7e, 0xa6, 0xc2, 0x79, 0x3b, 0x35, 0x49, 0xf8, 0xb1, 0x27, 0x59, 0xeb, 0xb8, 0x38, 0x06, 0x40, 0x2f, 0xc0, 0x19, 0x97, 0x9c, 0x30, 0x37, 0x72, 0xad, 0xb9, 0x75,
0xe8, 0xd1, 0xfd, 0xb6, 0x73, 0x41, 0xe7, 0x5f, 0x5f, 0x22, 0x7b, 0xa9, 0x14, 0xe7, 0x2c, 0x10, 0xb0, 0x09, 0xb4, 0xd9, 0x9e, 0x16, 0xe1, 0x58, 0x27, 0x3b, 0x23, 0x3d, 0xb1, 0x9d, 0x88, 0xb3,
0x85, 0xd3, 0x94, 0xc7, 0xbe, 0x73, 0x51, 0x5f, 0x0d, 0x63, 0x29, 0xc1, 0xf4, 0xe4, 0xec, 0xf2, 0x0e, 0x35, 0x4a, 0x0b, 0xae, 0x83, 0xcd, 0xd9, 0xb4, 0x33, 0xee, 0xf4, 0xe9, 0xf1, 0x80, 0x87,
0xd8, 0xc7, 0x3a, 0x3c, 0x7a, 0x1b, 0x2e, 0xfb, 0xe4, 0x44, 0xb5, 0x03, 0x1a, 0x49, 0x46, 0x85, 0x02, 0x63, 0x9e, 0x72, 0x9e, 0xcf, 0x80, 0x69, 0x11, 0x8e, 0x75, 0x79, 0x30, 0x63, 0xbf, 0x30,
0xb3, 0xae, 0x17, 0xbf, 0xa6, 0xda, 0xf1, 0x5e, 0x5e, 0x81, 0x8b, 0x76, 0xda, 0x91, 0xf1, 0x9c, 0x0c, 0xcc, 0x38, 0x0f, 0x78, 0xa0, 0x2f, 0xc3, 0x39, 0x97, 0x9c, 0xec, 0x52, 0xaf, 0x2d, 0x8e,
0xe3, 0xa5, 0x9c, 0x63, 0x5e, 0x81, 0x8b, 0x76, 0x2a, 0xd3, 0x11, 0x7d, 0x14, 0xb3, 0x88, 0xba, 0xac, 0xc5, 0x75, 0xb0, 0x59, 0xac, 0x2f, 0xf6, 0xba, 0xe5, 0xb9, 0xbd, 0x58, 0x88, 0x53, 0xbd,
0xce, 0x17, 0xf4, 0x40, 0xa5, 0x33, 0x8d, 0xad, 0x0c, 0xa7, 0x5a, 0xd4, 0x4d, 0x46, 0x4e, 0x47, 0x32, 0x66, 0x9e, 0x31, 0x7e, 0x2e, 0x63, 0x1c, 0x0b, 0x71, 0xaa, 0x97, 0x03, 0x5a, 0x40, 0x84,
0x1f, 0xc3, 0xfb, 0xe3, 0xed, 0xe4, 0xfb, 0xd1, 0x76, 0x14, 0x91, 0x5e, 0x73, 0x61, 0x78, 0xd8, 0x3c, 0x5c, 0xd6, 0x52, 0x7e, 0x40, 0x3b, 0xd0, 0x62, 0x1c, 0xeb, 0xd1, 0x26, 0x9c, 0x75, 0xc9,
0x44, 0x02, 0xce, 0x10, 0xcf, 0xdb, 0x6f, 0x3b, 0x97, 0x75, 0xee, 0xc7, 0x7d, 0x83, 0xa4, 0x5d, 0x89, 0x1a, 0x8e, 0xad, 0x65, 0x15, 0x76, 0x41, 0xce, 0x8e, 0x7b, 0x46, 0x86, 0x13, 0xad, 0xb2,
0x67, 0x5b, 0x81, 0x60, 0x83, 0xa5, 0x40, 0x03, 0xae, 0x4a, 0x63, 0x63, 0xb2, 0xa0, 0xfb, 0x0a, 0x64, 0x9e, 0xb6, 0x5c, 0xc9, 0x58, 0x1a, 0x19, 0x4e, 0xb4, 0xb2, 0x88, 0x23, 0x8f, 0xdd, 0x8f,
0x04, 0x1b, 0x2c, 0xbd, 0x52, 0xde, 0xdb, 0x6f, 0x3b, 0x5f, 0x9c, 0xf0, 0x4a, 0x15, 0x08, 0x36, 0xa8, 0x36, 0x46, 0x2a, 0x33, 0x49, 0x11, 0xdf, 0x49, 0x55, 0x38, 0x6b, 0x27, 0x87, 0x63, 0x37,
0x58, 0x88, 0xc1, 0x32, 0x0f, 0xa4, 0x73, 0x65, 0x22, 0xd7, 0xb3, 0xbe, 0x70, 0xee, 0x04, 0x12, 0x72, 0x04, 0x0b, 0x1c, 0xba, 0xdf, 0xb2, 0x2e, 0xa8, 0xfc, 0xab, 0xb9, 0x68, 0x2f, 0x91, 0xe2,
0x2b, 0x0c, 0xf4, 0x5b, 0x00, 0x61, 0x98, 0x95, 0xe8, 0x55, 0xbd, 0xca, 0x1f, 0x8d, 0x17, 0xb2, 0x8c, 0x05, 0xa2, 0x70, 0x92, 0x7a, 0x91, 0x6b, 0x5d, 0x54, 0xb3, 0xc5, 0x48, 0x4a, 0x30, 0x39,
0x9e, 0xd5, 0xf6, 0x2e, 0x97, 0x51, 0x2f, 0x1b, 0xb2, 0x72, 0x67, 0x20, 0xc7, 0x02, 0xfd, 0x11, 0x39, 0x3b, 0x5e, 0xe4, 0x62, 0x15, 0x1e, 0xbd, 0x0e, 0x17, 0x5d, 0x72, 0x22, 0xdb, 0x01, 0x0d,
0xc0, 0x8b, 0xc4, 0x35, 0x23, 0x17, 0xf1, 0x72, 0x27, 0xa8, 0xa2, 0x33, 0x72, 0x6f, 0xdc, 0x65, 0x05, 0xa3, 0xdc, 0x5a, 0x55, 0x8b, 0x5f, 0x91, 0xf7, 0xf9, 0x5e, 0x56, 0x81, 0xf3, 0x76, 0xca,
0xde, 0x0c, 0x02, 0xaf, 0xe9, 0x0c, 0xfa, 0xd5, 0x8b, 0xdb, 0x67, 0xa0, 0xe2, 0x33, 0xb9, 0xa0, 0x91, 0x79, 0x19, 0xc7, 0x4b, 0x19, 0xc7, 0xac, 0x02, 0xe7, 0xed, 0x64, 0xa6, 0x43, 0x7a, 0x3f,
0xbf, 0x00, 0xb8, 0x66, 0xbb, 0x68, 0x8e, 0x61, 0x55, 0x27, 0x90, 0x8e, 0x3b, 0x81, 0xc3, 0x38, 0x62, 0x21, 0x6d, 0x5a, 0x9f, 0x53, 0x6f, 0x04, 0x95, 0x69, 0x6c, 0x64, 0x38, 0xd1, 0xa2, 0x4e,
0x26, 0x8f, 0x97, 0x6d, 0x1e, 0xd7, 0x46, 0xf4, 0x78, 0x94, 0x1a, 0xfa, 0x07, 0x80, 0x4b, 0x2e, 0xfc, 0x8a, 0xb2, 0xd4, 0x31, 0xbc, 0x33, 0xda, 0x4e, 0xbe, 0x1f, 0x6e, 0x85, 0x21, 0x39, 0xd5,
0x0d, 0x29, 0x77, 0x29, 0x6f, 0x29, 0xae, 0x9b, 0x63, 0x99, 0xa9, 0x87, 0xb9, 0xee, 0xe4, 0x20, 0x37, 0x4d, 0xf6, 0xfd, 0x84, 0x38, 0x9c, 0x22, 0x8e, 0xb3, 0xdf, 0xb2, 0x2e, 0xab, 0xdc, 0x8f,
0x0c, 0xcd, 0xba, 0xa5, 0xb9, 0x94, 0x57, 0x9d, 0xf6, 0xab, 0x97, 0x32, 0xd7, 0xbc, 0x06, 0x17, 0xfa, 0x06, 0x49, 0xba, 0xce, 0x96, 0x04, 0xc1, 0x1a, 0x4b, 0x82, 0xfa, 0x9e, 0x2c, 0x8d, 0xb5,
0x58, 0xa2, 0xdf, 0x01, 0xb8, 0x92, 0x6d, 0x80, 0xb9, 0x52, 0xae, 0x4d, 0xb0, 0x0e, 0xf4, 0xf8, 0xf1, 0x82, 0xee, 0x4b, 0x10, 0xac, 0xb1, 0xd4, 0x4a, 0xbd, 0xd3, 0xfd, 0x96, 0xf5, 0xf9, 0x31,
0xba, 0x5d, 0x04, 0xc4, 0xc3, 0x0c, 0xd0, 0x5f, 0x81, 0x9a, 0xd4, 0x92, 0x29, 0x5d, 0x38, 0x35, 0xaf, 0x54, 0x82, 0x60, 0x8d, 0x85, 0x18, 0x2c, 0x7a, 0xbe, 0xb0, 0xae, 0x8c, 0xe5, 0x7a, 0x56,
0x9d, 0xcb, 0xf7, 0xc7, 0x9e, 0xcb, 0x14, 0xc1, 0xa4, 0xf2, 0xb5, 0x6c, 0x14, 0x4c, 0x35, 0xa7, 0x17, 0xce, 0x2d, 0x5f, 0x60, 0x89, 0x81, 0x7e, 0x03, 0x20, 0x0c, 0xd2, 0x12, 0xbd, 0xaa, 0x56,
0xfd, 0xea, 0x7a, 0x3e, 0x93, 0xa9, 0x02, 0xe7, 0x19, 0xa2, 0x5f, 0x02, 0xb8, 0x44, 0xb3, 0x89, 0xf9, 0xee, 0x68, 0x21, 0xab, 0x69, 0x6d, 0xef, 0x78, 0x22, 0x3c, 0x4d, 0xa7, 0xf4, 0xcc, 0x19,
0x5b, 0x38, 0x2f, 0x8f, 0x25, 0x89, 0x67, 0x0e, 0xf1, 0xcd, 0x55, 0xb5, 0xdd, 0x39, 0x95, 0xc0, 0xc8, 0xb0, 0x40, 0x7f, 0x04, 0xf0, 0x22, 0x69, 0xea, 0x99, 0x9d, 0x38, 0x99, 0x13, 0x54, 0x52,
0x05, 0x6c, 0x35, 0x41, 0xd2, 0x13, 0xe2, 0x87, 0x1e, 0x75, 0xbe, 0x34, 0xe6, 0x09, 0x72, 0xd7, 0x19, 0xb9, 0x3d, 0xea, 0x32, 0xaf, 0xfb, 0xbe, 0x53, 0xb7, 0x7a, 0xdd, 0xf2, 0xc5, 0xad, 0x73,
0xc4, 0xc5, 0x09, 0xc0, 0x86, 0x7a, 0xf9, 0x0c, 0x9d, 0x1c, 0xb4, 0x0a, 0xcb, 0xc7, 0xb4, 0x67, 0x50, 0xf1, 0xb9, 0x5c, 0xd0, 0x5f, 0x00, 0x5c, 0x31, 0x5d, 0x34, 0xc3, 0xb0, 0xac, 0x12, 0x48,
0x06, 0x7b, 0xac, 0x7e, 0x22, 0x17, 0xce, 0x74, 0x89, 0x17, 0x53, 0xfb, 0x9e, 0x1b, 0x73, 0xd7, 0x47, 0x9d, 0xc0, 0x7e, 0x1c, 0x9d, 0xc7, 0xcb, 0x26, 0x8f, 0x2b, 0x03, 0x7a, 0x3c, 0x48, 0x0d,
0xc5, 0x26, 0xf8, 0x37, 0x4b, 0xef, 0x80, 0x8d, 0x27, 0x00, 0x5e, 0x3a, 0xfb, 0x40, 0xbf, 0x50, 0xfd, 0x1d, 0xc0, 0x85, 0x26, 0x0d, 0xa8, 0xd7, 0xa4, 0x9e, 0x2d, 0xb9, 0xae, 0x8f, 0xe4, 0x51,
0x5a, 0x7f, 0x00, 0x70, 0x6d, 0xe4, 0xec, 0x9e, 0xc1, 0xe8, 0x51, 0x91, 0xd1, 0xbb, 0xe3, 0x3e, 0xd6, 0xcf, 0x75, 0x3b, 0x03, 0xa1, 0x69, 0x56, 0x0d, 0xcd, 0x85, 0xac, 0xea, 0xac, 0x5b, 0xbe,
0x84, 0x07, 0x32, 0x62, 0xbc, 0xa3, 0x27, 0x8f, 0x3c, 0xbd, 0x5f, 0x03, 0xb8, 0x3a, 0x7c, 0x1c, 0x94, 0xba, 0x66, 0x35, 0x38, 0xc7, 0x12, 0xfd, 0x16, 0xc0, 0xa5, 0x74, 0x03, 0xf4, 0x95, 0xb2,
0x5e, 0x64, 0xbe, 0x6a, 0x4f, 0x4a, 0xf0, 0xd2, 0xd9, 0x03, 0x13, 0x8a, 0xd2, 0x97, 0xe1, 0x64, 0x31, 0xc6, 0x3a, 0x50, 0xe3, 0xeb, 0x56, 0x1e, 0x10, 0xf7, 0x33, 0x40, 0x7f, 0x05, 0x72, 0x52,
0x5e, 0xd8, 0x30, 0x7b, 0x65, 0xa6, 0x8f, 0xca, 0x0f, 0x01, 0x5c, 0x7c, 0x98, 0xda, 0x25, 0x7f, 0x8b, 0x9f, 0x79, 0xdc, 0xaa, 0xa8, 0x5c, 0xbe, 0x37, 0xf2, 0x5c, 0x26, 0x08, 0x3a, 0x95, 0x2f,
0x42, 0x8e, 0xfd, 0x6d, 0x9f, 0xf4, 0x9f, 0x4c, 0x21, 0x70, 0x1e, 0xb7, 0xf6, 0x77, 0x00, 0xd7, 0xa5, 0xa3, 0x60, 0xa2, 0x39, 0xeb, 0x96, 0x57, 0xb3, 0x99, 0x4c, 0x14, 0x38, 0xcb, 0x10, 0xfd,
0xcf, 0x6c, 0xac, 0xea, 0x09, 0x4a, 0x3c, 0x2f, 0x78, 0x2c, 0x74, 0x56, 0xe6, 0xb3, 0x27, 0xe8, 0x1c, 0xc0, 0x05, 0x9a, 0x4e, 0xdc, 0xdc, 0x7a, 0x7e, 0x24, 0x49, 0x3c, 0x77, 0x88, 0xd7, 0xff,
0xb6, 0x96, 0x62, 0xab, 0xcd, 0x65, 0xaf, 0xf4, 0x79, 0x65, 0xaf, 0xf6, 0x4f, 0x00, 0xaf, 0x7c, 0x20, 0x64, 0x54, 0x1c, 0xe7, 0xb0, 0xe5, 0x04, 0x49, 0x4f, 0x88, 0x1b, 0x38, 0xd4, 0xfa, 0xc2,
0x52, 0x25, 0xbe, 0x90, 0x2d, 0xdd, 0x82, 0xf3, 0x76, 0x28, 0xea, 0xe9, 0xed, 0xb4, 0xef, 0x00, 0x88, 0x27, 0xc8, 0x1d, 0x1d, 0x17, 0xc7, 0x00, 0x6b, 0xf2, 0xe5, 0xd3, 0x77, 0x72, 0xd0, 0x32,
0xdb, 0x34, 0x7a, 0x38, 0xd5, 0x36, 0xaf, 0x3f, 0x7d, 0x56, 0x99, 0xfa, 0xe8, 0x59, 0x65, 0xea, 0x2c, 0x1e, 0xd3, 0x53, 0x3d, 0xd8, 0x63, 0xf9, 0x13, 0x35, 0xe1, 0x54, 0x87, 0x38, 0x51, 0xfc,
0xe3, 0x67, 0x95, 0xa9, 0x9f, 0x0e, 0x2a, 0xe0, 0xe9, 0xa0, 0x02, 0x3e, 0x1a, 0x54, 0xc0, 0xc7, 0x78, 0x1b, 0x71, 0xd7, 0xc5, 0x3a, 0xf8, 0xd7, 0x0b, 0x6f, 0x80, 0xb5, 0x87, 0x00, 0x5e, 0x3a,
0x83, 0x0a, 0xf8, 0xf7, 0xa0, 0x02, 0x7e, 0xf3, 0x9f, 0xca, 0xd4, 0x0f, 0xe6, 0x2c, 0xf8, 0xff, 0xff, 0x40, 0x3f, 0x53, 0x5a, 0xbf, 0x07, 0x70, 0x65, 0xe0, 0xec, 0x9e, 0xc3, 0xe8, 0x7e, 0x9e,
0x03, 0x00, 0x00, 0xff, 0xff, 0x73, 0x96, 0x20, 0xc5, 0xd0, 0x1b, 0x00, 0x00, 0xd1, 0x5b, 0xa3, 0x3e, 0x84, 0x0d, 0x11, 0x32, 0xaf, 0xad, 0x26, 0x8f, 0x2c, 0xbd, 0x5f, 0x02,
0xb8, 0xdc, 0x7f, 0x1c, 0x9e, 0x65, 0xbe, 0x2a, 0x0f, 0x0b, 0xf0, 0xd2, 0xf9, 0x03, 0x13, 0x0a,
0x93, 0x97, 0xe1, 0x78, 0x5e, 0xd8, 0x30, 0x7d, 0x65, 0x26, 0x8f, 0xca, 0x0f, 0x00, 0x9c, 0xbf,
0x97, 0xd8, 0xc5, 0xff, 0xab, 0x8f, 0xfc, 0x6d, 0x1f, 0xf7, 0x9f, 0x54, 0xc1, 0x71, 0x16, 0xb7,
0xf2, 0x37, 0x00, 0x57, 0xcf, 0x6d, 0xac, 0xf2, 0x09, 0x4a, 0x1c, 0xc7, 0x7f, 0xa0, 0xff, 0xa2,
0x99, 0x4d, 0x9f, 0xa0, 0x5b, 0x4a, 0x8a, 0x8d, 0x36, 0x93, 0xbd, 0xc2, 0xd3, 0xca, 0x5e, 0xe5,
0x1f, 0x00, 0x5e, 0xf9, 0xa4, 0x4a, 0x7c, 0x26, 0x5b, 0xba, 0x09, 0x67, 0xcd, 0x50, 0x74, 0xaa,
0xb6, 0xd3, 0xbc, 0x03, 0x4c, 0xd3, 0x38, 0xc5, 0x89, 0xb6, 0x7e, 0xed, 0xd1, 0xe3, 0xd2, 0xc4,
0x87, 0x8f, 0x4b, 0x13, 0x1f, 0x3d, 0x2e, 0x4d, 0xfc, 0xa8, 0x57, 0x02, 0x8f, 0x7a, 0x25, 0xf0,
0x61, 0xaf, 0x04, 0x3e, 0xea, 0x95, 0xc0, 0xbf, 0x7b, 0x25, 0xf0, 0xeb, 0xff, 0x94, 0x26, 0xbe,
0x37, 0x63, 0xc0, 0x3f, 0x0e, 0x00, 0x00, 0xff, 0xff, 0x35, 0x69, 0x71, 0xa6, 0xa3, 0x1e, 0x00,
0x00,
} }
...@@ -107,6 +107,12 @@ message CustomResourceDefinitionSpec { ...@@ -107,6 +107,12 @@ message CustomResourceDefinitionSpec {
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
// +optional // +optional
optional CustomResourceValidation validation = 5; optional CustomResourceValidation validation = 5;
// Subresources describes the subresources for CustomResources
// This field is alpha-level and should only be sent to servers that enable
// subresources via the CustomResourceSubresources feature gate.
// +optional
optional CustomResourceSubresources subresources = 6;
} }
// CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition // CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition
...@@ -119,6 +125,48 @@ message CustomResourceDefinitionStatus { ...@@ -119,6 +125,48 @@ message CustomResourceDefinitionStatus {
optional CustomResourceDefinitionNames acceptedNames = 2; optional CustomResourceDefinitionNames acceptedNames = 2;
} }
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
message CustomResourceSubresourceScale {
// SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .spec.
// If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.
optional string specReplicasPath = 1;
// StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource
// will default to 0.
optional string statusReplicasPath = 2;
// LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// Must be set to work with HPA.
// If there is no value under the given path in the CustomResource, the status label selector value in the /scale
// subresource will default to the empty string.
// +optional
optional string labelSelectorPath = 3;
}
// CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources.
// Status is represented by the `.status` JSON path inside of a CustomResource. When set,
// * exposes a /status subresource for the custom resource
// * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza
// * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza
message CustomResourceSubresourceStatus {
}
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
message CustomResourceSubresources {
// Status denotes the status subresource for CustomResources
optional CustomResourceSubresourceStatus status = 1;
// Scale denotes the scale subresource for CustomResources
optional CustomResourceSubresourceScale scale = 2;
}
// CustomResourceValidation is a list of validation methods for CustomResources. // CustomResourceValidation is a list of validation methods for CustomResources.
message CustomResourceValidation { message CustomResourceValidation {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against. // OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
......
...@@ -16,7 +16,9 @@ limitations under the License. ...@@ -16,7 +16,9 @@ limitations under the License.
package v1beta1 package v1beta1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// CustomResourceDefinitionSpec describes how a user wants their resource to appear // CustomResourceDefinitionSpec describes how a user wants their resource to appear
type CustomResourceDefinitionSpec struct { type CustomResourceDefinitionSpec struct {
...@@ -31,6 +33,11 @@ type CustomResourceDefinitionSpec struct { ...@@ -31,6 +33,11 @@ type CustomResourceDefinitionSpec struct {
// Validation describes the validation methods for CustomResources // Validation describes the validation methods for CustomResources
// +optional // +optional
Validation *CustomResourceValidation `json:"validation,omitempty" protobuf:"bytes,5,opt,name=validation"` Validation *CustomResourceValidation `json:"validation,omitempty" protobuf:"bytes,5,opt,name=validation"`
// Subresources describes the subresources for CustomResources
// This field is alpha-level and should only be sent to servers that enable
// subresources via the CustomResourceSubresources feature gate.
// +optional
Subresources *CustomResourceSubresources `json:"subresources,omitempty" protobuf:"bytes,6,opt,name=subresources"`
} }
// CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition // CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition
...@@ -147,3 +154,41 @@ type CustomResourceValidation struct { ...@@ -147,3 +154,41 @@ type CustomResourceValidation struct {
// OpenAPIV3Schema is the OpenAPI v3 schema to be validated against. // OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.
OpenAPIV3Schema *JSONSchemaProps `json:"openAPIV3Schema,omitempty" protobuf:"bytes,1,opt,name=openAPIV3Schema"` OpenAPIV3Schema *JSONSchemaProps `json:"openAPIV3Schema,omitempty" protobuf:"bytes,1,opt,name=openAPIV3Schema"`
} }
// CustomResourceSubresources defines the status and scale subresources for CustomResources.
type CustomResourceSubresources struct {
// Status denotes the status subresource for CustomResources
Status *CustomResourceSubresourceStatus `json:"status,omitempty" protobuf:"bytes,1,opt,name=status"`
// Scale denotes the scale subresource for CustomResources
Scale *CustomResourceSubresourceScale `json:"scale,omitempty" protobuf:"bytes,2,opt,name=scale"`
}
// CustomResourceSubresourceStatus defines how to serve the status subresource for CustomResources.
// Status is represented by the `.status` JSON path inside of a CustomResource. When set,
// * exposes a /status subresource for the custom resource
// * PUT requests to the /status subresource take a custom resource object, and ignore changes to anything except the status stanza
// * PUT/POST/PATCH requests to the custom resource ignore changes to the status stanza
type CustomResourceSubresourceStatus struct{}
// CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.
type CustomResourceSubresourceScale struct {
// SpecReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Spec.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .spec.
// If there is no value under the given path in the CustomResource, the /scale subresource will return an error on GET.
SpecReplicasPath string `json:"specReplicasPath" protobuf:"bytes,1,name=specReplicasPath"`
// StatusReplicasPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Replicas.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// If there is no value under the given path in the CustomResource, the status replica value in the /scale subresource
// will default to 0.
StatusReplicasPath string `json:"statusReplicasPath" protobuf:"bytes,2,opt,name=statusReplicasPath"`
// LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector.
// Only JSON paths without the array notation are allowed.
// Must be a JSON Path under .status.
// Must be set to work with HPA.
// If there is no value under the given path in the CustomResource, the status label selector value in the /scale
// subresource will default to the empty string.
// +optional
LabelSelectorPath *string `json:"labelSelectorPath,omitempty" protobuf:"bytes,3,opt,name=labelSelectorPath"`
}
...@@ -48,6 +48,12 @@ func RegisterConversions(scheme *runtime.Scheme) error { ...@@ -48,6 +48,12 @@ func RegisterConversions(scheme *runtime.Scheme) error {
Convert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomResourceDefinitionSpec, Convert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomResourceDefinitionSpec,
Convert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomResourceDefinitionStatus, Convert_v1beta1_CustomResourceDefinitionStatus_To_apiextensions_CustomResourceDefinitionStatus,
Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus, Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus,
Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale,
Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale,
Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus,
Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus,
Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources,
Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources,
Convert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation, Convert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation,
Convert_apiextensions_CustomResourceValidation_To_v1beta1_CustomResourceValidation, Convert_apiextensions_CustomResourceValidation_To_v1beta1_CustomResourceValidation,
Convert_v1beta1_ExternalDocumentation_To_apiextensions_ExternalDocumentation, Convert_v1beta1_ExternalDocumentation_To_apiextensions_ExternalDocumentation,
...@@ -211,6 +217,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes ...@@ -211,6 +217,7 @@ func autoConvert_v1beta1_CustomResourceDefinitionSpec_To_apiextensions_CustomRes
} else { } else {
out.Validation = nil out.Validation = nil
} }
out.Subresources = (*apiextensions.CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
return nil return nil
} }
...@@ -235,6 +242,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes ...@@ -235,6 +242,7 @@ func autoConvert_apiextensions_CustomResourceDefinitionSpec_To_v1beta1_CustomRes
} else { } else {
out.Validation = nil out.Validation = nil
} }
out.Subresources = (*CustomResourceSubresources)(unsafe.Pointer(in.Subresources))
return nil return nil
} }
...@@ -269,6 +277,70 @@ func Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResou ...@@ -269,6 +277,70 @@ func Convert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResou
return autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus(in, out, s) return autoConvert_apiextensions_CustomResourceDefinitionStatus_To_v1beta1_CustomResourceDefinitionStatus(in, out, s)
} }
func autoConvert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in *CustomResourceSubresourceScale, out *apiextensions.CustomResourceSubresourceScale, s conversion.Scope) error {
out.SpecReplicasPath = in.SpecReplicasPath
out.StatusReplicasPath = in.StatusReplicasPath
out.LabelSelectorPath = (*string)(unsafe.Pointer(in.LabelSelectorPath))
return nil
}
// Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in *CustomResourceSubresourceScale, out *apiextensions.CustomResourceSubresourceScale, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresourceScale_To_apiextensions_CustomResourceSubresourceScale(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in *apiextensions.CustomResourceSubresourceScale, out *CustomResourceSubresourceScale, s conversion.Scope) error {
out.SpecReplicasPath = in.SpecReplicasPath
out.StatusReplicasPath = in.StatusReplicasPath
out.LabelSelectorPath = (*string)(unsafe.Pointer(in.LabelSelectorPath))
return nil
}
// Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in *apiextensions.CustomResourceSubresourceScale, out *CustomResourceSubresourceScale, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresourceScale_To_v1beta1_CustomResourceSubresourceScale(in, out, s)
}
func autoConvert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in *CustomResourceSubresourceStatus, out *apiextensions.CustomResourceSubresourceStatus, s conversion.Scope) error {
return nil
}
// Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in *CustomResourceSubresourceStatus, out *apiextensions.CustomResourceSubresourceStatus, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresourceStatus_To_apiextensions_CustomResourceSubresourceStatus(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in *apiextensions.CustomResourceSubresourceStatus, out *CustomResourceSubresourceStatus, s conversion.Scope) error {
return nil
}
// Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in *apiextensions.CustomResourceSubresourceStatus, out *CustomResourceSubresourceStatus, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresourceStatus_To_v1beta1_CustomResourceSubresourceStatus(in, out, s)
}
func autoConvert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in *CustomResourceSubresources, out *apiextensions.CustomResourceSubresources, s conversion.Scope) error {
out.Status = (*apiextensions.CustomResourceSubresourceStatus)(unsafe.Pointer(in.Status))
out.Scale = (*apiextensions.CustomResourceSubresourceScale)(unsafe.Pointer(in.Scale))
return nil
}
// Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources is an autogenerated conversion function.
func Convert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in *CustomResourceSubresources, out *apiextensions.CustomResourceSubresources, s conversion.Scope) error {
return autoConvert_v1beta1_CustomResourceSubresources_To_apiextensions_CustomResourceSubresources(in, out, s)
}
func autoConvert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in *apiextensions.CustomResourceSubresources, out *CustomResourceSubresources, s conversion.Scope) error {
out.Status = (*CustomResourceSubresourceStatus)(unsafe.Pointer(in.Status))
out.Scale = (*CustomResourceSubresourceScale)(unsafe.Pointer(in.Scale))
return nil
}
// Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources is an autogenerated conversion function.
func Convert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in *apiextensions.CustomResourceSubresources, out *CustomResourceSubresources, s conversion.Scope) error {
return autoConvert_apiextensions_CustomResourceSubresources_To_v1beta1_CustomResourceSubresources(in, out, s)
}
func autoConvert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(in *CustomResourceValidation, out *apiextensions.CustomResourceValidation, s conversion.Scope) error { func autoConvert_v1beta1_CustomResourceValidation_To_apiextensions_CustomResourceValidation(in *CustomResourceValidation, out *apiextensions.CustomResourceValidation, s conversion.Scope) error {
if in.OpenAPIV3Schema != nil { if in.OpenAPIV3Schema != nil {
in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema in, out := &in.OpenAPIV3Schema, &out.OpenAPIV3Schema
......
...@@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
if in.Subresources != nil {
in, out := &in.Subresources, &out.Subresources
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresources)
(*in).DeepCopyInto(*out)
}
}
return return
} }
...@@ -176,6 +185,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt ...@@ -176,6 +185,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) {
*out = *in
if in.LabelSelectorPath != nil {
in, out := &in.LabelSelectorPath, &out.LabelSelectorPath
if *in == nil {
*out = nil
} else {
*out = new(string)
**out = **in
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceScale.
func (in *CustomResourceSubresourceScale) DeepCopy() *CustomResourceSubresourceScale {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceScale)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceStatus) DeepCopyInto(out *CustomResourceSubresourceStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceStatus.
func (in *CustomResourceSubresourceStatus) DeepCopy() *CustomResourceSubresourceStatus {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresources) DeepCopyInto(out *CustomResourceSubresources) {
*out = *in
if in.Status != nil {
in, out := &in.Status, &out.Status
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceStatus)
**out = **in
}
}
if in.Scale != nil {
in, out := &in.Scale, &out.Scale
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceScale)
(*in).DeepCopyInto(*out)
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresources.
func (in *CustomResourceSubresources) DeepCopy() *CustomResourceSubresources {
if in == nil {
return nil
}
out := new(CustomResourceSubresources)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) { func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) {
*out = *in *out = *in
if in.OpenAPIV3Schema != nil { if in.OpenAPIV3Schema != nil {
......
...@@ -18,6 +18,7 @@ package validation ...@@ -18,6 +18,7 @@ package validation
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
genericvalidation "k8s.io/apimachinery/pkg/api/validation" genericvalidation "k8s.io/apimachinery/pkg/api/validation"
...@@ -107,7 +108,13 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi ...@@ -107,7 +108,13 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, fldPath.Child("validation"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, fldPath.Child("validation"))...)
} else if spec.Validation != nil { } else if spec.Validation != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate")) allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate CustomResourceValidation"))
}
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
allErrs = append(allErrs, ValidateCustomResourceDefinitionSubresources(spec.Subresources, fldPath.Child("subresources"))...)
} else if spec.Subresources != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("subresources"), "disabled by feature-gate CustomResourceSubresources"))
} }
return allErrs return allErrs
...@@ -182,9 +189,27 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext ...@@ -182,9 +189,27 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
return allErrs return allErrs
} }
if customResourceValidation.OpenAPIV3Schema != nil { if schema := customResourceValidation.OpenAPIV3Schema; schema != nil {
// if subresources are enabled, only properties is allowed inside the root schema
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
v := reflect.ValueOf(schema).Elem()
fieldsPresent := 0
for i := 0; i < v.NumField(); i++ {
field := v.Field(i).Interface()
if !reflect.DeepEqual(field, reflect.Zero(reflect.TypeOf(field)).Interface()) {
fieldsPresent++
}
}
if fieldsPresent > 1 || (fieldsPresent == 1 && v.FieldByName("Properties").IsNil()) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), *schema, fmt.Sprintf("if subresources for custom resources are enabled, only properties can be used at the root of the schema")))
return allErrs
}
}
openAPIV3Schema := &specStandardValidatorV3{} openAPIV3Schema := &specStandardValidatorV3{}
allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(customResourceValidation.OpenAPIV3Schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...) allErrs = append(allErrs, ValidateCustomResourceDefinitionOpenAPISchema(schema, fldPath.Child("openAPIV3Schema"), openAPIV3Schema)...)
} }
// if validation passed otherwise, make sure we can actually construct a schema validator from this custom resource validation. // if validation passed otherwise, make sure we can actually construct a schema validator from this custom resource validation.
...@@ -326,3 +351,64 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps ...@@ -326,3 +351,64 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps
return allErrs return allErrs
} }
// ValidateCustomResourceDefinitionSubresources statically validates
func ValidateCustomResourceDefinitionSubresources(subresources *apiextensions.CustomResourceSubresources, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if subresources == nil {
return allErrs
}
if subresources.Scale != nil {
if len(subresources.Scale.SpecReplicasPath) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("scale.specReplicasPath"), ""))
} else {
// should be constrained json path under .spec
if errs := validateSimpleJSONPath(subresources.Scale.SpecReplicasPath, fldPath.Child("scale.specReplicasPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(subresources.Scale.SpecReplicasPath, ".spec.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.specReplicasPath"), subresources.Scale.SpecReplicasPath, "should be a json path under .spec"))
}
}
if len(subresources.Scale.StatusReplicasPath) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("scale.statusReplicasPath"), ""))
} else {
// should be constrained json path under .status
if errs := validateSimpleJSONPath(subresources.Scale.StatusReplicasPath, fldPath.Child("scale.statusReplicasPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(subresources.Scale.StatusReplicasPath, ".status.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.statusReplicasPath"), subresources.Scale.StatusReplicasPath, "should be a json path under .status"))
}
}
// if labelSelectorPath is present, it should be a constrained json path under .status
if subresources.Scale.LabelSelectorPath != nil && len(*subresources.Scale.LabelSelectorPath) > 0 {
if errs := validateSimpleJSONPath(*subresources.Scale.LabelSelectorPath, fldPath.Child("scale.labelSelectorPath")); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if !strings.HasPrefix(*subresources.Scale.LabelSelectorPath, ".status.") {
allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.labelSelectorPath"), subresources.Scale.LabelSelectorPath, "should be a json path under .status"))
}
}
}
return allErrs
}
func validateSimpleJSONPath(s string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
switch {
case len(s) == 0:
allErrs = append(allErrs, field.Invalid(fldPath, s, "must not be empty"))
case s[0] != '.':
allErrs = append(allErrs, field.Invalid(fldPath, s, "must be a simple json path starting with ."))
case s != ".":
if cs := strings.Split(s[1:], "."); len(cs) < 1 {
allErrs = append(allErrs, field.Invalid(fldPath, s, "must be a json path in the dot notation"))
}
}
return allErrs
}
...@@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti ...@@ -138,6 +138,15 @@ func (in *CustomResourceDefinitionSpec) DeepCopyInto(out *CustomResourceDefiniti
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
} }
if in.Subresources != nil {
in, out := &in.Subresources, &out.Subresources
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresources)
(*in).DeepCopyInto(*out)
}
}
return return
} }
...@@ -176,6 +185,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt ...@@ -176,6 +185,81 @@ func (in *CustomResourceDefinitionStatus) DeepCopy() *CustomResourceDefinitionSt
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceScale) DeepCopyInto(out *CustomResourceSubresourceScale) {
*out = *in
if in.LabelSelectorPath != nil {
in, out := &in.LabelSelectorPath, &out.LabelSelectorPath
if *in == nil {
*out = nil
} else {
*out = new(string)
**out = **in
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceScale.
func (in *CustomResourceSubresourceScale) DeepCopy() *CustomResourceSubresourceScale {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceScale)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresourceStatus) DeepCopyInto(out *CustomResourceSubresourceStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresourceStatus.
func (in *CustomResourceSubresourceStatus) DeepCopy() *CustomResourceSubresourceStatus {
if in == nil {
return nil
}
out := new(CustomResourceSubresourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceSubresources) DeepCopyInto(out *CustomResourceSubresources) {
*out = *in
if in.Status != nil {
in, out := &in.Status, &out.Status
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceStatus)
**out = **in
}
}
if in.Scale != nil {
in, out := &in.Scale, &out.Scale
if *in == nil {
*out = nil
} else {
*out = new(CustomResourceSubresourceScale)
(*in).DeepCopyInto(*out)
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CustomResourceSubresources.
func (in *CustomResourceSubresources) DeepCopy() *CustomResourceSubresources {
if in == nil {
return nil
}
out := new(CustomResourceSubresources)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) { func (in *CustomResourceValidation) DeepCopyInto(out *CustomResourceValidation) {
*out = *in *out = *in
if in.OpenAPIV3Schema != nil { if in.OpenAPIV3Schema != nil {
......
...@@ -16,7 +16,11 @@ go_library( ...@@ -16,7 +16,11 @@ go_library(
], ],
importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver", importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver",
deps = [ deps = [
"//vendor/github.com/go-openapi/spec:go_default_library",
"//vendor/github.com/go-openapi/strfmt:go_default_library",
"//vendor/github.com/go-openapi/validate:go_default_library",
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/install:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
...@@ -29,6 +33,7 @@ go_library( ...@@ -29,6 +33,7 @@ go_library(
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/controller/status:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/controller/status:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresourcedefinition:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
...@@ -60,7 +65,10 @@ go_library( ...@@ -60,7 +65,10 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/server:go_default_library", "//vendor/k8s.io/apiserver/pkg/server:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/storage:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/storage:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/storagebackend:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library", "//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/scale:go_default_library",
"//vendor/k8s.io/client-go/scale/scheme/autoscalingv1:go_default_library",
"//vendor/k8s.io/client-go/tools/cache:go_default_library", "//vendor/k8s.io/client-go/tools/cache:go_default_library",
"//vendor/k8s.io/client-go/util/workqueue:go_default_library", "//vendor/k8s.io/client-go/util/workqueue:go_default_library",
], ],
......
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
autoscaling "k8s.io/api/autoscaling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
...@@ -117,6 +118,26 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error { ...@@ -117,6 +118,26 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
Verbs: verbs, Verbs: verbs,
ShortNames: crd.Status.AcceptedNames.ShortNames, ShortNames: crd.Status.AcceptedNames.ShortNames,
}) })
if crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Name: crd.Status.AcceptedNames.Plural + "/status",
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
Kind: crd.Status.AcceptedNames.Kind,
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
})
}
if crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
apiResourcesForDiscovery = append(apiResourcesForDiscovery, metav1.APIResource{
Group: autoscaling.GroupName,
Version: "v1",
Kind: "Scale",
Name: crd.Status.AcceptedNames.Plural + "/scale",
Namespaced: crd.Spec.Scope == apiextensions.NamespaceScoped,
Verbs: metav1.Verbs([]string{"get", "patch", "update"}),
})
}
} }
if !foundGroup { if !foundGroup {
......
...@@ -25,6 +25,9 @@ import ( ...@@ -25,6 +25,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/go-openapi/spec"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/validate"
"github.com/golang/glog" "github.com/golang/glog"
apiequality "k8s.io/apimachinery/pkg/api/equality" apiequality "k8s.io/apimachinery/pkg/api/equality"
...@@ -35,6 +38,7 @@ import ( ...@@ -35,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/runtime/serializer/versioning" "k8s.io/apimachinery/pkg/runtime/serializer/versioning"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
...@@ -47,7 +51,10 @@ import ( ...@@ -47,7 +51,10 @@ import (
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/storage/storagebackend" "k8s.io/apiserver/pkg/storage/storagebackend"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/discovery" "k8s.io/client-go/discovery"
"k8s.io/client-go/scale"
"k8s.io/client-go/scale/scheme/autoscalingv1"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
...@@ -55,6 +62,7 @@ import ( ...@@ -55,6 +62,7 @@ import (
informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion" informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion"
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion" listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer" "k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource" "k8s.io/apiextensions-apiserver/pkg/registry/customresource"
) )
...@@ -87,8 +95,11 @@ type crdInfo struct { ...@@ -87,8 +95,11 @@ type crdInfo struct {
spec *apiextensions.CustomResourceDefinitionSpec spec *apiextensions.CustomResourceDefinitionSpec
acceptedNames *apiextensions.CustomResourceDefinitionNames acceptedNames *apiextensions.CustomResourceDefinitionNames
storage *customresource.REST storage customresource.CustomResourceStorage
requestScope handlers.RequestScope
requestScope handlers.RequestScope
scaleRequestScope handlers.RequestScope
statusRequestScope handlers.RequestScope
} }
// crdStorageMap goes from customresourcedefinition to its storage // crdStorageMap goes from customresourcedefinition to its storage
...@@ -172,10 +183,6 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -172,10 +183,6 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.delegate.ServeHTTP(w, req) r.delegate.ServeHTTP(w, req)
return return
} }
if len(requestInfo.Subresource) > 0 {
http.NotFound(w, req)
return
}
terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating) terminating := apiextensions.IsCRDConditionTrue(crd, apiextensions.Terminating)
...@@ -185,61 +192,126 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -185,61 +192,126 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return return
} }
storage := crdInfo.storage
requestScope := crdInfo.requestScope
minRequestTimeout := 1 * time.Minute
verb := strings.ToUpper(requestInfo.Verb) verb := strings.ToUpper(requestInfo.Verb)
resource := requestInfo.Resource resource := requestInfo.Resource
subresource := requestInfo.Subresource subresource := requestInfo.Subresource
scope := metrics.CleanScope(requestInfo) scope := metrics.CleanScope(requestInfo)
supportedTypes := []string{
string(types.JSONPatchType),
string(types.MergePatchType),
}
var handler http.HandlerFunc var handler http.HandlerFunc
switch {
case subresource == "status" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil:
handler = r.serveStatus(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case subresource == "scale" && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil:
handler = r.serveScale(w, req, requestInfo, crdInfo, terminating, supportedTypes)
case len(subresource) == 0:
handler = r.serveResource(w, req, requestInfo, crdInfo, terminating, supportedTypes)
default:
http.Error(w, "the server could not find the requested resource", http.StatusNotFound)
}
if handler != nil {
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
handler(w, req)
return
}
}
func (r *crdHandler) serveResource(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.requestScope
storage := crdInfo.storage.CustomResource
minRequestTimeout := 1 * time.Minute
switch requestInfo.Verb { switch requestInfo.Verb {
case "get": case "get":
handler = handlers.GetResource(storage, storage, requestScope) return handlers.GetResource(storage, storage, requestScope)
case "list": case "list":
forceWatch := false forceWatch := false
handler = handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout) return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
case "watch": case "watch":
forceWatch := true forceWatch := true
handler = handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout) return handlers.ListResource(storage, storage, requestScope, forceWatch, minRequestTimeout)
case "create": case "create":
if terminating { if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return return nil
} }
handler = handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission) return handlers.CreateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "update": case "update":
if terminating { if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return return nil
} }
handler = handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission) return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch": case "patch":
if terminating { if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed) http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return return nil
}
supportedTypes := []string{
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": case "delete":
allowsOptions := true allowsOptions := true
handler = handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission) return handlers.DeleteResource(storage, allowsOptions, requestScope, r.admission)
case "deletecollection": case "deletecollection":
checkBody := true checkBody := true
handler = handlers.DeleteCollection(storage, checkBody, requestScope, r.admission) return handlers.DeleteCollection(storage, checkBody, requestScope, r.admission)
default: default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed) http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return return nil
}
}
func (r *crdHandler) serveStatus(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.statusRequestScope
storage := crdInfo.storage.Status
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, nil, requestScope)
case "update":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
}
func (r *crdHandler) serveScale(w http.ResponseWriter, req *http.Request, requestInfo *apirequest.RequestInfo, crdInfo *crdInfo, terminating bool, supportedTypes []string) http.HandlerFunc {
requestScope := crdInfo.scaleRequestScope
storage := crdInfo.storage.Scale
switch requestInfo.Verb {
case "get":
return handlers.GetResource(storage, nil, requestScope)
case "update":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.UpdateResource(storage, requestScope, discovery.NewUnstructuredObjectTyper(nil), r.admission)
case "patch":
if terminating {
http.Error(w, fmt.Sprintf("%v not allowed while CustomResourceDefinition is terminating", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
}
return handlers.PatchResource(storage, requestScope, r.admission, unstructured.UnstructuredObjectConverter{}, supportedTypes)
default:
http.Error(w, fmt.Sprintf("unhandled verb %q", requestInfo.Verb), http.StatusMethodNotAllowed)
return nil
} }
handler = metrics.InstrumentHandlerFunc(verb, resource, subresource, scope, handler)
handler(w, req)
return
} }
func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) { func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) {
...@@ -265,7 +337,8 @@ func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{}) ...@@ -265,7 +337,8 @@ func (r *crdHandler) updateCustomResourceDefinition(oldObj, newObj interface{})
// as it is used without locking elsewhere. // as it is used without locking elsewhere.
storageMap2 := storageMap.clone() storageMap2 := storageMap.clone()
if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok { if oldInfo, ok := storageMap2[types.UID(oldCRD.UID)]; ok {
oldInfo.storage.DestroyFunc() // destroy only the main storage. Those for the subresources share cacher and etcd clients.
oldInfo.storage.CustomResource.DestroyFunc()
delete(storageMap2, types.UID(oldCRD.UID)) delete(storageMap2, types.UID(oldCRD.UID))
} }
...@@ -297,7 +370,8 @@ func (r *crdHandler) removeDeadStorage() { ...@@ -297,7 +370,8 @@ func (r *crdHandler) removeDeadStorage() {
} }
if !found { if !found {
glog.V(4).Infof("Removing dead CRD storage for %v", s.requestScope.Resource) glog.V(4).Infof("Removing dead CRD storage for %v", s.requestScope.Resource)
s.storage.DestroyFunc() // destroy only the main storage. Those for the subresources share cacher and etcd clients.
s.storage.CustomResource.DestroyFunc()
delete(storageMap2, uid) delete(storageMap2, uid)
} }
} }
...@@ -311,7 +385,7 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions ...@@ -311,7 +385,7 @@ func (r *crdHandler) GetCustomResourceListerCollectionDeleter(crd *apiextensions
if err != nil { if err != nil {
return nil, err return nil, err
} }
return info.storage, nil return info.storage.CustomResource, nil
} }
func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) { func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResourceDefinition) (*crdInfo, error) {
...@@ -340,9 +414,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -340,9 +414,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
parameterCodec := runtime.NewParameterCodec(parameterScheme) parameterCodec := runtime.NewParameterCodec(parameterScheme)
kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.Kind} kind := schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.Kind}
typer := unstructuredObjectTyper{ typer := UnstructuredObjectTyper{
delegate: parameterScheme, Delegate: parameterScheme,
unstructuredTyper: discovery.NewUnstructuredObjectTyper(nil), UnstructuredTyper: discovery.NewUnstructuredObjectTyper(nil),
} }
creator := unstructuredCreator{} creator := unstructuredCreator{}
...@@ -351,7 +425,29 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -351,7 +425,29 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
return nil, err return nil, err
} }
storage := customresource.NewREST( var statusSpec *apiextensions.CustomResourceSubresourceStatus
var statusValidator *validate.SchemaValidator
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Status != nil {
statusSpec = crd.Spec.Subresources.Status
// for the status subresource, validate only against the status schema
if crd.Spec.Validation != nil && crd.Spec.Validation.OpenAPIV3Schema != nil && crd.Spec.Validation.OpenAPIV3Schema.Properties != nil {
if statusSchema, ok := crd.Spec.Validation.OpenAPIV3Schema.Properties["status"]; ok {
openapiSchema := &spec.Schema{}
if err := apiservervalidation.ConvertJSONSchemaProps(&statusSchema, openapiSchema); err != nil {
return nil, err
}
statusValidator = validate.NewSchemaValidator(openapiSchema, nil, "", strfmt.Default)
}
}
}
var scaleSpec *apiextensions.CustomResourceSubresourceScale
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && crd.Spec.Subresources != nil && crd.Spec.Subresources.Scale != nil {
scaleSpec = crd.Spec.Subresources.Scale
}
customResourceStorage := customresource.NewStorage(
schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural}, schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Status.AcceptedNames.Plural},
schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.ListKind}, schema.GroupVersionKind{Group: crd.Spec.Group, Version: crd.Spec.Version, Kind: crd.Status.AcceptedNames.ListKind},
customresource.NewStrategy( customresource.NewStrategy(
...@@ -359,6 +455,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -359,6 +455,9 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
crd.Spec.Scope == apiextensions.NamespaceScoped, crd.Spec.Scope == apiextensions.NamespaceScoped,
kind, kind,
validator, validator,
statusValidator,
statusSpec,
scaleSpec,
), ),
r.restOptionsGetter, r.restOptionsGetter,
) )
...@@ -373,12 +472,15 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -373,12 +472,15 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped clusterScoped := crd.Spec.Scope == apiextensions.ClusterScoped
var ctxFn handlers.ContextFunc
ctxFn = func(req *http.Request) apirequest.Context {
ret, _ := r.requestContextMapper.Get(req)
return ret
}
requestScope := handlers.RequestScope{ requestScope := handlers.RequestScope{
Namer: handlers.ContextBasedNaming{ Namer: handlers.ContextBasedNaming{
GetContext: func(req *http.Request) apirequest.Context { GetContext: ctxFn,
ret, _ := r.requestContextMapper.Get(req)
return ret
},
SelfLinker: meta.NewAccessor(), SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped, ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix, SelfLinkPathPrefix: selfLinkPrefix,
...@@ -400,9 +502,8 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -400,9 +502,8 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
Typer: typer, Typer: typer,
UnsafeConvertor: unstructured.UnstructuredObjectConverter{}, UnsafeConvertor: unstructured.UnstructuredObjectConverter{},
Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural}, Resource: schema.GroupVersionResource{Group: crd.Spec.Group, Version: crd.Spec.Version, Resource: crd.Status.AcceptedNames.Plural},
Kind: kind, Kind: kind,
Subresource: "",
MetaGroupVersion: metav1.SchemeGroupVersion, MetaGroupVersion: metav1.SchemeGroupVersion,
} }
...@@ -411,8 +512,33 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource ...@@ -411,8 +512,33 @@ func (r *crdHandler) getOrCreateServingInfoFor(crd *apiextensions.CustomResource
spec: &crd.Spec, spec: &crd.Spec,
acceptedNames: &crd.Status.AcceptedNames, acceptedNames: &crd.Status.AcceptedNames,
storage: storage, storage: customResourceStorage,
requestScope: requestScope, requestScope: requestScope,
scaleRequestScope: requestScope, // shallow copy
statusRequestScope: requestScope, // shallow copy
}
// override scaleSpec subresource values
scaleConverter := scale.NewScaleConverter()
ret.scaleRequestScope.Subresource = "scale"
ret.scaleRequestScope.Serializer = serializer.NewCodecFactory(scaleConverter.Scheme())
ret.scaleRequestScope.Kind = autoscalingv1.SchemeGroupVersion.WithKind("Scale")
ret.scaleRequestScope.Namer = handlers.ContextBasedNaming{
GetContext: ctxFn,
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
SelfLinkPathSuffix: "/scale",
}
// override status subresource values
ret.statusRequestScope.Subresource = "status"
ret.statusRequestScope.Namer = handlers.ContextBasedNaming{
GetContext: ctxFn,
SelfLinker: meta.NewAccessor(),
ClusterScoped: clusterScoped,
SelfLinkPathPrefix: selfLinkPrefix,
SelfLinkPathSuffix: "/status",
} }
// Copy because we cannot write to storageMap without a race // Copy because we cannot write to storageMap without a race
...@@ -477,21 +603,21 @@ func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decod ...@@ -477,21 +603,21 @@ func (s unstructuredNegotiatedSerializer) DecoderToVersion(decoder runtime.Decod
return versioning.NewDefaultingCodecForScheme(Scheme, nil, decoder, nil, gv) return versioning.NewDefaultingCodecForScheme(Scheme, nil, decoder, nil, gv)
} }
type unstructuredObjectTyper struct { type UnstructuredObjectTyper struct {
delegate runtime.ObjectTyper Delegate runtime.ObjectTyper
unstructuredTyper runtime.ObjectTyper UnstructuredTyper runtime.ObjectTyper
} }
func (t unstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) { func (t UnstructuredObjectTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) {
// Delegate for things other than Unstructured. // Delegate for things other than Unstructured.
if _, ok := obj.(runtime.Unstructured); !ok { if _, ok := obj.(runtime.Unstructured); !ok {
return t.delegate.ObjectKinds(obj) return t.Delegate.ObjectKinds(obj)
} }
return t.unstructuredTyper.ObjectKinds(obj) return t.UnstructuredTyper.ObjectKinds(obj)
} }
func (t unstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool { func (t UnstructuredObjectTyper) Recognizes(gvk schema.GroupVersionKind) bool {
return t.delegate.Recognizes(gvk) || t.unstructuredTyper.Recognizes(gvk) return t.Delegate.Recognizes(gvk) || t.UnstructuredTyper.Recognizes(gvk)
} }
type unstructuredCreator struct{} type unstructuredCreator struct{}
......
...@@ -29,7 +29,7 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa ...@@ -29,7 +29,7 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
// Convert CRD schema to openapi schema // Convert CRD schema to openapi schema
openapiSchema := &spec.Schema{} openapiSchema := &spec.Schema{}
if customResourceValidation != nil { if customResourceValidation != nil {
if err := convertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil { if err := ConvertJSONSchemaProps(customResourceValidation.OpenAPIV3Schema, openapiSchema); err != nil {
return nil, err return nil, err
} }
} }
...@@ -39,6 +39,10 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa ...@@ -39,6 +39,10 @@ func NewSchemaValidator(customResourceValidation *apiextensions.CustomResourceVa
// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition. // ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
// CustomResource is a JSON data structure. // CustomResource is a JSON data structure.
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error { func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
if validator == nil {
return nil
}
result := validator.Validate(customResource) result := validator.Validate(customResource)
if result.AsError() != nil { if result.AsError() != nil {
return result.AsError() return result.AsError()
...@@ -46,7 +50,8 @@ func ValidateCustomResource(customResource interface{}, validator *validate.Sche ...@@ -46,7 +50,8 @@ func ValidateCustomResource(customResource interface{}, validator *validate.Sche
return nil return nil
} }
func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error { // ConvertJSONSchemaProps converts the schema from apiextensions.JSONSchemaPropos to go-openapi/spec.Schema
func ConvertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
if in == nil { if in == nil {
return nil return nil
} }
...@@ -99,7 +104,7 @@ func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) ...@@ -99,7 +104,7 @@ func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema)
if in.Not != nil { if in.Not != nil {
in, out := &in.Not, &out.Not in, out := &in.Not, &out.Not
*out = new(spec.Schema) *out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil { if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err return err
} }
} }
...@@ -176,7 +181,7 @@ func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]s ...@@ -176,7 +181,7 @@ func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]s
if in != nil { if in != nil {
for _, jsonSchemaProps := range *in { for _, jsonSchemaProps := range *in {
schema := spec.Schema{} schema := spec.Schema{}
if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil { if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return err return err
} }
*out = append(*out, schema) *out = append(*out, schema)
...@@ -190,7 +195,7 @@ func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (m ...@@ -190,7 +195,7 @@ func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (m
if len(in) != 0 { if len(in) != 0 {
for k, jsonSchemaProps := range in { for k, jsonSchemaProps := range in {
schema := spec.Schema{} schema := spec.Schema{}
if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil { if err := ConvertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
return nil, err return nil, err
} }
out[k] = schema out[k] = schema
...@@ -203,7 +208,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out ...@@ -203,7 +208,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
if in.Schema != nil { if in.Schema != nil {
in, out := &in.Schema, &out.Schema in, out := &in.Schema, &out.Schema
*out = new(spec.Schema) *out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil { if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err return err
} }
} }
...@@ -211,7 +216,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out ...@@ -211,7 +216,7 @@ func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out
in, out := &in.JSONSchemas, &out.Schemas in, out := &in.JSONSchemas, &out.Schemas
*out = make([]spec.Schema, len(*in)) *out = make([]spec.Schema, len(*in))
for i := range *in { for i := range *in {
if err := convertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil { if err := ConvertJSONSchemaProps(&(*in)[i], &(*out)[i]); err != nil {
return err return err
} }
} }
...@@ -224,7 +229,7 @@ func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out * ...@@ -224,7 +229,7 @@ func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *
if in.Schema != nil { if in.Schema != nil {
in, out := &in.Schema, &out.Schema in, out := &in.Schema, &out.Schema
*out = new(spec.Schema) *out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil { if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err return err
} }
} }
...@@ -236,7 +241,7 @@ func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStri ...@@ -236,7 +241,7 @@ func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStri
if in.Schema != nil { if in.Schema != nil {
in, out := &in.Schema, &out.Schema in, out := &in.Schema, &out.Schema
*out = new(spec.Schema) *out = new(spec.Schema)
if err := convertJSONSchemaProps(*in, *out); err != nil { if err := ConvertJSONSchemaProps(*in, *out); err != nil {
return err return err
} }
} }
......
...@@ -58,7 +58,7 @@ func TestRoundTrip(t *testing.T) { ...@@ -58,7 +58,7 @@ func TestRoundTrip(t *testing.T) {
// internal -> go-openapi // internal -> go-openapi
openAPITypes := &spec.Schema{} openAPITypes := &spec.Schema{}
if err := convertJSONSchemaProps(internal, openAPITypes); err != nil { if err := ConvertJSONSchemaProps(internal, openAPITypes); err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -33,6 +33,12 @@ const ( ...@@ -33,6 +33,12 @@ const (
// //
// CustomResourceValidation is a list of validation methods for CustomResources // CustomResourceValidation is a list of validation methods for CustomResources
CustomResourceValidation utilfeature.Feature = "CustomResourceValidation" CustomResourceValidation utilfeature.Feature = "CustomResourceValidation"
// owner: @sttts, @nikhita
// alpha: v1.10
//
// CustomResourceSubresources defines the subresources for CustomResources
CustomResourceSubresources utilfeature.Feature = "CustomResourceSubresources"
) )
func init() { func init() {
...@@ -43,5 +49,6 @@ func init() { ...@@ -43,5 +49,6 @@ func init() {
// To add a new feature, define a key for it above and add it here. The features will be // To add a new feature, define a key for it above and add it here. The features will be
// available throughout Kubernetes binaries. // available throughout Kubernetes binaries.
var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureSpec{
CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta}, CustomResourceValidation: {Default: true, PreRelease: utilfeature.Beta},
CustomResourceSubresources: {Default: false, PreRelease: utilfeature.Alpha},
} }
...@@ -3,20 +3,30 @@ package(default_visibility = ["//visibility:public"]) ...@@ -3,20 +3,30 @@ package(default_visibility = ["//visibility:public"])
load( load(
"@io_bazel_rules_go//go:def.bzl", "@io_bazel_rules_go//go:def.bzl",
"go_library", "go_library",
"go_test",
) )
go_library( go_library(
name = "go_default_library", name = "go_default_library",
srcs = [ srcs = [
"etcd.go", "etcd.go",
"registry.go",
"status_strategy.go",
"strategy.go", "strategy.go",
"validator.go",
], ],
importpath = "k8s.io/apiextensions-apiserver/pkg/registry/customresource", importpath = "k8s.io/apiextensions-apiserver/pkg/registry/customresource",
deps = [ deps = [
"//vendor/github.com/go-openapi/validate:go_default_library", "//vendor/github.com/go-openapi/validate:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/validation:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/internalversion:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/fields:go_default_library", "//vendor/k8s.io/apimachinery/pkg/fields:go_default_library",
...@@ -24,11 +34,14 @@ go_library( ...@@ -24,11 +34,14 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
], ],
) )
...@@ -44,3 +57,27 @@ filegroup( ...@@ -44,3 +57,27 @@ filegroup(
srcs = [":package-srcs"], srcs = [":package-srcs"],
tags = ["automanaged"], tags = ["automanaged"],
) )
go_test(
name = "go_default_xtest",
srcs = ["etcd_test.go"],
deps = [
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/registry/customresource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/generic/testing:go_default_library",
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/etcd/testing:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
],
)
...@@ -17,20 +17,64 @@ limitations under the License. ...@@ -17,20 +17,64 @@ limitations under the License.
package customresource package customresource
import ( import (
"fmt"
"strings"
autoscalingv1 "k8s.io/api/autoscaling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
) )
// CustomResourceStorage includes dummy storage for CustomResources, and their Status and Scale subresources.
type CustomResourceStorage struct {
CustomResource *REST
Status *StatusREST
Scale *ScaleREST
}
func NewStorage(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter) CustomResourceStorage {
customResourceREST, customResourceStatusREST := newREST(resource, listKind, strategy, optsGetter)
customResourceRegistry := NewRegistry(customResourceREST)
s := CustomResourceStorage{
CustomResource: customResourceREST,
}
if strategy.status != nil {
s.Status = customResourceStatusREST
}
if scale := strategy.scale; scale != nil {
var labelSelectorPath string
if scale.LabelSelectorPath != nil {
labelSelectorPath = *scale.LabelSelectorPath
}
s.Scale = &ScaleREST{
registry: customResourceRegistry,
specReplicasPath: scale.SpecReplicasPath,
statusReplicasPath: scale.StatusReplicasPath,
labelSelectorPath: labelSelectorPath,
}
}
return s
}
// REST implements a RESTStorage for API services against etcd // REST implements a RESTStorage for API services against etcd
type REST struct { type REST struct {
*genericregistry.Store *genericregistry.Store
} }
// NewREST returns a RESTStorage object that will work against API services. // newREST returns a RESTStorage object that will work against API services.
func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceDefinitionStorageStrategy, optsGetter generic.RESTOptionsGetter) *REST { func newREST(resource schema.GroupResource, listKind schema.GroupVersionKind, strategy customResourceStrategy, optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST) {
store := &genericregistry.Store{ store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &unstructured.Unstructured{} }, NewFunc: func() runtime.Object { return &unstructured.Unstructured{} },
NewListFunc: func() runtime.Object { NewListFunc: func() runtime.Object {
...@@ -50,5 +94,161 @@ func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, st ...@@ -50,5 +94,161 @@ func NewREST(resource schema.GroupResource, listKind schema.GroupVersionKind, st
if err := store.CompleteWithOptions(options); err != nil { if err := store.CompleteWithOptions(options); err != nil {
panic(err) // TODO: Propagate error up panic(err) // TODO: Propagate error up
} }
return &REST{store}
statusStore := *store
statusStore.UpdateStrategy = NewStatusStrategy(strategy)
return &REST{store}, &StatusREST{store: &statusStore}
}
// StatusREST implements the REST endpoint for changing the status of a CustomResource
type StatusREST struct {
store *genericregistry.Store
}
func (r *StatusREST) New() runtime.Object {
return &unstructured.Unstructured{}
}
// Get retrieves the object from the storage. It is required to support Patch.
func (r *StatusREST) Get(ctx genericapirequest.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return r.store.Get(ctx, name, options)
}
// Update alters the status subset of an object.
func (r *StatusREST) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) {
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation)
}
type ScaleREST struct {
registry Registry
specReplicasPath string
statusReplicasPath string
labelSelectorPath string
}
// ScaleREST implements Patcher
var _ = rest.Patcher(&ScaleREST{})
var _ = rest.GroupVersionKindProvider(&ScaleREST{})
func (r *ScaleREST) GroupVersionKind(containingGV schema.GroupVersion) schema.GroupVersionKind {
return autoscalingv1.SchemeGroupVersion.WithKind("Scale")
}
// New creates a new Scale object
func (r *ScaleREST) New() runtime.Object {
return &autoscalingv1.Scale{}
}
func (r *ScaleREST) Get(ctx genericapirequest.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
cr, err := r.registry.GetCustomResource(ctx, name, options)
if err != nil {
return nil, err
}
scaleObject, replicasFound, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, err
}
if !replicasFound {
return nil, apierrors.NewInternalError(fmt.Errorf("the spec replicas field %q does not exist", r.specReplicasPath))
}
return scaleObject, err
}
func (r *ScaleREST) Update(ctx genericapirequest.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (runtime.Object, bool, error) {
cr, err := r.registry.GetCustomResource(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, false, err
}
const invalidSpecReplicas = -2147483648 // smallest int32
oldScale, replicasFound, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, false, err
}
if !replicasFound {
oldScale.Spec.Replicas = invalidSpecReplicas // signal that this was not set before
}
obj, err := objInfo.UpdatedObject(ctx, oldScale)
if err != nil {
return nil, false, err
}
if obj == nil {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("nil update passed to Scale"))
}
scale, ok := obj.(*autoscalingv1.Scale)
if !ok {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("wrong object passed to Scale update: %v", obj))
}
if scale.Spec.Replicas == invalidSpecReplicas {
return nil, false, apierrors.NewBadRequest(fmt.Sprintf("the spec replicas field %q cannot be empty", r.specReplicasPath))
}
specReplicasPath := strings.TrimPrefix(r.specReplicasPath, ".") // ignore leading period
if err = unstructured.SetNestedField(cr.Object, int64(scale.Spec.Replicas), strings.Split(specReplicasPath, ".")...); err != nil {
return nil, false, err
}
cr.SetResourceVersion(scale.ResourceVersion)
cr, err = r.registry.UpdateCustomResource(ctx, cr, createValidation, updateValidation)
if err != nil {
return nil, false, err
}
newScale, _, err := scaleFromCustomResource(cr, r.specReplicasPath, r.statusReplicasPath, r.labelSelectorPath)
if err != nil {
return nil, false, apierrors.NewBadRequest(err.Error())
}
return newScale, false, err
}
// scaleFromCustomResource returns a scale subresource for a customresource and a bool signalling wether
// the specReplicas value was found.
func scaleFromCustomResource(cr *unstructured.Unstructured, specReplicasPath, statusReplicasPath, labelSelectorPath string) (*autoscalingv1.Scale, bool, error) {
specReplicasPath = strings.TrimPrefix(specReplicasPath, ".") // ignore leading period
specReplicas, foundSpecReplicas, err := unstructured.NestedInt64(cr.UnstructuredContent(), strings.Split(specReplicasPath, ".")...)
if err != nil {
return nil, false, err
} else if !foundSpecReplicas {
specReplicas = 0
}
statusReplicasPath = strings.TrimPrefix(statusReplicasPath, ".") // ignore leading period
statusReplicas, found, err := unstructured.NestedInt64(cr.UnstructuredContent(), strings.Split(statusReplicasPath, ".")...)
if err != nil {
return nil, false, err
} else if !found {
statusReplicas = 0
}
var labelSelector string
if len(labelSelectorPath) > 0 {
labelSelectorPath = strings.TrimPrefix(labelSelectorPath, ".") // ignore leading period
labelSelector, found, err = unstructured.NestedString(cr.UnstructuredContent(), strings.Split(labelSelectorPath, ".")...)
if err != nil {
return nil, false, err
}
}
scale := &autoscalingv1.Scale{
ObjectMeta: metav1.ObjectMeta{
Name: cr.GetName(),
Namespace: cr.GetNamespace(),
UID: cr.GetUID(),
ResourceVersion: cr.GetResourceVersion(),
CreationTimestamp: cr.GetCreationTimestamp(),
},
Spec: autoscalingv1.ScaleSpec{
Replicas: int32(specReplicas),
},
Status: autoscalingv1.ScaleStatus{
Replicas: int32(statusReplicas),
Selector: labelSelector,
},
}
return scale, foundSpecReplicas, nil
} }
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package customresource_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
}
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package customresource
import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
)
// Registry is an interface for things that know how to store CustomResources.
type Registry interface {
ListCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*unstructured.UnstructuredList, error)
WatchCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error)
GetCustomResource(ctx genericapirequest.Context, customResourceID string, options *metav1.GetOptions) (*unstructured.Unstructured, error)
CreateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc) (*unstructured.Unstructured, error)
UpdateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (*unstructured.Unstructured, error)
DeleteCustomResource(ctx genericapirequest.Context, customResourceID string) error
}
// storage puts strong typing around storage calls
type storage struct {
rest.StandardStorage
}
// NewRegistry returns a new Registry interface for the given Storage. Any mismatched
// types will panic.
func NewRegistry(s rest.StandardStorage) Registry {
return &storage{s}
}
func (s *storage) ListCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (*unstructured.UnstructuredList, error) {
if options != nil && options.FieldSelector != nil && !options.FieldSelector.Empty() {
return nil, fmt.Errorf("field selector not supported yet")
}
obj, err := s.List(ctx, options)
if err != nil {
return nil, err
}
return obj.(*unstructured.UnstructuredList), err
}
func (s *storage) WatchCustomResources(ctx genericapirequest.Context, options *metainternalversion.ListOptions) (watch.Interface, error) {
return s.Watch(ctx, options)
}
func (s *storage) GetCustomResource(ctx genericapirequest.Context, customResourceID string, options *metav1.GetOptions) (*unstructured.Unstructured, error) {
obj, err := s.Get(ctx, customResourceID, options)
customResource, ok := obj.(*unstructured.Unstructured)
if !ok {
return nil, fmt.Errorf("custom resource must be of type Unstructured")
}
if err != nil {
apiVersion := customResource.GetAPIVersion()
groupVersion := strings.Split(apiVersion, "/")
group := groupVersion[0]
return nil, errors.NewNotFound(schema.GroupResource{Group: group, Resource: "scale"}, customResourceID)
}
return customResource, nil
}
func (s *storage) CreateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc) (*unstructured.Unstructured, error) {
obj, err := s.Create(ctx, customResource, rest.ValidateAllObjectFunc, false)
if err != nil {
return nil, err
}
return obj.(*unstructured.Unstructured), nil
}
func (s *storage) UpdateCustomResource(ctx genericapirequest.Context, customResource *unstructured.Unstructured, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc) (*unstructured.Unstructured, error) {
obj, _, err := s.Update(ctx, customResource.GetName(), rest.DefaultUpdatedObjectInfo(customResource), createValidation, updateValidation)
if err != nil {
return nil, err
}
return obj.(*unstructured.Unstructured), nil
}
func (s *storage) DeleteCustomResource(ctx genericapirequest.Context, customResourceID string) error {
_, _, err := s.Delete(ctx, customResourceID, nil)
return err
}
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package customresource
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
)
type statusStrategy struct {
customResourceStrategy
}
func NewStatusStrategy(strategy customResourceStrategy) statusStrategy {
return statusStrategy{strategy}
}
func (a statusStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
newCustomResourceObject := obj.(*unstructured.Unstructured)
oldCustomResourceObject := old.(*unstructured.Unstructured)
newCustomResource := newCustomResourceObject.UnstructuredContent()
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
// update is not allowed to set spec and metadata
_, ok1 := newCustomResource["spec"]
_, ok2 := oldCustomResource["spec"]
switch {
case ok2:
newCustomResource["spec"] = oldCustomResource["spec"]
case ok1:
delete(newCustomResource, "spec")
}
newCustomResourceObject.SetAnnotations(oldCustomResourceObject.GetAnnotations())
newCustomResourceObject.SetFinalizers(oldCustomResourceObject.GetFinalizers())
newCustomResourceObject.SetGeneration(oldCustomResourceObject.GetGeneration())
newCustomResourceObject.SetLabels(oldCustomResourceObject.GetLabels())
newCustomResourceObject.SetOwnerReferences(oldCustomResourceObject.GetOwnerReferences())
newCustomResourceObject.SetSelfLink(oldCustomResourceObject.GetSelfLink())
}
// ValidateUpdate is the default update validation for an end user updating status.
func (a statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return a.customResourceStrategy.validator.ValidateStatusUpdate(ctx, obj, old, a.scale)
}
...@@ -17,12 +17,10 @@ limitations under the License. ...@@ -17,12 +17,10 @@ limitations under the License.
package customresource package customresource
import ( import (
"fmt"
"github.com/go-openapi/validate" "github.com/go-openapi/validate"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
...@@ -31,63 +29,129 @@ import ( ...@@ -31,63 +29,129 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/storage" apiserverstorage "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
apiservervalidation "k8s.io/apiextensions-apiserver/pkg/apiserver/validation" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
) )
type customResourceDefinitionStorageStrategy struct { // customResourceStrategy implements behavior for CustomResources.
type customResourceStrategy struct {
runtime.ObjectTyper runtime.ObjectTyper
names.NameGenerator names.NameGenerator
namespaceScoped bool namespaceScoped bool
validator customResourceValidator validator customResourceValidator
status *apiextensions.CustomResourceSubresourceStatus
scale *apiextensions.CustomResourceSubresourceScale
} }
func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, validator *validate.SchemaValidator) customResourceDefinitionStorageStrategy { func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.GroupVersionKind, schemaValidator, statusSchemaValidator *validate.SchemaValidator, status *apiextensions.CustomResourceSubresourceStatus, scale *apiextensions.CustomResourceSubresourceScale) customResourceStrategy {
return customResourceDefinitionStorageStrategy{ return customResourceStrategy{
ObjectTyper: typer, ObjectTyper: typer,
NameGenerator: names.SimpleNameGenerator, NameGenerator: names.SimpleNameGenerator,
namespaceScoped: namespaceScoped, namespaceScoped: namespaceScoped,
status: status,
scale: scale,
validator: customResourceValidator{ validator: customResourceValidator{
namespaceScoped: namespaceScoped, namespaceScoped: namespaceScoped,
kind: kind, kind: kind,
validator: validator, schemaValidator: schemaValidator,
statusSchemaValidator: statusSchemaValidator,
}, },
} }
} }
func (a customResourceDefinitionStorageStrategy) NamespaceScoped() bool { func (a customResourceStrategy) NamespaceScoped() bool {
return a.namespaceScoped return a.namespaceScoped
} }
func (customResourceDefinitionStorageStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { // PrepareForCreate clears the status of a CustomResource before creation.
func (a customResourceStrategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && a.status != nil {
customResourceObject := obj.(*unstructured.Unstructured)
customResource := customResourceObject.UnstructuredContent()
// create cannot set status
if _, ok := customResource["status"]; ok {
delete(customResource, "status")
}
}
accessor, _ := meta.Accessor(obj)
accessor.SetGeneration(1)
} }
func (customResourceDefinitionStorageStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { // PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (a customResourceStrategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) || a.status == nil {
return
}
newCustomResourceObject := obj.(*unstructured.Unstructured)
oldCustomResourceObject := old.(*unstructured.Unstructured)
newCustomResource := newCustomResourceObject.UnstructuredContent()
oldCustomResource := oldCustomResourceObject.UnstructuredContent()
// update is not allowed to set status
_, ok1 := newCustomResource["status"]
_, ok2 := oldCustomResource["status"]
switch {
case ok2:
newCustomResource["status"] = oldCustomResource["status"]
case ok1:
delete(newCustomResource, "status")
}
// Any changes to the spec increment the generation number, any changes to the
// status should reflect the generation number of the corresponding object. We push
// the burden of managing the status onto the clients because we can't (in general)
// know here what version of spec the writer of the status has seen. It may seem like
// we can at first -- since obj contains spec -- but in the future we will probably make
// status its own object, and even if we don't, writes may be the result of a
// read-update-write loop, so the contents of spec may not actually be the spec that
// the CustomResource has *seen*.
newSpec, ok1 := newCustomResource["spec"]
oldSpec, ok2 := oldCustomResource["spec"]
// spec is changed, created or deleted
if (ok1 && ok2 && !apiequality.Semantic.DeepEqual(oldSpec, newSpec)) || (ok1 && !ok2) || (!ok1 && ok2) {
oldAccessor, _ := meta.Accessor(oldCustomResourceObject)
newAccessor, _ := meta.Accessor(newCustomResourceObject)
newAccessor.SetGeneration(oldAccessor.GetGeneration() + 1)
}
} }
func (a customResourceDefinitionStorageStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { // Validate validates a new CustomResource.
return a.validator.Validate(ctx, obj) func (a customResourceStrategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
return a.validator.Validate(ctx, obj, a.scale)
} }
func (customResourceDefinitionStorageStrategy) AllowCreateOnUpdate() bool { // Canonicalize normalizes the object after validation.
return false func (customResourceStrategy) Canonicalize(obj runtime.Object) {
} }
func (customResourceDefinitionStorageStrategy) AllowUnconditionalUpdate() bool { // AllowCreateOnUpdate is false for CustomResources; this means a POST is
// needed to create one.
func (customResourceStrategy) AllowCreateOnUpdate() bool {
return false return false
} }
func (customResourceDefinitionStorageStrategy) Canonicalize(obj runtime.Object) { // AllowUnconditionalUpdate is the default update policy for CustomResource objects.
func (customResourceStrategy) AllowUnconditionalUpdate() bool {
return false
} }
func (a customResourceDefinitionStorageStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { // ValidateUpdate is the default update validation for an end user updating status.
return a.validator.ValidateUpdate(ctx, obj, old) 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) 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) accessor, err := meta.Accessor(obj)
if err != nil { if err != nil {
return nil, nil, false, err return nil, nil, false, err
...@@ -108,80 +172,13 @@ func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields. ...@@ -108,80 +172,13 @@ func objectMetaFieldsSet(objectMeta metav1.Object, namespaceScoped bool) fields.
} }
} }
func (a customResourceDefinitionStorageStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) storage.SelectionPredicate { // MatchCustomResourceDefinitionStorage is the filter used by the generic etcd backend to route
return storage.SelectionPredicate{ // watch events from etcd to clients of the apiserver only interested in specific
// labels/fields.
func (a customResourceStrategy) MatchCustomResourceDefinitionStorage(label labels.Selector, field fields.Selector) apiserverstorage.SelectionPredicate {
return apiserverstorage.SelectionPredicate{
Label: label, Label: label,
Field: field, Field: field,
GetAttrs: a.GetAttrs, GetAttrs: a.GetAttrs,
} }
} }
type customResourceValidator struct {
namespaceScoped bool
kind schema.GroupVersionKind
validator *validate.SchemaValidator
}
func (a customResourceValidator) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
accessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
return validation.ValidateObjectMetaAccessor(accessor, a.namespaceScoped, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
}
func (a customResourceValidator) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
objAccessor, err := meta.Accessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
oldAccessor, err := meta.Accessor(old)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("metadata"), nil, err.Error())}
}
typeAccessor, err := meta.TypeAccessor(obj)
if err != nil {
return field.ErrorList{field.Invalid(field.NewPath("kind"), nil, err.Error())}
}
if typeAccessor.GetKind() != a.kind.Kind {
return field.ErrorList{field.Invalid(field.NewPath("kind"), typeAccessor.GetKind(), fmt.Sprintf("must be %v", a.kind.Kind))}
}
if typeAccessor.GetAPIVersion() != a.kind.Group+"/"+a.kind.Version {
return field.ErrorList{field.Invalid(field.NewPath("apiVersion"), typeAccessor.GetAPIVersion(), fmt.Sprintf("must be %v", a.kind.Group+"/"+a.kind.Version))}
}
customResourceObject, ok := obj.(*unstructured.Unstructured)
// this will never happen.
if !ok {
return field.ErrorList{field.Invalid(field.NewPath(""), customResourceObject, fmt.Sprintf("has type %T. Must be a pointer to an Unstructured type", customResourceObject))}
}
customResource := customResourceObject.UnstructuredContent()
if err = apiservervalidation.ValidateCustomResource(customResource, a.validator); err != nil {
return field.ErrorList{field.Invalid(field.NewPath(""), customResource, err.Error())}
}
return validation.ValidateObjectMetaAccessorUpdate(objAccessor, oldAccessor, field.NewPath("metadata"))
}
/*
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 ( ...@@ -35,6 +35,7 @@ import (
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
) )
// strategy implements behavior for CustomResources.
type strategy struct { type strategy struct {
runtime.ObjectTyper runtime.ObjectTyper
names.NameGenerator names.NameGenerator
...@@ -48,6 +49,7 @@ func (strategy) NamespaceScoped() bool { ...@@ -48,6 +49,7 @@ func (strategy) NamespaceScoped() bool {
return false return false
} }
// PrepareForCreate clears the status of a CustomResourceDefinition before creation.
func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) { func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Object) {
crd := obj.(*apiextensions.CustomResourceDefinition) crd := obj.(*apiextensions.CustomResourceDefinition)
crd.Status = apiextensions.CustomResourceDefinitionStatus{} crd.Status = apiextensions.CustomResourceDefinitionStatus{}
...@@ -57,8 +59,12 @@ func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Obje ...@@ -57,8 +59,12 @@ func (strategy) PrepareForCreate(ctx genericapirequest.Context, obj runtime.Obje
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
crd.Spec.Validation = nil crd.Spec.Validation = nil
} }
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
crd.Spec.Subresources = nil
}
} }
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) { func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime.Object) {
newCRD := obj.(*apiextensions.CustomResourceDefinition) newCRD := obj.(*apiextensions.CustomResourceDefinition)
oldCRD := old.(*apiextensions.CustomResourceDefinition) oldCRD := old.(*apiextensions.CustomResourceDefinition)
...@@ -80,23 +86,33 @@ func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime ...@@ -80,23 +86,33 @@ func (strategy) PrepareForUpdate(ctx genericapirequest.Context, obj, old runtime
newCRD.Spec.Validation = nil newCRD.Spec.Validation = nil
oldCRD.Spec.Validation = nil oldCRD.Spec.Validation = nil
} }
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) {
newCRD.Spec.Subresources = nil
oldCRD.Spec.Subresources = nil
}
} }
// Validate validates a new CustomResourceDefinition.
func (strategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList { func (strategy) Validate(ctx genericapirequest.Context, obj runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition)) return validation.ValidateCustomResourceDefinition(obj.(*apiextensions.CustomResourceDefinition))
} }
// AllowCreateOnUpdate is false for CustomResourceDefinition; this means a POST is
// needed to create one.
func (strategy) AllowCreateOnUpdate() bool { func (strategy) AllowCreateOnUpdate() bool {
return false return false
} }
// AllowUnconditionalUpdate is the default update policy for CustomResourceDefinition objects.
func (strategy) AllowUnconditionalUpdate() bool { func (strategy) AllowUnconditionalUpdate() bool {
return false return false
} }
// Canonicalize normalizes the object after validation.
func (strategy) Canonicalize(obj runtime.Object) { func (strategy) Canonicalize(obj runtime.Object) {
} }
// ValidateUpdate is the default update validation for an end user updating status.
func (strategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList { func (strategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) return validation.ValidateCustomResourceDefinitionUpdate(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
} }
...@@ -143,10 +159,11 @@ func (statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old run ...@@ -143,10 +159,11 @@ func (statusStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old run
return validation.ValidateUpdateCustomResourceDefinitionStatus(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition)) return validation.ValidateUpdateCustomResourceDefinitionStatus(obj.(*apiextensions.CustomResourceDefinition), old.(*apiextensions.CustomResourceDefinition))
} }
// GetAttrs returns labels and fields of a given object for filtering purposes.
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) { func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, bool, error) {
apiserver, ok := obj.(*apiextensions.CustomResourceDefinition) apiserver, ok := obj.(*apiextensions.CustomResourceDefinition)
if !ok { if !ok {
return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition.") return nil, nil, false, fmt.Errorf("given object is not a CustomResourceDefinition")
} }
return labels.Set(apiserver.ObjectMeta.Labels), CustomResourceDefinitionToSelectableFields(apiserver), apiserver.Initializers != nil, nil return labels.Set(apiserver.ObjectMeta.Labels), CustomResourceDefinitionToSelectableFields(apiserver), apiserver.Initializers != nil, nil
} }
......
...@@ -11,6 +11,7 @@ go_test( ...@@ -11,6 +11,7 @@ go_test(
"basic_test.go", "basic_test.go",
"finalization_test.go", "finalization_test.go",
"registration_test.go", "registration_test.go",
"subresources_test.go",
"validation_test.go", "validation_test.go",
"yaml_test.go", "yaml_test.go",
], ],
...@@ -19,17 +20,23 @@ go_test( ...@@ -19,17 +20,23 @@ go_test(
"//vendor/github.com/coreos/etcd/clientv3:go_default_library", "//vendor/github.com/coreos/etcd/clientv3:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library", "//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/github.com/stretchr/testify/require:go_default_library", "//vendor/github.com/stretchr/testify/require:go_default_library",
"//vendor/k8s.io/api/autoscaling/v1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apiserver:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/test/integration/testserver:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/test/integration/testserver:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/watch:go_default_library", "//vendor/k8s.io/apimachinery/pkg/watch:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library",
], ],
) )
......
...@@ -35,7 +35,7 @@ import ( ...@@ -35,7 +35,7 @@ import (
) )
func TestServerUp(t *testing.T) { func TestServerUp(t *testing.T) {
stopCh, _, _, err := testserver.StartDefaultServer() stopCh, _, _, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -43,7 +43,7 @@ func TestServerUp(t *testing.T) { ...@@ -43,7 +43,7 @@ func TestServerUp(t *testing.T) {
} }
func TestNamespaceScopedCRUD(t *testing.T) { func TestNamespaceScopedCRUD(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -61,7 +61,7 @@ func TestNamespaceScopedCRUD(t *testing.T) { ...@@ -61,7 +61,7 @@ func TestNamespaceScopedCRUD(t *testing.T) {
} }
func TestClusterScopedCRUD(t *testing.T) { func TestClusterScopedCRUD(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -347,7 +347,7 @@ func TestDiscovery(t *testing.T) { ...@@ -347,7 +347,7 @@ func TestDiscovery(t *testing.T) {
group := "mygroup.example.com" group := "mygroup.example.com"
version := "v1beta1" version := "v1beta1"
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -391,7 +391,7 @@ func TestDiscovery(t *testing.T) { ...@@ -391,7 +391,7 @@ func TestDiscovery(t *testing.T) {
} }
func TestNoNamespaceReject(t *testing.T) { func TestNoNamespaceReject(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -430,7 +430,7 @@ func TestNoNamespaceReject(t *testing.T) { ...@@ -430,7 +430,7 @@ func TestNoNamespaceReject(t *testing.T) {
} }
func TestSameNameDiffNamespace(t *testing.T) { func TestSameNameDiffNamespace(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -450,7 +450,7 @@ func TestSameNameDiffNamespace(t *testing.T) { ...@@ -450,7 +450,7 @@ func TestSameNameDiffNamespace(t *testing.T) {
} }
func TestSelfLink(t *testing.T) { func TestSelfLink(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -503,7 +503,7 @@ func TestSelfLink(t *testing.T) { ...@@ -503,7 +503,7 @@ func TestSelfLink(t *testing.T) {
} }
func TestPreserveInt(t *testing.T) { func TestPreserveInt(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -548,7 +548,7 @@ func TestPreserveInt(t *testing.T) { ...@@ -548,7 +548,7 @@ func TestPreserveInt(t *testing.T) {
} }
func TestPatch(t *testing.T) { func TestPatch(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -622,7 +622,7 @@ func TestPatch(t *testing.T) { ...@@ -622,7 +622,7 @@ func TestPatch(t *testing.T) {
} }
func TestCrossNamespaceListWatch(t *testing.T) { func TestCrossNamespaceListWatch(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -758,7 +758,7 @@ func checkNamespacesWatchHelper(t *testing.T, ns string, namespacedwatch watch.I ...@@ -758,7 +758,7 @@ func checkNamespacesWatchHelper(t *testing.T, ns string, namespacedwatch watch.I
} }
func TestNameConflict(t *testing.T) { func TestNameConflict(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -28,7 +28,7 @@ import ( ...@@ -28,7 +28,7 @@ import (
) )
func TestFinalization(t *testing.T) { func TestFinalization(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
require.NoError(t, err) require.NoError(t, err)
defer close(stopCh) defer close(stopCh)
......
...@@ -30,6 +30,7 @@ import ( ...@@ -30,6 +30,7 @@ import (
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/testserver" "k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
...@@ -73,8 +74,22 @@ func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, defi ...@@ -73,8 +74,22 @@ func NewNamespacedCustomResourceClient(ns string, client dynamic.Interface, defi
}, ns) }, ns)
} }
func NewNamespacedCustomResourceStatusClient(ns string, client dynamic.Interface, definition *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
return client.Resource(&metav1.APIResource{
Name: definition.Spec.Names.Plural + "/status",
Namespaced: definition.Spec.Scope == apiextensionsv1beta1.NamespaceScoped,
}, ns)
}
func NewNamespacedCustomResourceScaleClient(ns string, client dynamic.Interface, definition *apiextensionsv1beta1.CustomResourceDefinition) dynamic.ResourceInterface {
return client.Resource(&metav1.APIResource{
Name: definition.Spec.Names.Plural + "/scale",
Namespaced: definition.Spec.Scope == apiextensionsv1beta1.NamespaceScoped,
}, ns)
}
func TestMultipleResourceInstances(t *testing.T) { func TestMultipleResourceInstances(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -198,7 +213,7 @@ func TestMultipleResourceInstances(t *testing.T) { ...@@ -198,7 +213,7 @@ func TestMultipleResourceInstances(t *testing.T) {
} }
func TestMultipleRegistration(t *testing.T) { func TestMultipleRegistration(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -254,7 +269,7 @@ func TestMultipleRegistration(t *testing.T) { ...@@ -254,7 +269,7 @@ func TestMultipleRegistration(t *testing.T) {
} }
func TestDeRegistrationAndReRegistration(t *testing.T) { func TestDeRegistrationAndReRegistration(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -347,12 +362,18 @@ func TestEtcdStorage(t *testing.T) { ...@@ -347,12 +362,18 @@ func TestEtcdStorage(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config) stopCh, clientConfig, err := testserver.StartServer(config)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer close(stopCh) defer close(stopCh)
apiExtensionClient, err := apiextensionsclientset.NewForConfig(clientConfig)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(clientConfig)
etcdPrefix := getPrefixFromConfig(t, config) etcdPrefix := getPrefixFromConfig(t, config)
ns1 := "another-default-is-possible" ns1 := "another-default-is-possible"
......
/*
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( ...@@ -28,7 +28,10 @@ go_library(
"//vendor/k8s.io/apiserver/pkg/server:go_default_library", "//vendor/k8s.io/apiserver/pkg/server:go_default_library",
"//vendor/k8s.io/apiserver/pkg/server/options:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/options:go_default_library",
"//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library", "//vendor/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//vendor/k8s.io/client-go/discovery:go_default_library",
"//vendor/k8s.io/client-go/dynamic:go_default_library", "//vendor/k8s.io/client-go/dynamic:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/scale:go_default_library",
], ],
) )
......
...@@ -30,7 +30,10 @@ import ( ...@@ -30,7 +30,10 @@ import (
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/apiserver/pkg/storage/names" "k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/scale"
) )
const ( const (
...@@ -293,3 +296,34 @@ func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefi ...@@ -293,3 +296,34 @@ func DeleteCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefi
func GetCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1beta1.CustomResourceDefinition, error) { func GetCustomResourceDefinition(crd *apiextensionsv1beta1.CustomResourceDefinition, apiExtensionsClient clientset.Interface) (*apiextensionsv1beta1.CustomResourceDefinition, error) {
return apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{}) return apiExtensionsClient.Apiextensions().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{})
} }
func CreateNewScaleClient(crd *apiextensionsv1beta1.CustomResourceDefinition, config *rest.Config) (scale.ScalesGetter, error) {
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
groupResource, err := discoveryClient.ServerResourcesForGroupVersion(crd.Spec.Group + "/" + crd.Spec.Version)
if err != nil {
return nil, err
}
resources := []*discovery.APIGroupResources{
{
Group: metav1.APIGroup{
Name: crd.Spec.Group,
Versions: []metav1.GroupVersionForDiscovery{
{Version: crd.Spec.Version},
},
PreferredVersion: metav1.GroupVersionForDiscovery{Version: crd.Spec.Version},
},
VersionedResources: map[string][]metav1.APIResource{
crd.Spec.Version: groupResource.APIResources,
},
},
}
restMapper := discovery.NewRESTMapper(resources, nil)
resolver := scale.NewDiscoveryScaleKindResolver(discoveryClient)
return scale.NewForConfig(config, restMapper, dynamic.LegacyAPIPathResolverFunc, resolver)
}
...@@ -32,6 +32,7 @@ import ( ...@@ -32,6 +32,7 @@ import (
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
genericapiserveroptions "k8s.io/apiserver/pkg/server/options" genericapiserveroptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
) )
func DefaultServerConfig() (*extensionsapiserver.Config, error) { func DefaultServerConfig() (*extensionsapiserver.Config, error) {
...@@ -87,11 +88,11 @@ func DefaultServerConfig() (*extensionsapiserver.Config, error) { ...@@ -87,11 +88,11 @@ func DefaultServerConfig() (*extensionsapiserver.Config, error) {
return config, nil return config, nil
} }
func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.Interface, dynamic.ClientPool, error) { func StartServer(config *extensionsapiserver.Config) (chan struct{}, *rest.Config, error) {
stopCh := make(chan struct{}) stopCh := make(chan struct{})
server, err := config.Complete().New(genericapiserver.EmptyDelegate) server, err := config.Complete().New(genericapiserver.EmptyDelegate)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, err
} }
go func() { go func() {
err := server.GenericAPIServer.PrepareRun().Run(stopCh) err := server.GenericAPIServer.PrepareRun().Run(stopCh)
...@@ -123,26 +124,32 @@ func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.I ...@@ -123,26 +124,32 @@ func StartServer(config *extensionsapiserver.Config) (chan struct{}, clientset.I
}) })
if err != nil { if err != nil {
close(stopCh) close(stopCh)
return nil, nil, nil, err return nil, nil, err
} }
apiExtensionsClient, err := clientset.NewForConfig(server.GenericAPIServer.LoopbackClientConfig) return stopCh, config.GenericConfig.LoopbackClientConfig, nil
}
func StartDefaultServer() (chan struct{}, *rest.Config, error) {
config, err := DefaultServerConfig()
if err != nil { if err != nil {
close(stopCh) return nil, nil, err
return nil, nil, nil, err
} }
bytes, _ := apiExtensionsClient.Discovery().RESTClient().Get().AbsPath("/apis/apiextensions.k8s.io/v1beta1").DoRaw() return StartServer(config)
fmt.Print(string(bytes))
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(server.GenericAPIServer.LoopbackClientConfig), nil
} }
func StartDefaultServer() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) { func StartDefaultServerWithClients() (chan struct{}, clientset.Interface, dynamic.ClientPool, error) {
config, err := DefaultServerConfig() stopCh, config, err := StartDefaultServer()
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
return StartServer(config) apiExtensionsClient, err := clientset.NewForConfig(config)
if err != nil {
close(stopCh)
return nil, nil, nil, err
}
return stopCh, apiExtensionsClient, dynamic.NewDynamicClientPool(config), nil
} }
...@@ -31,7 +31,7 @@ import ( ...@@ -31,7 +31,7 @@ import (
) )
func TestForProperValidationErrors(t *testing.T) { func TestForProperValidationErrors(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -169,7 +169,7 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure ...@@ -169,7 +169,7 @@ func newNoxuValidationInstance(namespace, name string) *unstructured.Unstructure
} }
func TestCustomResourceValidation(t *testing.T) { func TestCustomResourceValidation(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -190,7 +190,7 @@ func TestCustomResourceValidation(t *testing.T) { ...@@ -190,7 +190,7 @@ func TestCustomResourceValidation(t *testing.T) {
} }
func TestCustomResourceUpdateValidation(t *testing.T) { func TestCustomResourceUpdateValidation(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -233,7 +233,7 @@ func TestCustomResourceUpdateValidation(t *testing.T) { ...@@ -233,7 +233,7 @@ func TestCustomResourceUpdateValidation(t *testing.T) {
} }
func TestCustomResourceValidationErrors(t *testing.T) { func TestCustomResourceValidationErrors(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -324,7 +324,7 @@ func TestCustomResourceValidationErrors(t *testing.T) { ...@@ -324,7 +324,7 @@ func TestCustomResourceValidationErrors(t *testing.T) {
} }
func TestCRValidationOnCRDUpdate(t *testing.T) { func TestCRValidationOnCRDUpdate(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -378,7 +378,7 @@ func TestCRValidationOnCRDUpdate(t *testing.T) { ...@@ -378,7 +378,7 @@ func TestCRValidationOnCRDUpdate(t *testing.T) {
} }
func TestForbiddenFieldsInSchema(t *testing.T) { func TestForbiddenFieldsInSchema(t *testing.T) {
stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServer() stopCh, apiExtensionClient, clientPool, err := testserver.StartDefaultServerWithClients()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
......
...@@ -24,24 +24,31 @@ import ( ...@@ -24,24 +24,31 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
"k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/test/integration/testserver"
) )
func TestYAML(t *testing.T) { func TestYAML(t *testing.T) {
config, err := testserver.DefaultServerConfig() stopCh, config, err := testserver.StartDefaultServer()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer close(stopCh)
stopCh, apiExtensionClient, clientPool, err := testserver.StartServer(config) apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer close(stopCh) clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped) noxuDefinition := testserver.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool) _, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
...@@ -232,7 +239,7 @@ values: ...@@ -232,7 +239,7 @@ values:
Param("watch", "true"). Param("watch", "true").
DoRaw() DoRaw()
if !errors.IsNotAcceptable(err) { if !errors.IsNotAcceptable(err) {
t.Fatal("expected not acceptable error, got %v (%s)", err, string(result)) t.Fatalf("expected not acceptable error, got %v (%s)", err, string(result))
} }
obj, err := decodeYAML(result) obj, err := decodeYAML(result)
if err != nil { if err != nil {
...@@ -294,7 +301,7 @@ values: ...@@ -294,7 +301,7 @@ values:
t.Fatal(v, ok, err, string(result)) t.Fatal(v, ok, err, string(result))
} }
if obj.GetUID() != uid { if obj.GetUID() != uid {
t.Fatal("uid changed: %v vs %v", uid, obj.GetUID()) t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
} }
} }
...@@ -346,6 +353,179 @@ values: ...@@ -346,6 +353,179 @@ values:
} }
} }
func TestYAMLSubresource(t *testing.T) {
// enable alpha feature CustomResourceSubresources
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
stopCh, config, err := testserver.StartDefaultServer()
if err != nil {
t.Fatal(err)
}
defer close(stopCh)
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
clientPool := dynamic.NewDynamicClientPool(config)
noxuDefinition := NewNoxuSubresourcesCRD(apiextensionsv1beta1.ClusterScoped)
_, err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, clientPool)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
rest := apiExtensionClient.Discovery().RESTClient()
uid := types.UID("")
resourceVersion := ""
// Create
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
spec:
replicas: 3`, apiVersion, kind))
result, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
uid = obj.GetUID()
resourceVersion = obj.GetResourceVersion()
}
// Get at /status
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
// Update at /status
{
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: mytest
uid: %s
resourceVersion: "%s"
spec:
replicas: 5
status:
replicas: 3`, apiVersion, kind, uid, resourceVersion))
result, err := rest.Put().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "status").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatal(err, string(result))
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err)
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != apiVersion {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != kind {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if obj.GetUID() != uid {
t.Fatalf("uid changed: %v vs %v", uid, obj.GetUID())
}
}
// Get at /scale
{
result, err := rest.Get().
SetHeader("Accept", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "mytest", "scale").
DoRaw()
if err != nil {
t.Fatal(err)
}
obj, err := decodeYAML(result)
if err != nil {
t.Fatal(err, string(result))
}
if obj.GetName() != "mytest" {
t.Fatalf("expected mytest, got %s", obj.GetName())
}
if obj.GetAPIVersion() != "autoscaling/v1" {
t.Fatalf("expected %s, got %s", apiVersion, obj.GetAPIVersion())
}
if obj.GetKind() != "Scale" {
t.Fatalf("expected %s, got %s", kind, obj.GetKind())
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "spec", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
if v, ok, err := unstructured.NestedFloat64(obj.Object, "status", "replicas"); v != 3 || !ok || err != nil {
t.Fatal(v, ok, err, string(result))
}
}
}
func decodeYAML(data []byte) (*unstructured.Unstructured, error) { func decodeYAML(data []byte) (*unstructured.Unstructured, error) {
retval := &unstructured.Unstructured{Object: map[string]interface{}{}} retval := &unstructured.Unstructured{Object: map[string]interface{}{}}
// ensure this isn't JSON // ensure this isn't JSON
......
...@@ -280,6 +280,7 @@ func (rc *ResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error ...@@ -280,6 +280,7 @@ func (rc *ResourceClient) Watch(opts metav1.ListOptions) (watch.Interface, error
Watch() Watch()
} }
// Patch applies the patch and returns the patched resource.
func (rc *ResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) { func (rc *ResourceClient) Patch(name string, pt types.PatchType, data []byte) (*unstructured.Unstructured, error) {
result := new(unstructured.Unstructured) result := new(unstructured.Unstructured)
resourceName, subresourceName := rc.parseResourceSubresourceName() resourceName, subresourceName := rc.parseResourceSubresourceName()
......
...@@ -99,7 +99,7 @@ func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) { ...@@ -99,7 +99,7 @@ func fakeScaleClient(t *testing.T) (ScalesGetter, []schema.GroupResource) {
restMapperRes, err := discovery.GetAPIGroupResources(fakeDiscoveryClient) restMapperRes, err := discovery.GetAPIGroupResources(fakeDiscoveryClient)
if err != nil { if err != nil {
t.Fatalf("unexpected error while constructing resource list from fake discovery client: %v") t.Fatalf("unexpected error while constructing resource list from fake discovery client: %v", err)
} }
restMapper := discovery.NewRESTMapper(restMapperRes, apimeta.InterfacesForUnstructured) restMapper := discovery.NewRESTMapper(restMapperRes, apimeta.InterfacesForUnstructured)
......
eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.lisG21ATunZSCBP6vaqK_AZCIR18tN563RdqkAb6PGOipqTeg8R7VwZQIeDqS-2Vond6NX_KSC_D_uxxv0hBf2DiGXmwMUmP4nRXrsmbzT2qQKKIHYRDC_6jb2-FSfK14ezIe1Q07UiiJecDsN3CFEccS8E68Tdnp78p7yDwbTvpumnZmwYfyhlImtjFQv2YpyFVsjEHWK0R4e9T3ONQWcx6D2rSoxABbutrS03QwsJhHCeD9joL_gxfkFKm3CW8yWPSk2QYtx_Q1hu-tZR4IPb2tQPXPX3mtyhwBqziWgmJRDFCEjlCO5aCobiMm_9K5X05gue_DcgW163zh1P9jg.nleER2An8CUn_OuR.b77RFEFp0gC8j5yCAoARNKYmQIvWq99ibmf5ffJgdhQBF3sRYJLt_XflJ_2lsaiFOxvc45T2fnkMVy2lHFcri7F9f1BRiT_0AcDthxsecGzG8BZ9QvaM6b4Dn0rhjrOq8rsF0m3ZnbPBkkg3LV5EkbHWstMo2fgJPJhJswlGWhqJPJBDecG1nMBC8SMH32X-zVlSM-BLiaghvOGNxyb_RLZJZ3CLczIdQ2JO2UeYkOGCPGzernvkHDMpqQXc-8cmulDdHgCy87qFLy5ttGFgYbnTm92h_ChOGKZixeX0PL0pQY5wXd2xTO7Tg_Ov5E5FoVwIkwOextedVsF9iz_b_mwtCY3LXrvbJTW7zWrwBVsVyAXxT5iu0HyQ3tBVxT2GxS-yM5ApqLozcZCQg9flMyfSgThu82FfzEr0fI5vKw8zo0GdO4GBuVSppM9m6ToG6hlwyHD9g2YTZw9068hyq1_kZQhugJRjgGbpa2gyGqzx16fg0zVoupVIiq5KfvRlAQFeOVVjQwb0BWf25tJUj5tV3O9ge6dbKSXizEca33FJJwJWoXhd7DCREXUU9pBz06NCCf495BGoVbq3oLPDQc2mpcuy0XAPxSwXcc5Ts8DNs7MrxBlYdw81wMXuztIpOY4.XjKlMWl_H40XszToi2VU5g
\ No newline at end of file
clone:
path: github.com/go-openapi/validate
matrix:
GO_VERSION:
- "1.6"
build:
integration:
image: golang:$$GO_VERSION
pull: true
commands:
- go get -u github.com/axw/gocov/gocov
- go get -u gopkg.in/matm/v1/gocov-html
- go get -u github.com/cee-dub/go-junit-report
- go get -u github.com/stretchr/testify/assert
- go get -u gopkg.in/yaml.v2
- go get -u github.com/go-openapi/analysis
- go get -u github.com/go-openapi/errors
- go get -u github.com/go-openapi/loads
- go get -u github.com/go-openapi/strfmt
- go get -u github.com/go-openapi/runtime
- go test -race
- go test -v -cover -coverprofile=coverage.out -covermode=count
notify:
slack:
channel: bots
webhook_url: $$SLACK_URL
username: drone
publish:
coverage:
server: https://coverage.vmware.run
token: $$GITHUB_TOKEN
# threshold: 70
# must_increase: true
when:
matrix:
GO_VERSION: "1.6"
approve_by_comment: true
approve_regex: '^(:shipit:|:\+1:|\+1|LGTM|lgtm|Approved)'
reject_regex: ^[Rr]ejected
reset_on_push: false
reviewers:
members:
- casualjim
- chancez
- frapposelli
- vburenin
- pytlesk4
name: pullapprove
required: 1
language: go
go:
- 1.7
install:
- go get -u github.com/axw/gocov/gocov
- go get -u gopkg.in/matm/v1/gocov-html
- go get -u github.com/cee-dub/go-junit-report
- go get -u github.com/stretchr/testify/assert
- go get -u github.com/kr/pretty
- go get -u gopkg.in/yaml.v2
- go get -u github.com/go-openapi/analysis
- go get -u github.com/go-openapi/errors
- go get -u github.com/go-openapi/loads
- go get -u github.com/go-openapi/strfmt
- go get -u github.com/go-openapi/runtime
script:
- go test -v -race -cover -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
slack:
secure: EmObnQuM9Mw8J9vpFaKKHqSMN4Wsr/A9+v7ewAD5cEhA0T1P4m7MbJMiJOhxUhj/X+BFh2DamW+P2lT8mybj5wg8wnkQ2BteKA8Tawi6f9PRw2NRheO8tAi8o/npLnlmet0kc93mn+oLuqHw36w4+j5mkOl2FghkfGiUVhwrhkCP7KXQN+3TU87e+/HzQumlJ3nsE+6terVxkH3PmaUTsS5ONaODZfuxFpfb7RsoEl3skHf6d+tr+1nViLxxly7558Nc33C+W1mr0qiEvMLZ+kJ/CpGWBJ6CUJM3jm6hNe2eMuIPwEK2hxZob8c7n22VPap4K6a0bBRoydoDXaba+2sD7Ym6ivDO/DVyL44VeBBLyIiIBylDGQdZH+6SoWm90Qe/i7tnY/T5Ao5igT8f3cfQY1c3EsTfqmlDfrhmACBmwSlgkdVBLTprHL63JMY24LWmh4jhxsmMRZhCL4dze8su1w6pLN/pD1pGHtKYCEVbdTmaM3PblNRFf12XB7qosmQsgUndH4Vq3bTbU0s1pKjeDhRyLvFzvR0TBbo0pDLEoF1A/i5GVFWa7yLZNUDudQERRh7qv/xBl2excIaQ1sV4DSVm7bAE9l6Kp+yeHQJW2uN6Y3X8wu9gB9nv9l5HBze7wh8KE6PyWAOLYYqZg9/sAtsv/2GcQqXcKFF1zcA=
# Validation helpers [![Build Status](https://ci.vmware.run/api/badges/go-openapi/validate/status.svg)](https://ci.vmware.run/go-openapi/validate) [![Coverage](https://coverage.vmware.run/badges/go-openapi/validate/coverage.svg)](https://coverage.vmware.run/go-openapi/validate) [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io) # Validation helpers [![Build Status](https://travis-ci.org/go-openapi/validate.svg?branch=master)](https://travis-ci.org/go-openapi/validate) [![codecov](https://codecov.io/gh/go-openapi/validate/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/validate) [![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io)
[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/validate/master/LICENSE) [![GoDoc](https://godoc.org/github.com/go-openapi/validate?status.svg)](http://godoc.org/github.com/go-openapi/validate) [![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/validate/master/LICENSE) [![GoDoc](https://godoc.org/github.com/go-openapi/validate?status.svg)](http://godoc.org/github.com/go-openapi/validate)
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
package validate package validate
import ( import (
"log"
"reflect" "reflect"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
...@@ -51,7 +52,9 @@ func (f *formatValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -51,7 +52,9 @@ func (f *formatValidator) Applies(source interface{}, kind reflect.Kind) bool {
return false return false
} }
r := doit() r := doit()
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", f.Path, r, source, kind) if Debug {
log.Printf("format validator for %q applies %t for %T (kind: %v)\n", f.Path, r, source, kind)
}
return r return r
} }
......
...@@ -15,8 +15,10 @@ ...@@ -15,8 +15,10 @@
package validate package validate
import ( import (
"log"
"reflect" "reflect"
"regexp" "regexp"
"strings"
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
...@@ -45,10 +47,45 @@ func (o *objectValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -45,10 +47,45 @@ func (o *objectValidator) Applies(source interface{}, kind reflect.Kind) bool {
// there is a problem in the type validator where it will be unhappy about null values // there is a problem in the type validator where it will be unhappy about null values
// so that requires more testing // so that requires more testing
r := reflect.TypeOf(source) == specSchemaType && (kind == reflect.Map || kind == reflect.Struct) r := reflect.TypeOf(source) == specSchemaType && (kind == reflect.Map || kind == reflect.Struct)
//fmt.Printf("object validator for %q applies %t for %T (kind: %v)\n", o.Path, r, source, kind) if Debug {
log.Printf("object validator for %q applies %t for %T (kind: %v)\n", o.Path, r, source, kind)
}
return r return r
} }
func (o *objectValidator) isPropertyName() bool {
p := strings.Split(o.Path, ".")
return p[len(p)-1] == "properties" && p[len(p)-2] != "properties"
}
func (o *objectValidator) checkArrayMustHaveItems(res *Result, val map[string]interface{}) {
if t, typeFound := val["type"]; typeFound {
if tpe, ok := t.(string); ok && tpe == "array" {
if _, itemsKeyFound := val["items"]; !itemsKeyFound {
res.AddErrors(errors.Required("items", o.Path))
}
}
}
}
func (o *objectValidator) checkItemsMustBeTypeArray(res *Result, val map[string]interface{}) {
if !o.isPropertyName() {
if _, itemsKeyFound := val["items"]; itemsKeyFound {
t, typeFound := val["type"]
if typeFound {
if tpe, ok := t.(string); !ok || tpe != "array" {
res.AddErrors(errors.InvalidType(o.Path, o.In, "array", nil))
}
} else {
// there is no type
res.AddErrors(errors.Required("type", o.Path))
}
}
}
}
func (o *objectValidator) precheck(res *Result, val map[string]interface{}) {
o.checkArrayMustHaveItems(res, val)
o.checkItemsMustBeTypeArray(res, val)
}
func (o *objectValidator) Validate(data interface{}) *Result { func (o *objectValidator) Validate(data interface{}) *Result {
val := data.(map[string]interface{}) val := data.(map[string]interface{})
numKeys := int64(len(val)) numKeys := int64(len(val))
...@@ -61,14 +98,8 @@ func (o *objectValidator) Validate(data interface{}) *Result { ...@@ -61,14 +98,8 @@ func (o *objectValidator) Validate(data interface{}) *Result {
} }
res := new(Result) res := new(Result)
if len(o.Required) > 0 {
for _, k := range o.Required { o.precheck(res, val)
if _, ok := val[k]; !ok {
res.AddErrors(errors.Required(o.Path+"."+k, o.In))
continue
}
}
}
if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows { if o.AdditionalProperties != nil && !o.AdditionalProperties.Allows {
for k := range val { for k := range val {
...@@ -99,6 +130,8 @@ func (o *objectValidator) Validate(data interface{}) *Result { ...@@ -99,6 +130,8 @@ func (o *objectValidator) Validate(data interface{}) *Result {
} }
} }
createdFromDefaults := map[string]bool{}
for pName, pSchema := range o.Properties { for pName, pSchema := range o.Properties {
rName := pName rName := pName
if o.Path != "" { if o.Path != "" {
...@@ -106,7 +139,24 @@ func (o *objectValidator) Validate(data interface{}) *Result { ...@@ -106,7 +139,24 @@ func (o *objectValidator) Validate(data interface{}) *Result {
} }
if v, ok := val[pName]; ok { if v, ok := val[pName]; ok {
res.Merge(NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats).Validate(v)) r := NewSchemaValidator(&pSchema, o.Root, rName, o.KnownFormats).Validate(v)
res.Merge(r)
} else if pSchema.Default != nil {
createdFromDefaults[pName] = true
pName := pName // shaddow
def := pSchema.Default
res.Defaulters = append(res.Defaulters, DefaulterFunc(func() {
val[pName] = def
}))
}
}
if len(o.Required) > 0 {
for _, k := range o.Required {
if _, ok := val[k]; !ok && !createdFromDefaults[k] {
res.AddErrors(errors.Required(o.Path+"."+k, o.In))
continue
}
} }
} }
...@@ -137,9 +187,6 @@ func (o *objectValidator) validatePatternProperty(key string, value interface{}, ...@@ -137,9 +187,6 @@ func (o *objectValidator) validatePatternProperty(key string, value interface{},
res := validator.Validate(value) res := validator.Validate(value)
result.Merge(res) result.Merge(res)
if res.IsValid() {
succeededOnce = true
}
} }
} }
......
...@@ -14,12 +14,32 @@ ...@@ -14,12 +14,32 @@
package validate package validate
import "github.com/go-openapi/errors" import (
"os"
"github.com/go-openapi/errors"
)
var (
// Debug is true when the SWAGGER_DEBUG env var is not empty
Debug = os.Getenv("SWAGGER_DEBUG") != ""
)
type Defaulter interface {
Apply()
}
type DefaulterFunc func()
func (f DefaulterFunc) Apply() {
f()
}
// Result represents a validation result // Result represents a validation result
type Result struct { type Result struct {
Errors []error Errors []error
MatchCount int MatchCount int
Defaulters []Defaulter
} }
// Merge merges this result with the other one, preserving match counts etc // Merge merges this result with the other one, preserving match counts etc
...@@ -29,11 +49,13 @@ func (r *Result) Merge(other *Result) *Result { ...@@ -29,11 +49,13 @@ func (r *Result) Merge(other *Result) *Result {
} }
r.AddErrors(other.Errors...) r.AddErrors(other.Errors...)
r.MatchCount += other.MatchCount r.MatchCount += other.MatchCount
r.Defaulters = append(r.Defaulters, other.Defaulters...)
return r return r
} }
// AddErrors adds errors to this validation result // AddErrors adds errors to this validation result
func (r *Result) AddErrors(errors ...error) { func (r *Result) AddErrors(errors ...error) {
// TODO: filter already existing errors
r.Errors = append(r.Errors, errors...) r.Errors = append(r.Errors, errors...)
} }
...@@ -59,3 +81,9 @@ func (r *Result) AsError() error { ...@@ -59,3 +81,9 @@ func (r *Result) AsError() error {
} }
return errors.CompositeValidationError(r.Errors...) return errors.CompositeValidationError(r.Errors...)
} }
func (r *Result) ApplyDefaults() {
for _, d := range r.Defaulters {
d.Apply()
}
}
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
package validate package validate
import ( import (
"encoding/json"
"log"
"reflect" "reflect"
"github.com/go-openapi/spec" "github.com/go-openapi/spec"
...@@ -80,12 +82,16 @@ func (s *SchemaValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -80,12 +82,16 @@ func (s *SchemaValidator) Applies(source interface{}, kind reflect.Kind) bool {
// Validate validates the data against the schema // Validate validates the data against the schema
func (s *SchemaValidator) Validate(data interface{}) *Result { func (s *SchemaValidator) Validate(data interface{}) *Result {
result := new(Result)
if s == nil {
return result
}
if data == nil { if data == nil {
v := s.validators[0].Validate(data) v := s.validators[0].Validate(data)
v.Merge(s.validators[6].Validate(data)) v.Merge(s.validators[6].Validate(data))
return v return v
} }
result := new(Result)
tpe := reflect.TypeOf(data) tpe := reflect.TypeOf(data)
kind := tpe.Kind() kind := tpe.Kind()
...@@ -98,8 +104,35 @@ func (s *SchemaValidator) Validate(data interface{}) *Result { ...@@ -98,8 +104,35 @@ func (s *SchemaValidator) Validate(data interface{}) *Result {
d = swag.ToDynamicJSON(data) d = swag.ToDynamicJSON(data)
} }
isnumber := s.Schema.Type.Contains("number") || s.Schema.Type.Contains("integer")
if num, ok := data.(json.Number); ok && isnumber {
if s.Schema.Type.Contains("integer") { // avoid lossy conversion
in, erri := num.Int64()
if erri != nil {
result.AddErrors(erri)
result.Inc()
return result
}
d = in
} else {
nf, errf := num.Float64()
if errf != nil {
result.AddErrors(errf)
result.Inc()
return result
}
d = nf
}
tpe = reflect.TypeOf(d)
kind = tpe.Kind()
}
for _, v := range s.validators { for _, v := range s.validators {
if !v.Applies(s.Schema, kind) { if !v.Applies(s.Schema, kind) {
if Debug {
log.Printf("%T does not apply for %v", v, kind)
}
continue continue
} }
...@@ -117,10 +150,9 @@ func (s *SchemaValidator) typeValidator() valueValidator { ...@@ -117,10 +150,9 @@ func (s *SchemaValidator) typeValidator() valueValidator {
func (s *SchemaValidator) commonValidator() valueValidator { func (s *SchemaValidator) commonValidator() valueValidator {
return &basicCommonValidator{ return &basicCommonValidator{
Path: s.Path, Path: s.Path,
In: s.in, In: s.in,
Default: s.Schema.Default, Enum: s.Schema.Enum,
Enum: s.Schema.Enum,
} }
} }
...@@ -155,7 +187,6 @@ func (s *SchemaValidator) stringValidator() valueValidator { ...@@ -155,7 +187,6 @@ func (s *SchemaValidator) stringValidator() valueValidator {
return &stringValidator{ return &stringValidator{
Path: s.Path, Path: s.Path,
In: s.in, In: s.in,
Default: s.Schema.Default,
MaxLength: s.Schema.MaxLength, MaxLength: s.Schema.MaxLength,
MinLength: s.Schema.MinLength, MinLength: s.Schema.MinLength,
Pattern: s.Schema.Pattern, Pattern: s.Schema.Pattern,
...@@ -164,9 +195,8 @@ func (s *SchemaValidator) stringValidator() valueValidator { ...@@ -164,9 +195,8 @@ func (s *SchemaValidator) stringValidator() valueValidator {
func (s *SchemaValidator) formatValidator() valueValidator { func (s *SchemaValidator) formatValidator() valueValidator {
return &formatValidator{ return &formatValidator{
Path: s.Path, Path: s.Path,
In: s.in, In: s.in,
//Default: s.Schema.Default,
Format: s.Schema.Format, Format: s.Schema.Format,
KnownFormats: s.KnownFormats, KnownFormats: s.KnownFormats,
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
package validate package validate
import ( import (
"log"
"reflect" "reflect"
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
...@@ -80,12 +81,15 @@ func newSchemaPropsValidator(path string, in string, allOf, oneOf, anyOf []spec. ...@@ -80,12 +81,15 @@ func newSchemaPropsValidator(path string, in string, allOf, oneOf, anyOf []spec.
func (s *schemaPropsValidator) Applies(source interface{}, kind reflect.Kind) bool { func (s *schemaPropsValidator) Applies(source interface{}, kind reflect.Kind) bool {
r := reflect.TypeOf(source) == specSchemaType r := reflect.TypeOf(source) == specSchemaType
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind) if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
}
return r return r
} }
func (s *schemaPropsValidator) Validate(data interface{}) *Result { func (s *schemaPropsValidator) Validate(data interface{}) *Result {
mainResult := new(Result) mainResult := new(Result)
var firstSuccess *Result
if len(s.anyOfValidators) > 0 { if len(s.anyOfValidators) > 0 {
var bestFailures *Result var bestFailures *Result
succeededOnce := false succeededOnce := false
...@@ -94,6 +98,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result { ...@@ -94,6 +98,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if result.IsValid() { if result.IsValid() {
bestFailures = nil bestFailures = nil
succeededOnce = true succeededOnce = true
if firstSuccess == nil {
firstSuccess = result
}
break break
} }
if bestFailures == nil || result.MatchCount > bestFailures.MatchCount { if bestFailures == nil || result.MatchCount > bestFailures.MatchCount {
...@@ -106,11 +113,14 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result { ...@@ -106,11 +113,14 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
} }
if bestFailures != nil { if bestFailures != nil {
mainResult.Merge(bestFailures) mainResult.Merge(bestFailures)
} else if firstSuccess != nil {
mainResult.Merge(firstSuccess)
} }
} }
if len(s.oneOfValidators) > 0 { if len(s.oneOfValidators) > 0 {
var bestFailures *Result var bestFailures *Result
var firstSuccess *Result
validated := 0 validated := 0
for _, oneOfSchema := range s.oneOfValidators { for _, oneOfSchema := range s.oneOfValidators {
...@@ -118,6 +128,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result { ...@@ -118,6 +128,9 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if result.IsValid() { if result.IsValid() {
validated++ validated++
bestFailures = nil bestFailures = nil
if firstSuccess == nil {
firstSuccess = result
}
continue continue
} }
if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) { if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) {
...@@ -130,6 +143,8 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result { ...@@ -130,6 +143,8 @@ func (s *schemaPropsValidator) Validate(data interface{}) *Result {
if bestFailures != nil { if bestFailures != nil {
mainResult.Merge(bestFailures) mainResult.Merge(bestFailures)
} }
} else if firstSuccess != nil {
mainResult.Merge(firstSuccess)
} }
} }
......
...@@ -67,6 +67,9 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result { ...@@ -67,6 +67,9 @@ func (s *schemaSliceValidator) Validate(data interface{}) *Result {
itemsSize = int64(len(s.Items.Schemas)) itemsSize = int64(len(s.Items.Schemas))
for i := int64(0); i < itemsSize; i++ { for i := int64(0); i < itemsSize; i++ {
validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats) validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats)
if val.Len() <= int(i) {
break
}
result.Merge(validator.Validate(val.Index(int(i)).Interface())) result.Merge(validator.Validate(val.Index(int(i)).Interface()))
} }
......
...@@ -19,6 +19,7 @@ import ( ...@@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"log" "log"
"regexp" "regexp"
"strconv"
"strings" "strings"
"github.com/go-openapi/analysis" "github.com/go-openapi/analysis"
...@@ -193,6 +194,13 @@ func (s *SpecValidator) validateDuplicatePropertyNames() *Result { ...@@ -193,6 +194,13 @@ func (s *SpecValidator) validateDuplicatePropertyNames() *Result {
return res return res
} }
func (s *SpecValidator) resolveRef(ref *spec.Ref) (*spec.Schema, error) {
if s.spec.SpecFilePath() != "" {
return spec.ResolveRefWithBase(s.spec.Spec(), ref, &spec.ExpandOptions{RelativeBase: s.spec.SpecFilePath()})
}
return spec.ResolveRef(s.spec.Spec(), ref)
}
func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, knowns map[string]struct{}) []dupProp { func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, knowns map[string]struct{}) []dupProp {
var dups []dupProp var dups []dupProp
...@@ -200,7 +208,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema, ...@@ -200,7 +208,7 @@ func (s *SpecValidator) validateSchemaPropertyNames(nm string, sch spec.Schema,
schc := &sch schc := &sch
for schc.Ref.String() != "" { for schc.Ref.String() != "" {
// gather property names // gather property names
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref) reso, err := s.resolveRef(&schc.Ref)
if err != nil { if err != nil {
panic(err) panic(err)
} }
...@@ -236,7 +244,7 @@ func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, kno ...@@ -236,7 +244,7 @@ func (s *SpecValidator) validateCircularAncestry(nm string, sch spec.Schema, kno
schn := nm schn := nm
schc := &sch schc := &sch
for schc.Ref.String() != "" { for schc.Ref.String() != "" {
reso, err := spec.ResolveRef(s.spec.Spec(), &schc.Ref) reso, err := s.resolveRef(&schc.Ref)
if err != nil { if err != nil {
panic(err) panic(err)
} }
...@@ -335,15 +343,15 @@ func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID str ...@@ -335,15 +343,15 @@ func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID str
return errors.New(422, "%s for %q is a collection without an element type", prefix, opID) return errors.New(422, "%s for %q is a collection without an element type", prefix, opID)
} }
schemas := schema.Items.Schemas
if schema.Items.Schema != nil { if schema.Items.Schema != nil {
schemas = []spec.Schema{*schema.Items.Schema} schema = *schema.Items.Schema
} if _, err := regexp.Compile(schema.Pattern); err != nil {
for _, sch := range schemas { return errors.New(422, "%s for %q has invalid items pattern: %q", prefix, opID, schema.Pattern)
if err := s.validateSchemaItems(sch, prefix, opID); err != nil {
return err
} }
return s.validateSchemaItems(schema, prefix, opID)
} }
return nil return nil
} }
...@@ -524,8 +532,15 @@ func (s *SpecValidator) validateParameters() *Result { ...@@ -524,8 +532,15 @@ func (s *SpecValidator) validateParameters() *Result {
} }
var fromPath []string var fromPath []string
for _, i := range params { for _, i := range params {
fromPath = append(fromPath, knowns[i]) knownsi := knowns[i]
knowns[i] = "!" iparams := extractPathParams(knownsi)
if len(iparams) > 0 {
fromPath = append(fromPath, iparams...)
for _, iparam := range iparams {
knownsi = strings.Replace(knownsi, iparam, "!", 1)
}
knowns[i] = knownsi
}
} }
knownPath := strings.Join(knowns, "/") knownPath := strings.Join(knowns, "/")
if orig, ok := knownPaths[knownPath]; ok { if orig, ok := knownPaths[knownPath]; ok {
...@@ -544,7 +559,9 @@ func (s *SpecValidator) validateParameters() *Result { ...@@ -544,7 +559,9 @@ func (s *SpecValidator) validateParameters() *Result {
for pr.Ref.String() != "" { for pr.Ref.String() != "" {
obj, _, err := pr.Ref.GetPointer().Get(sw) obj, _, err := pr.Ref.GetPointer().Get(sw)
if err != nil { if err != nil {
log.Println(err) if Debug {
log.Println(err)
}
res.AddErrors(err) res.AddErrors(err)
break PARAMETERS break PARAMETERS
} }
...@@ -575,6 +592,10 @@ func (s *SpecValidator) validateParameters() *Result { ...@@ -575,6 +592,10 @@ func (s *SpecValidator) validateParameters() *Result {
pr = obj.(spec.Parameter) pr = obj.(spec.Parameter)
} }
if _, err := regexp.Compile(pr.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in param %q: %q", op.ID, pr.Name, pr.Pattern))
}
if pr.In == "body" { if pr.In == "body" {
if firstBodyParam != "" { if firstBodyParam != "" {
res.AddErrors(errors.New(422, "operation %q has more than 1 body param (accepted: %q, dropped: %q)", op.ID, firstBodyParam, pr.Name)) res.AddErrors(errors.New(422, "operation %q has more than 1 body param (accepted: %q, dropped: %q)", op.ID, firstBodyParam, pr.Name))
...@@ -595,18 +616,35 @@ func (s *SpecValidator) validateParameters() *Result { ...@@ -595,18 +616,35 @@ func (s *SpecValidator) validateParameters() *Result {
func parsePath(path string) (segments []string, params []int) { func parsePath(path string) (segments []string, params []int) {
for i, p := range strings.Split(path, "/") { for i, p := range strings.Split(path, "/") {
segments = append(segments, p) segments = append(segments, p)
if len(p) > 0 && p[0] == '{' && p[len(p)-1] == '}' { if d0 := strings.Index(p, "{"); d0 >= 0 && d0 < strings.Index(p, "}") {
params = append(params, i) params = append(params, i)
} }
} }
return return
} }
func extractPathParams(segment string) (params []string) {
for {
d0 := strings.IndexByte(segment, '{')
if d0 < 0 {
break
}
d1 := strings.IndexByte(segment[d0:], '}')
if d1 > 0 {
params = append(params, segment[d0:d0+d1+1])
} else {
break
}
segment = segment[d1:]
}
return params
}
func (s *SpecValidator) validateReferencesValid() *Result { func (s *SpecValidator) validateReferencesValid() *Result {
// each reference must point to a valid object // each reference must point to a valid object
res := new(Result) res := new(Result)
for _, r := range s.analyzer.AllRefs() { for _, r := range s.analyzer.AllRefs() {
if !r.IsValidURI() { if !r.IsValidURI(s.spec.SpecFilePath()) {
res.AddErrors(errors.New(404, "invalid ref %q", r.String())) res.AddErrors(errors.New(404, "invalid ref %q", r.String()))
} }
} }
...@@ -698,7 +736,9 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result { ...@@ -698,7 +736,9 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
} }
// check simple paramters first // check simple paramters first
if param.Default != nil && param.Schema == nil { if param.Default != nil && param.Schema == nil {
//fmt.Println(param.Name, "in", param.In, "has a default without a schema") if Debug {
log.Println(param.Name, "in", param.In, "has a default without a schema")
}
// check param valid // check param valid
res.Merge(NewParamValidator(&param, s.KnownFormats).Validate(param.Default)) res.Merge(NewParamValidator(&param, s.KnownFormats).Validate(param.Default))
} }
...@@ -721,9 +761,15 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result { ...@@ -721,9 +761,15 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
if h.Items != nil { if h.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items)) res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items))
} }
if _, err := regexp.Compile(h.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in default header %q: %q", op.ID, nm, h.Pattern))
}
}
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 { for nm, h := range r.Headers {
if h.Default != nil { if h.Default != nil {
res.Merge(NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default)) res.Merge(NewHeaderValidator(nm, &h, s.KnownFormats).Validate(h.Default))
...@@ -731,6 +777,12 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result { ...@@ -731,6 +777,12 @@ func (s *SpecValidator) validateDefaultValueValidAgainstSchema() *Result {
if h.Items != nil { if h.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items)) res.Merge(s.validateDefaultValueItemsAgainstSchema(nm, "header", &h, h.Items))
} }
if _, err := regexp.Compile(h.Pattern); err != nil {
res.AddErrors(errors.New(422, "operation %q has invalid pattern in %v's header %q: %q", op.ID, code, nm, h.Pattern))
}
}
if r.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(strconv.Itoa(code), "response", r.Schema))
} }
} }
...@@ -758,6 +810,9 @@ func (s *SpecValidator) validateDefaultValueSchemaAgainstSchema(path, in string, ...@@ -758,6 +810,9 @@ func (s *SpecValidator) validateDefaultValueSchemaAgainstSchema(path, in string,
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.items[%d]", path, i), in, &sch)) res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.items[%d]", path, i), in, &sch))
} }
} }
if _, err := regexp.Compile(schema.Pattern); err != nil {
res.AddErrors(errors.New(422, "%s in %s has invalid pattern: %q", path, in, schema.Pattern))
}
if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil { if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil {
res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema)) res.Merge(s.validateDefaultValueSchemaAgainstSchema(fmt.Sprintf("%s.additionalItems", path), in, schema.AdditionalItems.Schema))
} }
...@@ -787,6 +842,9 @@ func (s *SpecValidator) validateDefaultValueItemsAgainstSchema(path, in string, ...@@ -787,6 +842,9 @@ func (s *SpecValidator) validateDefaultValueItemsAgainstSchema(path, in string,
if items.Items != nil { if items.Items != nil {
res.Merge(s.validateDefaultValueItemsAgainstSchema(path+"[0]", in, root, items.Items)) res.Merge(s.validateDefaultValueItemsAgainstSchema(path+"[0]", in, root, items.Items))
} }
if _, err := regexp.Compile(items.Pattern); err != nil {
res.AddErrors(errors.New(422, "%s in %s has invalid pattern: %q", path, in, items.Pattern))
}
} }
return res return res
} }
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
package validate package validate
import ( import (
"log"
"reflect" "reflect"
"strings" "strings"
...@@ -32,16 +33,6 @@ type typeValidator struct { ...@@ -32,16 +33,6 @@ type typeValidator struct {
Path string Path string
} }
var jsonTypeNames = map[string]struct{}{
"array": struct{}{},
"boolean": struct{}{},
"integer": struct{}{},
"null": struct{}{},
"number": struct{}{},
"object": struct{}{},
"string": struct{}{},
}
func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) { func (t *typeValidator) schemaInfoForType(data interface{}) (string, string) {
switch data.(type) { switch data.(type) {
case []byte: case []byte:
...@@ -121,7 +112,9 @@ func (t *typeValidator) SetPath(path string) { ...@@ -121,7 +112,9 @@ func (t *typeValidator) SetPath(path string) {
func (t *typeValidator) Applies(source interface{}, kind reflect.Kind) bool { func (t *typeValidator) Applies(source interface{}, kind reflect.Kind) bool {
stpe := reflect.TypeOf(source) stpe := reflect.TypeOf(source)
r := (len(t.Type) > 0 || t.Format != "") && (stpe == specSchemaType || stpe == specParameterType || stpe == specHeaderType) r := (len(t.Type) > 0 || t.Format != "") && (stpe == specSchemaType || stpe == specParameterType || stpe == specHeaderType)
//fmt.Printf("type validator for %q applies %t for %T (kind: %v)\n", t.Path, r, source, kind) if Debug {
log.Printf("type validator for %q applies %t for %T (kind: %v)\n", t.Path, r, source, kind)
}
return r return r
} }
...@@ -140,7 +133,9 @@ func (t *typeValidator) Validate(data interface{}) *Result { ...@@ -140,7 +133,9 @@ func (t *typeValidator) Validate(data interface{}) *Result {
kind := val.Kind() kind := val.Kind()
schType, format := t.schemaInfoForType(data) schType, format := t.schemaInfoForType(data)
//fmt.Println("path:", t.Path, "schType:", schType, "format:", format, "expType:", t.Type, "expFmt:", t.Format, "kind:", val.Kind().String()) if Debug {
log.Println("path:", t.Path, "schType:", schType, "format:", format, "expType:", t.Type, "expFmt:", t.Format, "kind:", val.Kind().String())
}
isLowerInt := t.Format == "int64" && format == "int32" isLowerInt := t.Format == "int64" && format == "int32"
isLowerFloat := t.Format == "float64" && format == "float32" isLowerFloat := t.Format == "float64" && format == "float32"
isFloatInt := schType == "number" && swag.IsFloat64AJSONInteger(val.Float()) && t.Type.Contains("integer") isFloatInt := schType == "number" && swag.IsFloat64AJSONInteger(val.Float()) && t.Type.Contains("integer")
......
#!/bin/bash
set -eu -o pipefail
dir=$(git rev-parse --show-toplevel)
scratch=$(mktemp -d -t tmp.XXXXXXXXXX)
function finish {
rm -rf "$scratch"
}
trap finish EXIT SIGHUP SIGINT SIGTERM
cd "$scratch"
git clone https://github.com/json-schema-org/JSON-Schema-Test-Suite Suite
cp -r Suite/tests/draft4/* "$dir/fixtures/jsonschema_suite"
cp -a Suite/remotes "$dir/fixtures/jsonschema_suite"
...@@ -16,6 +16,7 @@ package validate ...@@ -16,6 +16,7 @@ package validate
import ( import (
"fmt" "fmt"
"log"
"reflect" "reflect"
"github.com/go-openapi/errors" "github.com/go-openapi/errors"
...@@ -156,8 +157,15 @@ func (b *basicCommonValidator) Applies(source interface{}, kind reflect.Kind) bo ...@@ -156,8 +157,15 @@ func (b *basicCommonValidator) Applies(source interface{}, kind reflect.Kind) bo
func (b *basicCommonValidator) Validate(data interface{}) (res *Result) { func (b *basicCommonValidator) Validate(data interface{}) (res *Result) {
if len(b.Enum) > 0 { if len(b.Enum) > 0 {
for _, enumValue := range b.Enum { for _, enumValue := range b.Enum {
if data != nil && reflect.DeepEqual(enumValue, data) { actualType := reflect.TypeOf(enumValue)
return nil 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)) 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 { ...@@ -474,10 +482,14 @@ func (n *numberValidator) Applies(source interface{}, kind reflect.Kind) bool {
isInt := kind >= reflect.Int && kind <= reflect.Uint64 isInt := kind >= reflect.Int && kind <= reflect.Uint64
isFloat := kind == reflect.Float32 || kind == reflect.Float64 isFloat := kind == reflect.Float32 || kind == reflect.Float64
r := isInt || isFloat r := isInt || isFloat
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, r, source, kind) if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, r, source, kind)
}
return r return r
} }
// fmt.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, false, source, kind) if Debug {
log.Printf("schema props validator for %q applies %t for %T (kind: %v)\n", n.Path, false, source, kind)
}
return false return false
} }
...@@ -536,15 +548,22 @@ func (s *stringValidator) Applies(source interface{}, kind reflect.Kind) bool { ...@@ -536,15 +548,22 @@ func (s *stringValidator) Applies(source interface{}, kind reflect.Kind) bool {
switch source.(type) { switch source.(type) {
case *spec.Parameter, *spec.Schema, *spec.Items, *spec.Header: case *spec.Parameter, *spec.Schema, *spec.Items, *spec.Header:
r := kind == reflect.String r := kind == reflect.String
// fmt.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind) if Debug {
log.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, r, source, kind)
}
return r return r
} }
// fmt.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, false, source, kind) if Debug {
log.Printf("string validator for %q applies %t for %T (kind: %v)\n", s.Path, false, source, kind)
}
return false return false
} }
func (s *stringValidator) Validate(val interface{}) *Result { func (s *stringValidator) Validate(val interface{}) *Result {
data := val.(string) data, ok := val.(string)
if !ok {
return sErr(errors.InvalidType(s.Path, s.In, "string", val))
}
if s.Required && !s.AllowEmptyValue && (s.Default == nil || s.Default == "") { if s.Required && !s.AllowEmptyValue && (s.Default == nil || s.Default == "") {
if err := RequiredString(s.Path, s.In, data); err != nil { if err := RequiredString(s.Path, s.In, data); err != nil {
......
...@@ -196,7 +196,11 @@ func MinimumUint(path, in string, data, min uint64, exclusive bool) *errors.Vali ...@@ -196,7 +196,11 @@ func MinimumUint(path, in string, data, min uint64, exclusive bool) *errors.Vali
// MultipleOf validates if the provided number is a multiple of the factor // MultipleOf validates if the provided number is a multiple of the factor
func MultipleOf(path, in string, data, factor float64) *errors.Validation { func MultipleOf(path, in string, data, factor float64) *errors.Validation {
if !swag.IsFloat64AJSONInteger(data / factor) { mult := data / factor
if factor < 1 {
mult = 1 / factor * data
}
if !swag.IsFloat64AJSONInteger(mult) {
return errors.NotMultipleOf(path, in, factor) return errors.NotMultipleOf(path, in, factor)
} }
return nil return nil
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment