diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index a3faf6f3cbb..d23078279b5 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -3099,27 +3099,27 @@ }, { "ImportPath": "k8s.io/kube-openapi/pkg/aggregator", - "Rev": "80f07ef71bb4f781233c65aa8d0369e4ecafab87" + "Rev": "868f2f29720b192240e18284659231b440f9cda5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/builder", - "Rev": "80f07ef71bb4f781233c65aa8d0369e4ecafab87" + "Rev": "868f2f29720b192240e18284659231b440f9cda5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/common", - "Rev": "80f07ef71bb4f781233c65aa8d0369e4ecafab87" + "Rev": "868f2f29720b192240e18284659231b440f9cda5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/generators", - "Rev": "80f07ef71bb4f781233c65aa8d0369e4ecafab87" + "Rev": "868f2f29720b192240e18284659231b440f9cda5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/handler", - "Rev": "80f07ef71bb4f781233c65aa8d0369e4ecafab87" + "Rev": "868f2f29720b192240e18284659231b440f9cda5" }, { "ImportPath": "k8s.io/kube-openapi/pkg/util", - "Rev": "80f07ef71bb4f781233c65aa8d0369e4ecafab87" + "Rev": "868f2f29720b192240e18284659231b440f9cda5" }, { "ImportPath": "k8s.io/utils/exec", diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index c04928fcbf5..fdd28917f3d 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -19845,6 +19845,824 @@ } ] }, + "/apis/apiextensions.k8s.io/": { + "get": { + "description": "get information of a group", + "consumes": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions" + ], + "operationId": "getApiextensionsAPIGroup", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup" + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/apis/apiextensions.k8s.io/v1beta1/": { + "get": { + "description": "get available resources", + "consumes": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "getApiextensionsV1beta1APIResources", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "401": { + "description": "Unauthorized" + } + } + } + }, + "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions": { + "get": { + "description": "list or watch objects of kind CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "listApiextensionsV1beta1CustomResourceDefinition", + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server the server will respond with a 410 ResourceExpired error indicating the client must restart their list without the continue field. This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "name": "continue", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "name": "fieldSelector", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "If true, partially initialized resources are included in the response.", + "name": "includeUninitialized", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "name": "labelSelector", + "in": "query" + }, + { + "uniqueItems": true, + "type": "integer", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "name": "limit", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "When specified with a watch call, shows changes that occur after that particular version of a resource. Defaults to changes from the beginning of history. When specified for list: - if unset, then the result is returned from remote storage based on quorum-read flag; - if it's 0, then we simply return what we currently have in cache, no guarantee; - if set to non zero, then the result is at least as fresh as given rv.", + "name": "resourceVersion", + "in": "query" + }, + { + "uniqueItems": true, + "type": "integer", + "description": "Timeout for the list/watch call.", + "name": "timeoutSeconds", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "name": "watch", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionList" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "post": { + "description": "create a CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "createApiextensionsV1beta1CustomResourceDefinition", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "delete": { + "description": "delete collection of CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "deleteApiextensionsV1beta1CollectionCustomResourceDefinition", + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server the server will respond with a 410 ResourceExpired error indicating the client must restart their list without the continue field. This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "name": "continue", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "name": "fieldSelector", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "If true, partially initialized resources are included in the response.", + "name": "includeUninitialized", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "name": "labelSelector", + "in": "query" + }, + { + "uniqueItems": true, + "type": "integer", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "name": "limit", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "When specified with a watch call, shows changes that occur after that particular version of a resource. Defaults to changes from the beginning of history. When specified for list: - if unset, then the result is returned from remote storage based on quorum-read flag; - if it's 0, then we simply return what we currently have in cache, no guarantee; - if set to non zero, then the result is at least as fresh as given rv.", + "name": "resourceVersion", + "in": "query" + }, + { + "uniqueItems": true, + "type": "integer", + "description": "Timeout for the list/watch call.", + "name": "timeoutSeconds", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "name": "watch", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "description": "If 'true', then the output is pretty printed.", + "name": "pretty", + "in": "query" + } + ] + }, + "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/{name}": { + "get": { + "description": "read the specified CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "readApiextensionsV1beta1CustomResourceDefinition", + "parameters": [ + { + "uniqueItems": true, + "type": "boolean", + "description": "Should the export be exact. Exact export maintains cluster-specific fields like 'Namespace'.", + "name": "exact", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "Should this value be exported. Export strips fields that a user can not specify.", + "name": "export", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "put": { + "description": "replace the specified CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "replaceApiextensionsV1beta1CustomResourceDefinition", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "delete": { + "description": "delete a CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "deleteApiextensionsV1beta1CustomResourceDefinition", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions" + } + }, + { + "uniqueItems": true, + "type": "integer", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "name": "gracePeriodSeconds", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "name": "orphanDependents", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy.", + "name": "propagationPolicy", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "patch": { + "description": "partially update the specified CustomResourceDefinition", + "consumes": [ + "application/json-patch+json", + "application/merge-patch+json", + "application/strategic-merge-patch+json" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "patchApiextensionsV1beta1CustomResourceDefinition", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "description": "name of the CustomResourceDefinition", + "name": "name", + "in": "path", + "required": true + }, + { + "uniqueItems": true, + "type": "string", + "description": "If 'true', then the output is pretty printed.", + "name": "pretty", + "in": "query" + } + ] + }, + "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/{name}/status": { + "put": { + "description": "replace status of the specified CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "replaceApiextensionsV1beta1CustomResourceDefinitionStatus", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "description": "name of the CustomResourceDefinition", + "name": "name", + "in": "path", + "required": true + }, + { + "uniqueItems": true, + "type": "string", + "description": "If 'true', then the output is pretty printed.", + "name": "pretty", + "in": "query" + } + ] + }, + "/apis/apiextensions.k8s.io/v1beta1/watch/customresourcedefinitions": { + "get": { + "description": "watch individual changes to a list of CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "watchApiextensionsV1beta1CustomResourceDefinitionList", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "watchlist", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server the server will respond with a 410 ResourceExpired error indicating the client must restart their list without the continue field. This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "name": "continue", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "name": "fieldSelector", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "If true, partially initialized resources are included in the response.", + "name": "includeUninitialized", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "name": "labelSelector", + "in": "query" + }, + { + "uniqueItems": true, + "type": "integer", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "name": "limit", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "If 'true', then the output is pretty printed.", + "name": "pretty", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "When specified with a watch call, shows changes that occur after that particular version of a resource. Defaults to changes from the beginning of history. When specified for list: - if unset, then the result is returned from remote storage based on quorum-read flag; - if it's 0, then we simply return what we currently have in cache, no guarantee; - if set to non zero, then the result is at least as fresh as given rv.", + "name": "resourceVersion", + "in": "query" + }, + { + "uniqueItems": true, + "type": "integer", + "description": "Timeout for the list/watch call.", + "name": "timeoutSeconds", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "name": "watch", + "in": "query" + } + ] + }, + "/apis/apiextensions.k8s.io/v1beta1/watch/customresourcedefinitions/{name}": { + "get": { + "description": "watch changes to an object of kind CustomResourceDefinition", + "consumes": [ + "*/*" + ], + "produces": [ + "application/json", + "application/yaml", + "application/vnd.kubernetes.protobuf", + "application/json;stream=watch", + "application/vnd.kubernetes.protobuf;stream=watch" + ], + "schemes": [ + "https" + ], + "tags": [ + "apiextensions_v1beta1" + ], + "operationId": "watchApiextensionsV1beta1CustomResourceDefinition", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.WatchEvent" + } + }, + "401": { + "description": "Unauthorized" + } + }, + "x-kubernetes-action": "watch", + "x-kubernetes-group-version-kind": { + "group": "apiextensions.k8s.io", + "kind": "CustomResourceDefinition", + "version": "v1beta1" + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server the server will respond with a 410 ResourceExpired error indicating the client must restart their list without the continue field. This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "name": "continue", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "name": "fieldSelector", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "If true, partially initialized resources are included in the response.", + "name": "includeUninitialized", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "name": "labelSelector", + "in": "query" + }, + { + "uniqueItems": true, + "type": "integer", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "name": "limit", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "name of the CustomResourceDefinition", + "name": "name", + "in": "path", + "required": true + }, + { + "uniqueItems": true, + "type": "string", + "description": "If 'true', then the output is pretty printed.", + "name": "pretty", + "in": "query" + }, + { + "uniqueItems": true, + "type": "string", + "description": "When specified with a watch call, shows changes that occur after that particular version of a resource. Defaults to changes from the beginning of history. When specified for list: - if unset, then the result is returned from remote storage based on quorum-read flag; - if it's 0, then we simply return what we currently have in cache, no guarantee; - if set to non zero, then the result is at least as fresh as given rv.", + "name": "resourceVersion", + "in": "query" + }, + { + "uniqueItems": true, + "type": "integer", + "description": "Timeout for the list/watch call.", + "name": "timeoutSeconds", + "in": "query" + }, + { + "uniqueItems": true, + "type": "boolean", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "name": "watch", + "in": "query" + } + ] + }, "/apis/apiregistration.k8s.io/": { "get": { "description": "get information of a group", @@ -70050,6 +70868,400 @@ } ] }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition": { + "description": "CustomResourceDefinition represents a resource that should be exposed on the API server. Its name MUST be in the format \u003c.spec.name\u003e.\u003c.spec.group\u003e.", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + }, + "spec": { + "description": "Spec describes how the user wants the resources to appear", + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionSpec" + }, + "status": { + "description": "Status indicates the actual state of the CustomResourceDefinition", + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionStatus" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionCondition": { + "description": "CustomResourceDefinitionCondition contains details for the current condition of this pod.", + "required": [ + "type", + "status" + ], + "properties": { + "lastTransitionTime": { + "description": "Last time the condition transitioned from one status to another.", + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + }, + "message": { + "description": "Human-readable message indicating details about last transition.", + "type": "string" + }, + "reason": { + "description": "Unique, one-word, CamelCase reason for the condition's last transition.", + "type": "string" + }, + "status": { + "description": "Status is the status of the condition. Can be True, False, Unknown.", + "type": "string" + }, + "type": { + "description": "Type is the type of the condition.", + "type": "string" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionList": { + "description": "CustomResourceDefinitionList is a list of CustomResourceDefinition objects.", + "required": [ + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", + "type": "string" + }, + "items": { + "description": "Items individual CustomResourceDefinitions", + "type": "array", + "items": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinition" + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionNames": { + "description": "CustomResourceDefinitionNames indicates the names to serve this CustomResourceDefinition", + "required": [ + "plural", + "kind" + ], + "properties": { + "kind": { + "description": "Kind is the serialized kind of the resource. It is normally CamelCase and singular.", + "type": "string" + }, + "listKind": { + "description": "ListKind is the serialized kind of the list for this resource. Defaults to \u003ckind\u003eList.", + "type": "string" + }, + "plural": { + "description": "Plural is the plural name of the resource to serve. It must match the name of the CustomResourceDefinition-registration too: plural.group and it must be all lowercase.", + "type": "string" + }, + "shortNames": { + "description": "ShortNames are short names for the resource. It must be all lowercase.", + "type": "array", + "items": { + "type": "string" + } + }, + "singular": { + "description": "Singular is the singular name of the resource. It must be all lowercase Defaults to lowercased \u003ckind\u003e", + "type": "string" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionSpec": { + "description": "CustomResourceDefinitionSpec describes how a user wants their resource to appear", + "required": [ + "group", + "version", + "names", + "scope" + ], + "properties": { + "group": { + "description": "Group is the group this resource belongs in", + "type": "string" + }, + "names": { + "description": "Names are the names used to describe this custom resource", + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionNames" + }, + "scope": { + "description": "Scope indicates whether this resource is cluster or namespace scoped. Default is namespaced", + "type": "string" + }, + "validation": { + "description": "Validation describes the validation methods for CustomResources This field is alpha-level and should only be sent to servers that enable the CustomResourceValidation feature.", + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation" + }, + "version": { + "description": "Version is the version this resource belongs in", + "type": "string" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionStatus": { + "description": "CustomResourceDefinitionStatus indicates the state of the CustomResourceDefinition", + "required": [ + "conditions", + "acceptedNames" + ], + "properties": { + "acceptedNames": { + "description": "AcceptedNames are the names that are actually being used to serve discovery They may be different than the names in spec.", + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionNames" + }, + "conditions": { + "description": "Conditions indicate state for particular aspects of a CustomResourceDefinition", + "type": "array", + "items": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceDefinitionCondition" + } + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.CustomResourceValidation": { + "description": "CustomResourceValidation is a list of validation methods for CustomResources.", + "properties": { + "openAPIV3Schema": { + "description": "OpenAPIV3Schema is the OpenAPI v3 schema to be validated against.", + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.ExternalDocumentation": { + "description": "ExternalDocumentation allows referencing an external resource for extended documentation.", + "properties": { + "description": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSON": { + "description": "JSON represents any valid JSON value. These types are supported: bool, int64, float64, string, []interface{}, map[string]interface{} and nil.", + "required": [ + "Raw" + ], + "properties": { + "Raw": { + "type": "string", + "format": "byte" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps": { + "description": "JSONSchemaProps is a JSON-Schema following Specification Draft 4 (http://json-schema.org/).", + "properties": { + "$ref": { + "type": "string" + }, + "$schema": { + "type": "string" + }, + "additionalItems": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaPropsOrBool" + }, + "additionalProperties": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaPropsOrBool" + }, + "allOf": { + "type": "array", + "items": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + }, + "anyOf": { + "type": "array", + "items": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + }, + "default": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSON" + }, + "definitions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaPropsOrStringArray" + } + }, + "description": { + "type": "string" + }, + "enum": { + "type": "array", + "items": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSON" + } + }, + "example": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSON" + }, + "exclusiveMaximum": { + "type": "boolean" + }, + "exclusiveMinimum": { + "type": "boolean" + }, + "externalDocs": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.ExternalDocumentation" + }, + "format": { + "type": "string" + }, + "id": { + "type": "string" + }, + "items": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaPropsOrArray" + }, + "maxItems": { + "type": "integer", + "format": "int64" + }, + "maxLength": { + "type": "integer", + "format": "int64" + }, + "maxProperties": { + "type": "integer", + "format": "int64" + }, + "maximum": { + "type": "number", + "format": "double" + }, + "minItems": { + "type": "integer", + "format": "int64" + }, + "minLength": { + "type": "integer", + "format": "int64" + }, + "minProperties": { + "type": "integer", + "format": "int64" + }, + "minimum": { + "type": "number", + "format": "double" + }, + "multipleOf": { + "type": "number", + "format": "double" + }, + "not": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + }, + "pattern": { + "type": "string" + }, + "patternProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "uniqueItems": { + "type": "boolean" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaPropsOrArray": { + "description": "JSONSchemaPropsOrArray represents a value that can either be a JSONSchemaProps or an array of JSONSchemaProps. Mainly here for serialization purposes.", + "required": [ + "Schema", + "JSONSchemas" + ], + "properties": { + "JSONSchemas": { + "type": "array", + "items": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + }, + "Schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaPropsOrBool": { + "description": "JSONSchemaPropsOrBool represents JSONSchemaProps or a boolean value. Defaults to true for the boolean property.", + "required": [ + "Allows", + "Schema" + ], + "properties": { + "Allows": { + "type": "boolean" + }, + "Schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + } + }, + "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaPropsOrStringArray": { + "description": "JSONSchemaPropsOrStringArray represents a JSONSchemaProps or a string array.", + "required": [ + "Schema", + "Property" + ], + "properties": { + "Property": { + "type": "array", + "items": { + "type": "string" + } + }, + "Schema": { + "$ref": "#/definitions/io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps" + } + } + }, "io.k8s.apimachinery.pkg.api.resource.Quantity": { "type": "string" }, diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index 837799fd2f8..53d8b1f415b 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -165,6 +165,9 @@ func CreateServerChain(runOptions *options.ServerRunOptions, stopCh <-chan struc // this wires up openapi kubeAPIServer.GenericAPIServer.PrepareRun() + // This will wire up openapi for extension api server + apiExtensionsServer.GenericAPIServer.PrepareRun() + // aggregator comes last in the chain aggregatorConfig, err := createAggregatorConfig(*kubeAPIServerConfig.GenericConfig, runOptions, versionedInformers, serviceResolver, proxyTransport) if err != nil { diff --git a/pkg/generated/openapi/BUILD b/pkg/generated/openapi/BUILD index 719645b90be..23dfe667f49 100644 --- a/pkg/generated/openapi/BUILD +++ b/pkg/generated/openapi/BUILD @@ -44,6 +44,7 @@ openapi_library( "k8s.io/api/settings/v1alpha1", "k8s.io/api/storage/v1", "k8s.io/api/storage/v1beta1", + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1", "k8s.io/apimachinery/pkg/api/resource", "k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/apis/meta/v1alpha1", diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/doc.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/doc.go index 3976988bf8f..5ef43c18457 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/doc.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/doc.go @@ -20,4 +20,5 @@ limitations under the License. // Package v1beta1 is the v1beta1 version of the API. // +groupName=apiextensions.k8s.io +// +k8s:openapi-gen=true package v1beta1 // import "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" diff --git a/staging/src/k8s.io/apiserver/pkg/server/config.go b/staging/src/k8s.io/apiserver/pkg/server/config.go index d86aa4643df..e1824421221 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/config.go +++ b/staging/src/k8s.io/apiserver/pkg/server/config.go @@ -405,6 +405,7 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G requestContextMapper: c.RequestContextMapper, Serializer: c.Serializer, AuditBackend: c.AuditBackend, + delegationTarget: delegationTarget, minRequestTimeout: time.Duration(c.MinRequestTimeout) * time.Second, diff --git a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go index 4a86448d9da..9be725d4e26 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go +++ b/staging/src/k8s.io/apiserver/pkg/server/genericapiserver.go @@ -145,6 +145,9 @@ type GenericAPIServer struct { // enableAPIResponseCompression indicates whether API Responses should support compression // if the client requests it via Accept-Encoding enableAPIResponseCompression bool + + // delegationTarget is the next delegate in the chain or nil + delegationTarget DelegationTarget } // DelegationTarget is an interface which allows for composition of API servers with top level handling that works @@ -165,6 +168,9 @@ type DelegationTarget interface { // ListedPaths returns the paths for supporting an index ListedPaths() []string + + // NextDelegate returns the next delegationTarget in the chain of delegations + NextDelegate() DelegationTarget } func (s *GenericAPIServer) UnprotectedHandler() http.Handler { @@ -181,6 +187,10 @@ func (s *GenericAPIServer) ListedPaths() []string { return s.listedPathProvider.ListedPaths() } +func (s *GenericAPIServer) NextDelegate() DelegationTarget { + return s.delegationTarget +} + var EmptyDelegate = emptyDelegate{ requestContextMapper: apirequest.NewRequestContextMapper(), } @@ -204,6 +214,9 @@ func (s emptyDelegate) ListedPaths() []string { func (s emptyDelegate) RequestContextMapper() apirequest.RequestContextMapper { return s.requestContextMapper } +func (s emptyDelegate) NextDelegate() DelegationTarget { + return nil +} // RequestContextMapper is exposed so that third party resource storage can be build in a different location. // TODO refactor third party resource storage diff --git a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json index 433daae2052..507a8129352 100644 --- a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json @@ -274,6 +274,10 @@ "ImportPath": "github.com/pkg/errors", "Rev": "a22138067af1c4942683050411a841ade67fe1eb" }, + { + "ImportPath": "github.com/pmezard/go-difflib/difflib", + "Rev": "d8ed2627bdf02c080bf22230dbb337003b7aba2d" + }, { "ImportPath": "github.com/prometheus/client_golang/prometheus", "Rev": "e7e903064f5e9eb5da98208bae10b475d4db0f8c" @@ -310,6 +314,10 @@ "ImportPath": "github.com/spf13/pflag", "Rev": "9ff6c6923cfffbcd502984b8e0c80539a94968b7" }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "f6abca593680b2315d2075e0f5e2a9751e3f431a" + }, { "ImportPath": "github.com/ugorji/go/codec", "Rev": "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74" diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD index ccc0b52b072..2aa58ebed09 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/BUILD @@ -11,11 +11,9 @@ go_test( srcs = [ "handler_apis_test.go", "handler_proxy_test.go", - "openapi_aggregator_test.go", ], library = ":go_default_library", deps = [ - "//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/golang.org/x/net/websocket:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", @@ -37,12 +35,9 @@ go_library( "apiservice_controller.go", "handler_apis.go", "handler_proxy.go", - "openapi_aggregator.go", "resolvers.go", ], deps = [ - "//vendor/github.com/emicklei/go-restful:go_default_library", - "//vendor/github.com/go-openapi/spec:go_default_library", "//vendor/github.com/golang/glog:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", @@ -60,7 +55,6 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", - "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/handlers/responsewriters:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//vendor/k8s.io/apiserver/pkg/features:go_default_library", @@ -84,12 +78,9 @@ go_library( "//vendor/k8s.io/kube-aggregator/pkg/client/informers/internalversion/apiregistration/internalversion:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/client/listers/apiregistration/internalversion:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/controllers:go_default_library", + "//vendor/k8s.io/kube-aggregator/pkg/controllers/openapi:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/controllers/status:go_default_library", "//vendor/k8s.io/kube-aggregator/pkg/registry/apiservice/etcd:go_default_library", - "//vendor/k8s.io/kube-openapi/pkg/aggregator:go_default_library", - "//vendor/k8s.io/kube-openapi/pkg/builder:go_default_library", - "//vendor/k8s.io/kube-openapi/pkg/common:go_default_library", - "//vendor/k8s.io/kube-openapi/pkg/handler:go_default_library", ], ) diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go index 0d72b9e43df..65873c88e0c 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go +++ b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go @@ -17,7 +17,6 @@ limitations under the License. package apiserver import ( - "fmt" "net/http" "time" @@ -27,7 +26,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" genericapirequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/registry/rest" @@ -41,6 +39,7 @@ import ( "k8s.io/kube-aggregator/pkg/client/clientset_generated/internalclientset" informers "k8s.io/kube-aggregator/pkg/client/informers/internalversion" listers "k8s.io/kube-aggregator/pkg/client/listers/apiregistration/internalversion" + openapicontroller "k8s.io/kube-aggregator/pkg/controllers/openapi" statuscontrollers "k8s.io/kube-aggregator/pkg/controllers/status" apiservicestorage "k8s.io/kube-aggregator/pkg/registry/apiservice/etcd" ) @@ -119,7 +118,7 @@ type APIAggregator struct { // Information needed to determine routing for the aggregator serviceResolver ServiceResolver - openAPIAggregator *openAPIAggregator + openAPIAggregationController *openapicontroller.AggregationController } type completedConfig struct { @@ -222,15 +221,22 @@ func (c completedConfig) NewWithDelegate(delegationTarget genericapiserver.Deleg }) if openApiConfig != nil { - s.openAPIAggregator, err = buildAndRegisterOpenAPIAggregator( - s.delegateHandler, + specDownloader := openapicontroller.NewDownloader(s.contextMapper) + openAPIAggregator, err := openapicontroller.BuildAndRegisterAggregator( + &specDownloader, + delegationTarget, s.GenericAPIServer.Handler.GoRestfulContainer.RegisteredWebServices(), openApiConfig, - s.GenericAPIServer.Handler.NonGoRestfulMux, - s.contextMapper) + s.GenericAPIServer.Handler.NonGoRestfulMux) if err != nil { return nil, err } + s.openAPIAggregationController = openapicontroller.NewAggregationController(&specDownloader, openAPIAggregator) + + s.GenericAPIServer.AddPostStartHook("apiservice-openapi-controller", func(context genericapiserver.PostStartHookContext) error { + go s.openAPIAggregationController.Run(context.StopCh) + return nil + }) } return s, nil @@ -243,7 +249,10 @@ func (s *APIAggregator) AddAPIService(apiService *apiregistration.APIService) er // since they are wired against listers because they require multiple resources to respond if proxyHandler, exists := s.proxyHandlers[apiService.Name]; exists { proxyHandler.updateAPIService(apiService) - return s.openAPIAggregator.loadApiServiceSpec(proxyHandler, apiService) + if s.openAPIAggregationController != nil { + s.openAPIAggregationController.UpdateAPIService(proxyHandler, apiService) + } + return nil } proxyPath := "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version @@ -262,8 +271,8 @@ func (s *APIAggregator) AddAPIService(apiService *apiregistration.APIService) er serviceResolver: s.serviceResolver, } proxyHandler.updateAPIService(apiService) - if err := s.openAPIAggregator.loadApiServiceSpec(proxyHandler, apiService); err != nil { - utilruntime.HandleError(fmt.Errorf("unable to load OpenAPI spec for API service %s: %v", apiService.Name, err)) + if s.openAPIAggregationController != nil { + s.openAPIAggregationController.AddAPIService(proxyHandler, apiService) } s.proxyHandlers[apiService.Name] = proxyHandler s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(proxyPath, proxyHandler) @@ -307,6 +316,9 @@ func (s *APIAggregator) RemoveAPIService(apiServiceName string) { } s.GenericAPIServer.Handler.NonGoRestfulMux.Unregister(proxyPath) s.GenericAPIServer.Handler.NonGoRestfulMux.Unregister(proxyPath + "/") + if s.openAPIAggregationController != nil { + s.openAPIAggregationController.RemoveAPIService(apiServiceName) + } delete(s.proxyHandlers, apiServiceName) // TODO unregister group level discovery when there are no more versions for the group diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/openapi_aggregator.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/openapi_aggregator.go deleted file mode 100644 index f5941ec8070..00000000000 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/openapi_aggregator.go +++ /dev/null @@ -1,270 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package apiserver - -import ( - "encoding/json" - "fmt" - "net/http" - "sort" - "time" - - "github.com/emicklei/go-restful" - "github.com/go-openapi/spec" - - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/kube-aggregator/pkg/apis/apiregistration" - "k8s.io/kube-openapi/pkg/aggregator" - "k8s.io/kube-openapi/pkg/builder" - "k8s.io/kube-openapi/pkg/common" - "k8s.io/kube-openapi/pkg/handler" -) - -const ( - aggregatorUser = "system:aggregator" - specDownloadTimeout = 60 * time.Second -) - -type openAPIAggregator struct { - // Map of API Services' OpenAPI specs by their name - openAPISpecs map[string]*openAPISpecInfo - - // provided for dynamic OpenAPI spec - openAPIService *handler.OpenAPIService - - // Aggregator's OpenAPI spec (holds apiregistration group). - aggregatorOpenAPISpec *spec.Swagger - - // Local (in process) delegate's OpenAPI spec. - inProcessDelegatesOpenAPISpec *spec.Swagger - - contextMapper request.RequestContextMapper -} - -func buildAndRegisterOpenAPIAggregator(delegateHandler http.Handler, webServices []*restful.WebService, config *common.Config, pathHandler common.PathHandler, contextMapper request.RequestContextMapper) (s *openAPIAggregator, err error) { - s = &openAPIAggregator{ - openAPISpecs: map[string]*openAPISpecInfo{}, - contextMapper: contextMapper, - } - - // Get Local delegate's Spec - s.inProcessDelegatesOpenAPISpec, err = s.downloadOpenAPISpec(delegateHandler) - if err != nil { - return nil, err - } - - // Build Aggregator's spec - s.aggregatorOpenAPISpec, err = builder.BuildOpenAPISpec( - webServices, config) - if err != nil { - return nil, err - } - // Remove any non-API endpoints from aggregator's spec. aggregatorOpenAPISpec - // is the source of truth for all non-api endpoints. - aggregator.FilterSpecByPaths(s.aggregatorOpenAPISpec, []string{"/apis/"}) - - // Build initial spec to serve. - specToServe, err := s.buildOpenAPISpec() - if err != nil { - return nil, err - } - - // Install handler - s.openAPIService, err = handler.RegisterOpenAPIService( - specToServe, "/swagger.json", pathHandler) - if err != nil { - return nil, err - } - - return s, nil -} - -// openAPISpecInfo is used to store OpenAPI spec with its priority. -// It can be used to sort specs with their priorities. -type openAPISpecInfo struct { - apiService apiregistration.APIService - spec *spec.Swagger -} - -// byPriority can be used in sort.Sort to sort specs with their priorities. -type byPriority struct { - specs []openAPISpecInfo - groupPriorities map[string]int32 -} - -func (a byPriority) Len() int { return len(a.specs) } -func (a byPriority) Swap(i, j int) { a.specs[i], a.specs[j] = a.specs[j], a.specs[i] } -func (a byPriority) Less(i, j int) bool { - var iPriority, jPriority int32 - if a.specs[i].apiService.Spec.Group == a.specs[j].apiService.Spec.Group { - iPriority = a.specs[i].apiService.Spec.VersionPriority - jPriority = a.specs[i].apiService.Spec.VersionPriority - } else { - iPriority = a.groupPriorities[a.specs[i].apiService.Spec.Group] - jPriority = a.groupPriorities[a.specs[j].apiService.Spec.Group] - } - if iPriority != jPriority { - // Sort by priority, higher first - return iPriority > jPriority - } - // Sort by service name. - return a.specs[i].apiService.Name < a.specs[j].apiService.Name -} - -func sortByPriority(specs []openAPISpecInfo) { - b := byPriority{ - specs: specs, - groupPriorities: map[string]int32{}, - } - for _, spec := range specs { - if pr, found := b.groupPriorities[spec.apiService.Spec.Group]; !found || spec.apiService.Spec.GroupPriorityMinimum > pr { - b.groupPriorities[spec.apiService.Spec.Group] = spec.apiService.Spec.GroupPriorityMinimum - } - } - sort.Sort(b) -} - -// buildOpenAPISpec aggregates all OpenAPI specs. It is not thread-safe. -func (s *openAPIAggregator) buildOpenAPISpec() (specToReturn *spec.Swagger, err error) { - specToReturn, err = aggregator.CloneSpec(s.inProcessDelegatesOpenAPISpec) - if err != nil { - return nil, err - } - if err := aggregator.MergeSpecs(specToReturn, s.aggregatorOpenAPISpec); err != nil { - return nil, fmt.Errorf("cannot merge local delegate spec with aggregator spec: %s", err.Error()) - } - specs := []openAPISpecInfo{} - for _, specInfo := range s.openAPISpecs { - specs = append(specs, openAPISpecInfo{specInfo.apiService, specInfo.spec}) - } - sortByPriority(specs) - for _, specInfo := range specs { - if err := aggregator.MergeSpecs(specToReturn, specInfo.spec); err != nil { - return nil, err - } - } - return specToReturn, nil -} - -// updateOpenAPISpec aggregates all OpenAPI specs. It is not thread-safe. -func (s *openAPIAggregator) updateOpenAPISpec() error { - if s.openAPIService == nil { - return nil - } - specToServe, err := s.buildOpenAPISpec() - if err != nil { - return err - } - return s.openAPIService.UpdateSpec(specToServe) -} - -// inMemoryResponseWriter is a http.Writer that keep the response in memory. -type inMemoryResponseWriter struct { - header http.Header - respCode int - data []byte -} - -func newInMemoryResponseWriter() *inMemoryResponseWriter { - return &inMemoryResponseWriter{header: http.Header{}} -} - -func (r *inMemoryResponseWriter) Header() http.Header { - return r.header -} - -func (r *inMemoryResponseWriter) WriteHeader(code int) { - r.respCode = code -} - -func (r *inMemoryResponseWriter) Write(in []byte) (int, error) { - r.data = append(r.data, in...) - return len(in), nil -} - -func (r *inMemoryResponseWriter) String() string { - s := fmt.Sprintf("ResponseCode: %d", r.respCode) - if r.data != nil { - s += fmt.Sprintf(", Body: %s", string(r.data)) - } - if r.header != nil { - s += fmt.Sprintf(", Header: %s", r.header) - } - return s -} - -func (s *openAPIAggregator) handlerWithUser(handler http.Handler, info user.Info) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if ctx, ok := s.contextMapper.Get(req); ok { - s.contextMapper.Update(req, request.WithUser(ctx, info)) - } - handler.ServeHTTP(w, req) - }) -} - -// downloadOpenAPISpec downloads openAPI spec from /swagger.json endpoint of the given handler. -func (s *openAPIAggregator) downloadOpenAPISpec(handler http.Handler) (*spec.Swagger, error) { - handler = s.handlerWithUser(handler, &user.DefaultInfo{Name: aggregatorUser}) - handler = request.WithRequestContext(handler, s.contextMapper) - handler = http.TimeoutHandler(handler, specDownloadTimeout, "request timed out") - - req, err := http.NewRequest("GET", "/swagger.json", nil) - if err != nil { - return nil, err - } - writer := newInMemoryResponseWriter() - handler.ServeHTTP(writer, req) - - switch writer.respCode { - case http.StatusOK: - openApiSpec := &spec.Swagger{} - if err := json.Unmarshal(writer.data, openApiSpec); err != nil { - return nil, err - } - return openApiSpec, nil - default: - return nil, fmt.Errorf("failed to retrive openAPI spec, http error: %s", writer.String()) - } -} - -// loadApiServiceSpec loads OpenAPI spec for the given API Service and then updates aggregator's spec. -func (s *openAPIAggregator) loadApiServiceSpec(handler http.Handler, apiService *apiregistration.APIService) error { - - // Ignore local services - if apiService.Spec.Service == nil { - return nil - } - - openApiSpec, err := s.downloadOpenAPISpec(handler) - if err != nil { - return err - } - aggregator.FilterSpecByPaths(openApiSpec, []string{"/apis/" + apiService.Spec.Group + "/"}) - - s.openAPISpecs[apiService.Name] = &openAPISpecInfo{ - apiService: *apiService, - spec: openApiSpec, - } - - err = s.updateOpenAPISpec() - if err != nil { - delete(s.openAPISpecs, apiService.Name) - return err - } - return nil -} diff --git a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/openapi_aggregator_test.go b/staging/src/k8s.io/kube-aggregator/pkg/apiserver/openapi_aggregator_test.go deleted file mode 100644 index 40130b88d80..00000000000 --- a/staging/src/k8s.io/kube-aggregator/pkg/apiserver/openapi_aggregator_test.go +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright 2017 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package apiserver - -import ( - "reflect" - "testing" - - "github.com/go-openapi/spec" - - "k8s.io/kube-aggregator/pkg/apis/apiregistration" -) - -func newApiServiceForTest(name, group string, minGroupPriority, versionPriority int32) apiregistration.APIService { - r := apiregistration.APIService{} - r.Spec.Group = group - r.Spec.GroupPriorityMinimum = minGroupPriority - r.Spec.VersionPriority = versionPriority - r.Name = name - return r -} - -func assertSortedServices(t *testing.T, actual []openAPISpecInfo, expectedNames []string) { - actualNames := []string{} - for _, a := range actual { - actualNames = append(actualNames, a.apiService.Name) - } - if !reflect.DeepEqual(actualNames, expectedNames) { - t.Errorf("Expected %s got %s.", expectedNames, actualNames) - } -} - -func TestApiServiceSort(t *testing.T) { - list := []openAPISpecInfo{ - { - apiService: newApiServiceForTest("FirstService", "Group1", 10, 5), - spec: &spec.Swagger{}, - }, - { - apiService: newApiServiceForTest("SecondService", "Group2", 15, 3), - spec: &spec.Swagger{}, - }, - { - apiService: newApiServiceForTest("FirstServiceInternal", "Group1", 16, 3), - spec: &spec.Swagger{}, - }, - { - apiService: newApiServiceForTest("ThirdService", "Group3", 15, 3), - spec: &spec.Swagger{}, - }, - } - sortByPriority(list) - assertSortedServices(t, list, []string{"FirstService", "FirstServiceInternal", "SecondService", "ThirdService"}) -} diff --git a/staging/src/k8s.io/kube-aggregator/pkg/controllers/BUILD b/staging/src/k8s.io/kube-aggregator/pkg/controllers/BUILD index 9644e6dc03b..ed9d7227bbd 100644 --- a/staging/src/k8s.io/kube-aggregator/pkg/controllers/BUILD +++ b/staging/src/k8s.io/kube-aggregator/pkg/controllers/BUILD @@ -27,6 +27,7 @@ filegroup( srcs = [ ":package-srcs", "//staging/src/k8s.io/kube-aggregator/pkg/controllers/autoregister:all-srcs", + "//staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi:all-srcs", "//staging/src/k8s.io/kube-aggregator/pkg/controllers/status:all-srcs", ], tags = ["automanaged"], diff --git a/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/BUILD b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/BUILD new file mode 100644 index 00000000000..b45e675eaa4 --- /dev/null +++ b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/BUILD @@ -0,0 +1,53 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "aggregator.go", + "controller.go", + "downloader.go", + ], + visibility = ["//visibility:public"], + deps = [ + "//vendor/github.com/emicklei/go-restful:go_default_library", + "//vendor/github.com/go-openapi/spec:go_default_library", + "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//vendor/k8s.io/apiserver/pkg/server:go_default_library", + "//vendor/k8s.io/client-go/util/workqueue:go_default_library", + "//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/aggregator:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/builder:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/common:go_default_library", + "//vendor/k8s.io/kube-openapi/pkg/handler:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["aggregator_test.go"], + library = ":go_default_library", + deps = [ + "//vendor/github.com/go-openapi/spec:go_default_library", + "//vendor/github.com/stretchr/testify/assert:go_default_library", + "//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library", + "//vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/aggregator.go b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/aggregator.go new file mode 100644 index 00000000000..c32da4cdd79 --- /dev/null +++ b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/aggregator.go @@ -0,0 +1,318 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "fmt" + "net/http" + "sort" + "sync" + "time" + + "github.com/emicklei/go-restful" + "github.com/go-openapi/spec" + + "k8s.io/apiserver/pkg/server" + "k8s.io/kube-aggregator/pkg/apis/apiregistration" + "k8s.io/kube-openapi/pkg/aggregator" + "k8s.io/kube-openapi/pkg/builder" + "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/handler" +) + +const ( + aggregatorUser = "system:aggregator" + specDownloadTimeout = 60 * time.Second + localDelegateChainNamePattern = "k8s_internal_local_delegation_chain_%010d" + + // A randomly generated UUID to differentiate local and remote eTags. + locallyGeneratedEtagPrefix = "\"6E8F849B434D4B98A569B9D7718876E9-" +) + +type specAggregator struct { + // mutex protects all members of this struct. + rwMutex sync.RWMutex + + // Map of API Services' OpenAPI specs by their name + openAPISpecs map[string]*openAPISpecInfo + + // provided for dynamic OpenAPI spec + openAPIService *handler.OpenAPIService +} + +var _ AggregationManager = &specAggregator{} + +// This function is not thread safe as it only being called on startup. +func (s *specAggregator) addLocalSpec(spec *spec.Swagger, localHandler http.Handler, name, etag string) { + localAPIService := apiregistration.APIService{} + localAPIService.Name = name + s.openAPISpecs[name] = &openAPISpecInfo{ + etag: etag, + apiService: localAPIService, + handler: localHandler, + spec: spec, + } +} + +// BuildAndRegisterAggregator registered OpenAPI aggregator handler. This function is not thread safe as it only being called on startup. +func BuildAndRegisterAggregator(downloader *Downloader, delegationTarget server.DelegationTarget, webServices []*restful.WebService, + config *common.Config, pathHandler common.PathHandler) (AggregationManager, error) { + s := &specAggregator{ + openAPISpecs: map[string]*openAPISpecInfo{}, + } + + i := 0 + // Build Aggregator's spec + aggregatorOpenAPISpec, err := builder.BuildOpenAPISpec( + webServices, config) + if err != nil { + return nil, err + } + + // Reserving non-name spec for aggregator's Spec. + s.addLocalSpec(aggregatorOpenAPISpec, nil, fmt.Sprintf(localDelegateChainNamePattern, i), "") + i++ + for delegate := delegationTarget; delegate != nil; delegate = delegate.NextDelegate() { + handler := delegate.UnprotectedHandler() + if handler == nil { + continue + } + delegateSpec, etag, _, err := downloader.Download(handler, "") + if err != nil { + return nil, err + } + if delegateSpec == nil { + continue + } + s.addLocalSpec(delegateSpec, handler, fmt.Sprintf(localDelegateChainNamePattern, i), etag) + i++ + } + + // Build initial spec to serve. + specToServe, err := s.buildOpenAPISpec() + if err != nil { + return nil, err + } + + // Install handler + s.openAPIService, err = handler.RegisterOpenAPIService( + specToServe, "/swagger.json", pathHandler) + if err != nil { + return nil, err + } + + return s, nil +} + +// openAPISpecInfo is used to store OpenAPI spec with its priority. +// It can be used to sort specs with their priorities. +type openAPISpecInfo struct { + apiService apiregistration.APIService + + // Specification of this API Service. If null then the spec is not loaded yet. + spec *spec.Swagger + handler http.Handler + etag string +} + +// byPriority can be used in sort.Sort to sort specs with their priorities. +type byPriority struct { + specs []openAPISpecInfo + groupPriorities map[string]int32 +} + +func (a byPriority) Len() int { return len(a.specs) } +func (a byPriority) Swap(i, j int) { a.specs[i], a.specs[j] = a.specs[j], a.specs[i] } +func (a byPriority) Less(i, j int) bool { + // All local specs will come first + // WARNING: This will result in not following priorities for local APIServices. + if a.specs[i].apiService.Spec.Service == nil { + // Sort local specs with their name. This is the order in the delegation chain (aggregator first). + return a.specs[i].apiService.Name < a.specs[j].apiService.Name + } + var iPriority, jPriority int32 + if a.specs[i].apiService.Spec.Group == a.specs[j].apiService.Spec.Group { + iPriority = a.specs[i].apiService.Spec.VersionPriority + jPriority = a.specs[i].apiService.Spec.VersionPriority + } else { + iPriority = a.groupPriorities[a.specs[i].apiService.Spec.Group] + jPriority = a.groupPriorities[a.specs[j].apiService.Spec.Group] + } + if iPriority != jPriority { + // Sort by priority, higher first + return iPriority > jPriority + } + // Sort by service name. + return a.specs[i].apiService.Name < a.specs[j].apiService.Name +} + +func sortByPriority(specs []openAPISpecInfo) { + b := byPriority{ + specs: specs, + groupPriorities: map[string]int32{}, + } + for _, spec := range specs { + if spec.apiService.Spec.Service == nil { + continue + } + if pr, found := b.groupPriorities[spec.apiService.Spec.Group]; !found || spec.apiService.Spec.GroupPriorityMinimum > pr { + b.groupPriorities[spec.apiService.Spec.Group] = spec.apiService.Spec.GroupPriorityMinimum + } + } + sort.Sort(b) +} + +// buildOpenAPISpec aggregates all OpenAPI specs. It is not thread-safe. The caller is responsible to hold proper locks. +func (s *specAggregator) buildOpenAPISpec() (specToReturn *spec.Swagger, err error) { + specs := []openAPISpecInfo{} + for _, specInfo := range s.openAPISpecs { + if specInfo.spec == nil { + continue + } + specs = append(specs, *specInfo) + } + if len(specs) == 0 { + return &spec.Swagger{}, nil + } + sortByPriority(specs) + for _, specInfo := range specs { + // TODO: Make kube-openapi.MergeSpec(s) accept nil or empty spec as destination and just clone the spec in that case. + if specToReturn == nil { + specToReturn, err = aggregator.CloneSpec(specInfo.spec) + if err != nil { + return nil, err + } + continue + } + if err := aggregator.MergeSpecsIgnorePathConflict(specToReturn, specInfo.spec); err != nil { + return nil, err + } + } + return specToReturn, nil +} + +// updateOpenAPISpec aggregates all OpenAPI specs. It is not thread-safe. The caller is responsible to hold proper locks. +func (s *specAggregator) updateOpenAPISpec() error { + if s.openAPIService == nil { + return nil + } + specToServe, err := s.buildOpenAPISpec() + if err != nil { + return err + } + return s.openAPIService.UpdateSpec(specToServe) +} + +// tryUpdatingServiceSpecs tries updating openAPISpecs map with specified specInfo, and keeps the map intact +// if the update fails. +func (s *specAggregator) tryUpdatingServiceSpecs(specInfo *openAPISpecInfo) error { + orgSpecInfo, exists := s.openAPISpecs[specInfo.apiService.Name] + s.openAPISpecs[specInfo.apiService.Name] = specInfo + if err := s.updateOpenAPISpec(); err != nil { + if exists { + s.openAPISpecs[specInfo.apiService.Name] = orgSpecInfo + } else { + delete(s.openAPISpecs, specInfo.apiService.Name) + } + return err + } + return nil +} + +// tryDeleteServiceSpecs tries delete specified specInfo from openAPISpecs map, and keeps the map intact +// if the update fails. +func (s *specAggregator) tryDeleteServiceSpecs(apiServiceName string) error { + orgSpecInfo, exists := s.openAPISpecs[apiServiceName] + if !exists { + return nil + } + delete(s.openAPISpecs, apiServiceName) + if err := s.updateOpenAPISpec(); err != nil { + s.openAPISpecs[apiServiceName] = orgSpecInfo + return err + } + return nil +} + +// UpdateAPIServiceSpec updates the api service's OpenAPI spec. It is thread safe. +func (s *specAggregator) UpdateAPIServiceSpec(apiServiceName string, spec *spec.Swagger, etag string) error { + s.rwMutex.Lock() + defer s.rwMutex.Unlock() + + specInfo, existingService := s.openAPISpecs[apiServiceName] + if !existingService { + return fmt.Errorf("APIService %q does not exists", apiServiceName) + } + + // For APIServices (non-local) specs, only merge their /apis/ prefixed endpoint as it is the only paths + // proxy handler delegates. + if specInfo.apiService.Spec.Service != nil { + aggregator.FilterSpecByPaths(spec, []string{"/apis/"}) + } + + return s.tryUpdatingServiceSpecs(&openAPISpecInfo{ + apiService: specInfo.apiService, + spec: spec, + handler: specInfo.handler, + etag: etag, + }) +} + +// AddUpdateAPIService adds or updates the api service. It is thread safe. +func (s *specAggregator) AddUpdateAPIService(handler http.Handler, apiService *apiregistration.APIService) error { + s.rwMutex.Lock() + defer s.rwMutex.Unlock() + + if apiService.Spec.Service == nil { + // All local specs should be already aggregated using local delegate chain + return nil + } + + newSpec := &openAPISpecInfo{ + apiService: *apiService, + handler: handler, + } + if specInfo, existingService := s.openAPISpecs[apiService.Name]; existingService { + newSpec.etag = specInfo.etag + newSpec.spec = specInfo.spec + } + return s.tryUpdatingServiceSpecs(newSpec) +} + +// RemoveAPIServiceSpec removes an api service from OpenAPI aggregation. If it does not exist, no error is returned. +// It is thread safe. +func (s *specAggregator) RemoveAPIServiceSpec(apiServiceName string) error { + s.rwMutex.Lock() + defer s.rwMutex.Unlock() + + if _, existingService := s.openAPISpecs[apiServiceName]; !existingService { + return nil + } + + return s.tryDeleteServiceSpecs(apiServiceName) +} + +// GetAPIServiceSpec returns api service spec info +func (s *specAggregator) GetAPIServiceInfo(apiServiceName string) (handler http.Handler, etag string, exists bool) { + s.rwMutex.RLock() + defer s.rwMutex.RUnlock() + + if info, existingService := s.openAPISpecs[apiServiceName]; existingService { + return info.handler, info.etag, true + } + return nil, "", false +} diff --git a/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/aggregator_test.go b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/aggregator_test.go new file mode 100644 index 00000000000..4bebfe506b6 --- /dev/null +++ b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/aggregator_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/go-openapi/spec" + "github.com/stretchr/testify/assert" + + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/kube-aggregator/pkg/apis/apiregistration" +) + +func newAPIServiceForTest(name, group string, minGroupPriority, versionPriority int32) apiregistration.APIService { + r := apiregistration.APIService{} + r.Spec.Group = group + r.Spec.GroupPriorityMinimum = minGroupPriority + r.Spec.VersionPriority = versionPriority + r.Spec.Service = &apiregistration.ServiceReference{} + r.Name = name + return r +} + +func assertSortedServices(t *testing.T, actual []openAPISpecInfo, expectedNames []string) { + actualNames := []string{} + for _, a := range actual { + actualNames = append(actualNames, a.apiService.Name) + } + if !reflect.DeepEqual(actualNames, expectedNames) { + t.Errorf("Expected %s got %s.", expectedNames, actualNames) + } +} + +func TestAPIServiceSort(t *testing.T) { + list := []openAPISpecInfo{ + { + apiService: newAPIServiceForTest("FirstService", "Group1", 10, 5), + spec: &spec.Swagger{}, + }, + { + apiService: newAPIServiceForTest("SecondService", "Group2", 15, 3), + spec: &spec.Swagger{}, + }, + { + apiService: newAPIServiceForTest("FirstServiceInternal", "Group1", 16, 3), + spec: &spec.Swagger{}, + }, + { + apiService: newAPIServiceForTest("ThirdService", "Group3", 15, 3), + spec: &spec.Swagger{}, + }, + } + sortByPriority(list) + assertSortedServices(t, list, []string{"FirstService", "FirstServiceInternal", "SecondService", "ThirdService"}) +} + +type handlerTest struct { + etag string + data []byte +} + +var _ http.Handler = handlerTest{} + +func (h handlerTest) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if len(h.etag) > 0 { + w.Header().Add("Etag", h.etag) + } + ifNoneMatches := r.Header["If-None-Match"] + for _, match := range ifNoneMatches { + if match == h.etag { + w.WriteHeader(http.StatusNotModified) + return + } + } + w.Write(h.data) +} + +func assertDownloadedSpec(actualSpec *spec.Swagger, actualEtag string, err error, + expectedSpecID string, expectedEtag string) error { + if err != nil { + return fmt.Errorf("downloadOpenAPISpec failed : %s", err) + } + if expectedSpecID == "" && actualSpec != nil { + return fmt.Errorf("expected Not Modified, actual ID %s", actualSpec.ID) + } + if actualSpec != nil && actualSpec.ID != expectedSpecID { + return fmt.Errorf("expected ID %s, actual ID %s", expectedSpecID, actualSpec.ID) + } + if actualEtag != expectedEtag { + return fmt.Errorf("expected ETag '%s', actual ETag '%s'", expectedEtag, actualEtag) + } + return nil +} + +func TestDownloadOpenAPISpec(t *testing.T) { + + s := Downloader{contextMapper: request.NewRequestContextMapper()} + + // Test with no eTag + actualSpec, actualEtag, _, err := s.Download(handlerTest{data: []byte("{\"id\": \"test\"}")}, "") + assert.NoError(t, assertDownloadedSpec(actualSpec, actualEtag, err, "test", "\"6E8F849B434D4B98A569B9D7718876E9-356ECAB19D7FBE1336BABB1E70F8F3025050DE218BE78256BE81620681CFC9A268508E542B8B55974E17B2184BBFC8FFFAA577E51BE195D32B3CA2547818ABE4\"")) + + // Test with eTag + actualSpec, actualEtag, _, err = s.Download( + handlerTest{data: []byte("{\"id\": \"test\"}"), etag: "etag_test"}, "") + assert.NoError(t, assertDownloadedSpec(actualSpec, actualEtag, err, "test", "etag_test")) + + // Test not modified + actualSpec, actualEtag, _, err = s.Download( + handlerTest{data: []byte("{\"id\": \"test\"}"), etag: "etag_test"}, "etag_test") + assert.NoError(t, assertDownloadedSpec(actualSpec, actualEtag, err, "", "etag_test")) + + // Test different eTags + actualSpec, actualEtag, _, err = s.Download( + handlerTest{data: []byte("{\"id\": \"test\"}"), etag: "etag_test1"}, "etag_test2") + assert.NoError(t, assertDownloadedSpec(actualSpec, actualEtag, err, "test", "etag_test1")) +} diff --git a/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/controller.go b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/controller.go new file mode 100644 index 00000000000..c38350ddbbe --- /dev/null +++ b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/controller.go @@ -0,0 +1,186 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-openapi/spec" + "github.com/golang/glog" + + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/workqueue" + "k8s.io/kube-aggregator/pkg/apis/apiregistration" +) + +const ( + successfulUpdateDelay = time.Minute + failedUpdateMaxExpDelay = time.Hour +) + +type syncAction int + +const ( + syncRequeue syncAction = iota + syncRequeueRateLimited + syncNothing +) + +// AggregationManager is the interface between this controller and OpenAPI Aggregator service. +type AggregationManager interface { + AddUpdateAPIService(handler http.Handler, apiService *apiregistration.APIService) error + UpdateAPIServiceSpec(apiServiceName string, spec *spec.Swagger, etag string) error + RemoveAPIServiceSpec(apiServiceName string) error + GetAPIServiceInfo(apiServiceName string) (handler http.Handler, etag string, exists bool) +} + +// AggregationController periodically check for changes in OpenAPI specs of APIServices and update/remove +// them if necessary. +type AggregationController struct { + openAPIAggregationManager AggregationManager + queue workqueue.RateLimitingInterface + downloader *Downloader + + // To allow injection for testing. + syncHandler func(key string) (syncAction, error) +} + +// NewAggregationController creates new OpenAPI aggregation controller. +func NewAggregationController(downloader *Downloader, openAPIAggregationManager AggregationManager) *AggregationController { + c := &AggregationController{ + openAPIAggregationManager: openAPIAggregationManager, + queue: workqueue.NewNamedRateLimitingQueue( + workqueue.NewItemExponentialFailureRateLimiter(successfulUpdateDelay, failedUpdateMaxExpDelay), "APIServiceOpenAPIAggregationControllerQueue1"), + downloader: downloader, + } + + c.syncHandler = c.sync + + return c +} + +// Run starts OpenAPI AggregationController +func (c *AggregationController) Run(stopCh <-chan struct{}) { + defer utilruntime.HandleCrash() + defer c.queue.ShutDown() + + glog.Infof("Starting OpenAPI AggregationController") + defer glog.Infof("Shutting down OpenAPI AggregationController") + + go wait.Until(c.runWorker, time.Second, stopCh) + + <-stopCh +} + +func (c *AggregationController) runWorker() { + for c.processNextWorkItem() { + } +} + +// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit. +func (c *AggregationController) processNextWorkItem() bool { + key, quit := c.queue.Get() + defer c.queue.Done(key) + if quit { + return false + } + + glog.Infof("OpenAPI AggregationController: Processing item %s", key) + + action, err := c.syncHandler(key.(string)) + if err == nil { + c.queue.Forget(key) + } else { + utilruntime.HandleError(fmt.Errorf("loading OpenAPI spec for %q failed with: %v", key, err)) + } + + switch action { + case syncRequeue: + glog.Infof("OpenAPI AggregationController: action for item %s: Requeue.", key) + c.queue.AddAfter(key, successfulUpdateDelay) + case syncRequeueRateLimited: + glog.Infof("OpenAPI AggregationController: action for item %s: Rate Limited Requeue.", key) + c.queue.AddRateLimited(key) + case syncNothing: + glog.Infof("OpenAPI AggregationController: action for item %s: Nothing (removed from the queue).", key) + } + + return true +} + +func (c *AggregationController) sync(key string) (syncAction, error) { + handler, etag, exists := c.openAPIAggregationManager.GetAPIServiceInfo(key) + if !exists || handler == nil { + return syncNothing, nil + } + returnSpec, newEtag, httpStatus, err := c.downloader.Download(handler, etag) + switch { + case err != nil: + return syncRequeueRateLimited, err + case httpStatus == http.StatusNotModified: + case httpStatus == http.StatusNotFound || returnSpec == nil: + return syncRequeueRateLimited, fmt.Errorf("OpenAPI spec does not exists") + case httpStatus == http.StatusOK: + if err := c.openAPIAggregationManager.UpdateAPIServiceSpec(key, returnSpec, newEtag); err != nil { + return syncRequeueRateLimited, err + } + } + return syncRequeue, nil +} + +// AddAPIService adds a new API Service to OpenAPI Aggregation. +func (c *AggregationController) AddAPIService(handler http.Handler, apiService *apiregistration.APIService) { + if apiService.Spec.Service == nil { + return + } + if err := c.openAPIAggregationManager.AddUpdateAPIService(handler, apiService); err != nil { + utilruntime.HandleError(fmt.Errorf("adding %q to AggregationController failed with: %v", apiService.Name, err)) + } + c.queue.AddAfter(apiService.Name, time.Second) +} + +// UpdateAPIService updates API Service's info and handler. +func (c *AggregationController) UpdateAPIService(handler http.Handler, apiService *apiregistration.APIService) { + if apiService.Spec.Service == nil { + return + } + if err := c.openAPIAggregationManager.AddUpdateAPIService(handler, apiService); err != nil { + utilruntime.HandleError(fmt.Errorf("updating %q to AggregationController failed with: %v", apiService.Name, err)) + } + key := apiService.Name + if c.queue.NumRequeues(key) > 0 { + // The item has failed before. Remove it from failure queue and + // update it in a second + c.queue.Forget(key) + c.queue.AddAfter(key, time.Second) + } + // Else: The item has been succeeded before and it will be updated soon (after successfulUpdateDelay) + // we don't add it again as it will cause a duplication of items. +} + +// RemoveAPIService removes API Service from OpenAPI Aggregation Controller. +func (c *AggregationController) RemoveAPIService(apiServiceName string) { + if err := c.openAPIAggregationManager.RemoveAPIServiceSpec(apiServiceName); err != nil { + utilruntime.HandleError(fmt.Errorf("removing %q from AggregationController failed with: %v", apiServiceName, err)) + } + // This will only remove it if it was failing before. If it was successful, processNextWorkItem will figure it out + // and will not add it again to the queue. + c.queue.Forget(apiServiceName) +} diff --git a/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/downloader.go b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/downloader.go new file mode 100644 index 00000000000..0da4e7e99af --- /dev/null +++ b/staging/src/k8s.io/kube-aggregator/pkg/controllers/openapi/downloader.go @@ -0,0 +1,146 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openapi + +import ( + "crypto/sha512" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/go-openapi/spec" + + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" +) + +// Downloader is the OpenAPI downloader type. It will try to download spec from /swagger.json endpoint. +type Downloader struct { + contextMapper request.RequestContextMapper +} + +// NewDownloader creates a new OpenAPI Downloader. +func NewDownloader(contextMapper request.RequestContextMapper) Downloader { + return Downloader{contextMapper} +} + +// inMemoryResponseWriter is a http.Writer that keep the response in memory. +type inMemoryResponseWriter struct { + writeHeaderCalled bool + header http.Header + respCode int + data []byte +} + +func newInMemoryResponseWriter() *inMemoryResponseWriter { + return &inMemoryResponseWriter{header: http.Header{}} +} + +func (r *inMemoryResponseWriter) Header() http.Header { + return r.header +} + +func (r *inMemoryResponseWriter) WriteHeader(code int) { + r.writeHeaderCalled = true + r.respCode = code +} + +func (r *inMemoryResponseWriter) Write(in []byte) (int, error) { + if !r.writeHeaderCalled { + r.WriteHeader(http.StatusOK) + } + r.data = append(r.data, in...) + return len(in), nil +} + +func (r *inMemoryResponseWriter) String() string { + s := fmt.Sprintf("ResponseCode: %d", r.respCode) + if r.data != nil { + s += fmt.Sprintf(", Body: %s", string(r.data)) + } + if r.header != nil { + s += fmt.Sprintf(", Header: %s", r.header) + } + return s +} + +func (s *Downloader) handlerWithUser(handler http.Handler, info user.Info) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if ctx, ok := s.contextMapper.Get(req); ok { + s.contextMapper.Update(req, request.WithUser(ctx, info)) + } + handler.ServeHTTP(w, req) + }) +} + +func etagFor(data []byte) string { + return fmt.Sprintf("%s%X\"", locallyGeneratedEtagPrefix, sha512.Sum512(data)) +} + +// Download downloads openAPI spec from /swagger.json endpoint of the given handler. +// httpStatus is only valid if err == nil +func (s *Downloader) Download(handler http.Handler, etag string) (returnSpec *spec.Swagger, newEtag string, httpStatus int, err error) { + handler = s.handlerWithUser(handler, &user.DefaultInfo{Name: aggregatorUser}) + handler = request.WithRequestContext(handler, s.contextMapper) + handler = http.TimeoutHandler(handler, specDownloadTimeout, "request timed out") + + req, err := http.NewRequest("GET", "/swagger.json", nil) + if err != nil { + return nil, "", 0, err + } + + // Only pass eTag if it is not generated locally + if len(etag) > 0 && !strings.HasPrefix(etag, locallyGeneratedEtagPrefix) { + req.Header.Add("If-None-Match", etag) + } + + writer := newInMemoryResponseWriter() + handler.ServeHTTP(writer, req) + + switch writer.respCode { + case http.StatusNotModified: + if len(etag) == 0 { + return nil, etag, http.StatusNotModified, fmt.Errorf("http.StatusNotModified is not allowed in absence of etag") + } + return nil, etag, http.StatusNotModified, nil + case http.StatusNotFound: + // Gracefully skip 404, assuming the server won't provide any spec + return nil, "", http.StatusNotFound, nil + case http.StatusOK: + openAPISpec := &spec.Swagger{} + if err := json.Unmarshal(writer.data, openAPISpec); err != nil { + return nil, "", 0, err + } + newEtag = writer.Header().Get("Etag") + if len(newEtag) == 0 { + newEtag = etagFor(writer.data) + if len(etag) > 0 && strings.HasPrefix(etag, locallyGeneratedEtagPrefix) { + // The function call with an etag and server does not report an etag. + // That means this server does not support etag and the etag that passed + // to the function generated previously by us. Just compare etags and + // return StatusNotModified if they are the same. + if etag == newEtag { + return nil, etag, http.StatusNotModified, nil + } + } + } + return openAPISpec, newEtag, http.StatusOK, nil + default: + return nil, "", 0, fmt.Errorf("failed to retrieve openAPI spec, http error: %s", writer.String()) + } +} diff --git a/vendor/k8s.io/kube-openapi/pkg/aggregator/aggregator.go b/vendor/k8s.io/kube-openapi/pkg/aggregator/aggregator.go index 15f0830f5c6..aceaa463560 100644 --- a/vendor/k8s.io/kube-openapi/pkg/aggregator/aggregator.go +++ b/vendor/k8s.io/kube-openapi/pkg/aggregator/aggregator.go @@ -154,8 +154,28 @@ func (s *referenceWalker) Start() { } } -// FilterSpecByPaths remove unnecessary paths and unused definitions. +// usedDefinitionForSpec returns a map with all used definition in the provided spec as keys and true as values. +func usedDefinitionForSpec(sp *spec.Swagger) map[string]bool { + usedDefinitions := map[string]bool{} + walkOnAllReferences(func(ref spec.Ref) spec.Ref { + if refStr := ref.String(); refStr != "" && strings.HasPrefix(refStr, definitionPrefix) { + usedDefinitions[refStr[len(definitionPrefix):]] = true + } + return ref + }, sp) + return usedDefinitions +} + +// FilterSpecByPaths removes unnecessary paths and definitions used by those paths. +// i.e. if a Path removed by this function, all definition used by it and not used +// anywhere else will also be removed. func FilterSpecByPaths(sp *spec.Swagger, keepPathPrefixes []string) { + // Walk all references to find all used definitions. This function + // want to only deal with unused definitions resulted from filtering paths. + // Thus a definition will be removed only if it has been used before but + // it is unused because of a path prune. + initialUsedDefinitions := usedDefinitionForSpec(sp) + // First remove unwanted paths prefixes := util.NewTrie(keepPathPrefixes) orgPaths := sp.Paths @@ -174,34 +194,24 @@ func FilterSpecByPaths(sp *spec.Swagger, keepPathPrefixes []string) { } // Walk all references to find all definition references. - usedDefinitions := map[string]bool{} - - walkOnAllReferences(func(ref spec.Ref) spec.Ref { - if ref.String() != "" { - refStr := ref.String() - if strings.HasPrefix(refStr, definitionPrefix) { - usedDefinitions[refStr[len(definitionPrefix):]] = true - } - } - return ref - }, sp) + usedDefinitions := usedDefinitionForSpec(sp) // Remove unused definitions orgDefinitions := sp.Definitions sp.Definitions = spec.Definitions{} for k, v := range orgDefinitions { - if usedDefinitions[k] { + if usedDefinitions[k] || !initialUsedDefinitions[k] { sp.Definitions[k] = v } } } func renameDefinition(s *spec.Swagger, old, new string) { - old_ref := definitionPrefix + old - new_ref := definitionPrefix + new + oldRef := definitionPrefix + old + newRef := definitionPrefix + new walkOnAllReferences(func(ref spec.Ref) spec.Ref { - if ref.String() == old_ref { - return spec.MustCreateRef(new_ref) + if ref.String() == oldRef { + return spec.MustCreateRef(newRef) } return ref }, s) @@ -209,58 +219,117 @@ func renameDefinition(s *spec.Swagger, old, new string) { delete(s.Definitions, old) } -// Copy paths and definitions from source to dest, rename definitions if needed. -// dest will be mutated, and source will not be changed. +// MergeSpecsIgnorePathConflict is the same as MergeSpecs except it will ignore any path +// conflicts by keeping the paths of destination. It will rename definition conflicts. +func MergeSpecsIgnorePathConflict(dest, source *spec.Swagger) error { + return mergeSpecs(dest, source, true, true) +} + +// MergeSpecsFailOnDefinitionConflict is differ from MergeSpecs as it fails if there is +// a definition conflict. +func MergeSpecsFailOnDefinitionConflict(dest, source *spec.Swagger) error { + return mergeSpecs(dest, source, false, false) +} + +// MergeSpecs copies paths and definitions from source to dest, rename definitions if needed. +// dest will be mutated, and source will not be changed. It will fail on path conflicts. func MergeSpecs(dest, source *spec.Swagger) error { - sourceCopy, err := CloneSpec(source) - if err != nil { - return err + return mergeSpecs(dest, source, true, false) +} + +func mergeSpecs(dest, source *spec.Swagger, renameModelConflicts, ignorePathConflicts bool) (err error) { + specCloned := false + if ignorePathConflicts { + keepPaths := []string{} + hasConflictingPath := false + for k := range source.Paths.Paths { + if _, found := dest.Paths.Paths[k]; !found { + keepPaths = append(keepPaths, k) + } else { + hasConflictingPath = true + } + } + if len(keepPaths) == 0 { + // There is nothing to merge. All paths are conflicting. + return nil + } + if hasConflictingPath { + source, err = CloneSpec(source) + if err != nil { + return err + } + specCloned = true + FilterSpecByPaths(source, keepPaths) + } } - for k, v := range sourceCopy.Paths.Paths { + // Check for model conflicts + conflicts := false + for k, v := range source.Definitions { + v2, found := dest.Definitions[k] + if found && !reflect.DeepEqual(v, v2) { + if !renameModelConflicts { + return fmt.Errorf("model name conflict in merging OpenAPI spec: %s", k) + } + conflicts = true + break + } + } + + if conflicts { + if !specCloned { + source, err = CloneSpec(source) + if err != nil { + return err + } + } + specCloned = true + usedNames := map[string]bool{} + for k := range dest.Definitions { + usedNames[k] = true + } + type Rename struct { + from, to string + } + renames := []Rename{} + for k, v := range source.Definitions { + if usedNames[k] { + v2, found := dest.Definitions[k] + // Reuse model iff they are exactly the same. + if found && reflect.DeepEqual(v, v2) { + continue + } + i := 2 + newName := fmt.Sprintf("%s_v%d", k, i) + _, foundInSource := source.Definitions[newName] + for usedNames[newName] || foundInSource { + i++ + newName = fmt.Sprintf("%s_v%d", k, i) + _, foundInSource = source.Definitions[newName] + } + renames = append(renames, Rename{from: k, to: newName}) + usedNames[newName] = true + } + } + for _, r := range renames { + renameDefinition(source, r.from, r.to) + } + } + for k, v := range source.Definitions { + if _, found := dest.Definitions[k]; !found { + dest.Definitions[k] = v + } + } + // Check for path conflicts + for k, v := range source.Paths.Paths { if _, found := dest.Paths.Paths[k]; found { return fmt.Errorf("unable to merge: duplicated path %s", k) } dest.Paths.Paths[k] = v } - usedNames := map[string]bool{} - for k := range dest.Definitions { - usedNames[k] = true - } - type Rename struct { - from, to string - } - renames := []Rename{} - for k, v := range sourceCopy.Definitions { - if usedNames[k] { - v2, found := dest.Definitions[k] - // Reuse model iff they are exactly the same. - if found && reflect.DeepEqual(v, v2) { - continue - } - i := 2 - newName := fmt.Sprintf("%s_v%d", k, i) - _, foundInSource := sourceCopy.Definitions[newName] - for usedNames[newName] || foundInSource { - i += 1 - newName = fmt.Sprintf("%s_v%d", k, i) - _, foundInSource = sourceCopy.Definitions[newName] - } - renames = append(renames, Rename{from: k, to: newName}) - usedNames[newName] = true - } - } - for _, r := range renames { - renameDefinition(sourceCopy, r.from, r.to) - } - for k, v := range sourceCopy.Definitions { - if _, found := dest.Definitions[k]; !found { - dest.Definitions[k] = v - } - } return nil } -// Clone OpenAPI spec +// CloneSpec clones OpenAPI spec func CloneSpec(source *spec.Swagger) (*spec.Swagger, error) { // TODO(mehdy): Find a faster way to clone an spec bytes, err := json.Marshal(source) diff --git a/vendor/k8s.io/kube-openapi/pkg/generators/openapi.go b/vendor/k8s.io/kube-openapi/pkg/generators/openapi.go index 0e8e6c7d5cf..c9f05fd29bf 100644 --- a/vendor/k8s.io/kube-openapi/pkg/generators/openapi.go +++ b/vendor/k8s.io/kube-openapi/pkg/generators/openapi.go @@ -328,6 +328,7 @@ func (g openAPITypeWriter) generateMembers(t *types.Type, required []string) ([] required = append(required, name) } if err = g.generateProperty(&m, t); err != nil { + glog.Errorf("Error when generating: %v, %v\n", name, m) return required, err } }