mirror of https://github.com/k3s-io/k3s
Merge pull request #46734 from mbohlool/aggr
Automatic merge from submit-queue (batch tested with PRs 46734, 46810, 46759, 46259, 46771) OpenAPI aggregation for kube-aggregator This PR implements OpenAPI aggregation layer for kube-aggregator. On each API registration, it tries to download swagger.spec of the user api server. On failure it will try again next time (either on another add or get /swagger.* on aggregator server) up to five times. To merge specs, it first remove all unrelated paths from the downloaded spec (anything other than group/version of the API service) and then remove all unused definitions. Adding paths are straightforward as they won't have any conflicts, but definitions will most probably have conflicts. To resolve that, we would reused any definition that is not changed (documentation changes are fine) and rename the definition otherwise. To use this PR, kube aggregator should have nonResourceURLs (for get verb) to user apiserver. ```release-note Support OpenAPI spec aggregation for kube-aggregator ``` fixes: #43717pull/6/head
commit
a72967454d
|
@ -18571,6 +18571,768 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"/apis/apiregistration.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": [
|
||||
"apiregistration"
|
||||
],
|
||||
"operationId": "getApiregistrationAPIGroup",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.APIGroup"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/apis/apiregistration.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": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "getApiregistrationV1beta1APIResources",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/apis/apiregistration.k8s.io/v1beta1/apiservices": {
|
||||
"get": {
|
||||
"description": "list or watch objects of kind APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf",
|
||||
"application/json;stream=watch",
|
||||
"application/vnd.kubernetes.protobuf;stream=watch"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "listApiregistrationV1beta1APIService",
|
||||
"parameters": [
|
||||
{
|
||||
"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": "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.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIServiceList"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "list",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "create an APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "createApiregistrationV1beta1APIService",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "post",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "delete collection of APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "deleteApiregistrationV1beta1CollectionAPIService",
|
||||
"parameters": [
|
||||
{
|
||||
"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": "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": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"uniqueItems": true,
|
||||
"type": "string",
|
||||
"description": "If 'true', then the output is pretty printed.",
|
||||
"name": "pretty",
|
||||
"in": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/apiregistration.k8s.io/v1beta1/apiservices/{name}": {
|
||||
"get": {
|
||||
"description": "read the specified APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "readApiregistrationV1beta1APIService",
|
||||
"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.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "get",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "replace the specified APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "replaceApiregistrationV1beta1APIService",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "put",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"description": "delete an APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "deleteApiregistrationV1beta1APIService",
|
||||
"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": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"description": "partially update the specified APIService",
|
||||
"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": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "patchApiregistrationV1beta1APIService",
|
||||
"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.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "patch",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"uniqueItems": true,
|
||||
"type": "string",
|
||||
"description": "name of the APIService",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"uniqueItems": true,
|
||||
"type": "string",
|
||||
"description": "If 'true', then the output is pretty printed.",
|
||||
"name": "pretty",
|
||||
"in": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/apiregistration.k8s.io/v1beta1/apiservices/{name}/status": {
|
||||
"put": {
|
||||
"description": "replace status of the specified APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "replaceApiregistrationV1beta1APIServiceStatus",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized"
|
||||
}
|
||||
},
|
||||
"x-kubernetes-action": "put",
|
||||
"x-kubernetes-group-version-kind": {
|
||||
"group": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"uniqueItems": true,
|
||||
"type": "string",
|
||||
"description": "name of the APIService",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"uniqueItems": true,
|
||||
"type": "string",
|
||||
"description": "If 'true', then the output is pretty printed.",
|
||||
"name": "pretty",
|
||||
"in": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"/apis/apiregistration.k8s.io/v1beta1/watch/apiservices": {
|
||||
"get": {
|
||||
"description": "watch individual changes to a list of APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf",
|
||||
"application/json;stream=watch",
|
||||
"application/vnd.kubernetes.protobuf;stream=watch"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "watchApiregistrationV1beta1APIServiceList",
|
||||
"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": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"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": "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/v1beta1/watch/apiservices/{name}": {
|
||||
"get": {
|
||||
"description": "watch changes to an object of kind APIService",
|
||||
"consumes": [
|
||||
"*/*"
|
||||
],
|
||||
"produces": [
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/vnd.kubernetes.protobuf",
|
||||
"application/json;stream=watch",
|
||||
"application/vnd.kubernetes.protobuf;stream=watch"
|
||||
],
|
||||
"schemes": [
|
||||
"https"
|
||||
],
|
||||
"tags": [
|
||||
"apiregistration_v1beta1"
|
||||
],
|
||||
"operationId": "watchApiregistrationV1beta1APIService",
|
||||
"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": "apiregistration.k8s.io",
|
||||
"version": "v1beta1",
|
||||
"kind": "APIService"
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"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": "string",
|
||||
"description": "name of the APIService",
|
||||
"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/apps/": {
|
||||
"get": {
|
||||
"description": "get information of a group",
|
||||
|
@ -45895,6 +46657,146 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService": {
|
||||
"description": "APIService represents a server for a particular GroupVersion. Name must be \"version.group\".",
|
||||
"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: http://releases.k8s.io/HEAD/docs/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: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Spec contains information for locating and communicating with a server",
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIServiceSpec"
|
||||
},
|
||||
"status": {
|
||||
"description": "Status contains derived information about an API server",
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIServiceStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIServiceCondition": {
|
||||
"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.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIServiceList": {
|
||||
"description": "APIServiceList is a list of APIService 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: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources",
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIService"
|
||||
}
|
||||
},
|
||||
"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: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds",
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta"
|
||||
}
|
||||
}
|
||||
},
|
||||
"io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIServiceSpec": {
|
||||
"description": "APIServiceSpec contains information for locating and communicating with a server. Only https is supported, though you are able to disable certificate verification.",
|
||||
"required": [
|
||||
"service",
|
||||
"caBundle",
|
||||
"priority"
|
||||
],
|
||||
"properties": {
|
||||
"caBundle": {
|
||||
"description": "CABundle is a PEM encoded CA bundle which will be used to validate an API server's serving certificate.",
|
||||
"type": "string",
|
||||
"format": "byte"
|
||||
},
|
||||
"group": {
|
||||
"description": "Group is the API group name this server hosts",
|
||||
"type": "string"
|
||||
},
|
||||
"insecureSkipTLSVerify": {
|
||||
"description": "InsecureSkipTLSVerify disables TLS certificate verification when communicating with this server. This is strongly discouraged. You should use the CABundle instead.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"priority": {
|
||||
"description": "Priority controls the ordering of this API group in the overall discovery document that gets served. Client tools like `kubectl` use this ordering to derive preference, so this ordering mechanism is important. Values must be between 1 and 1000 The primary sort is based on priority, ordered lowest number to highest (10 before 20). The secondary sort is based on the alphabetical comparison of the name of the object. (v1.bar before v1.foo) We'd recommend something like: *.k8s.io (except extensions) at 100, extensions at 150 PaaSes (OpenShift, Deis) are recommended to be in the 200s",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"service": {
|
||||
"description": "Service is a reference to the service for this API server. It must communicate on port 443 If the Service is nil, that means the handling for the API groupversion is handled locally on this server. The call will simply delegate to the normal handler chain to be fulfilled.",
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.ServiceReference"
|
||||
},
|
||||
"version": {
|
||||
"description": "Version is the API version this server hosts. For example, \"v1\"",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIServiceStatus": {
|
||||
"description": "APIServiceStatus contains derived information about an API server",
|
||||
"properties": {
|
||||
"conditions": {
|
||||
"description": "Current service state of apiService.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.APIServiceCondition"
|
||||
},
|
||||
"x-kubernetes-patch-merge-key": "type",
|
||||
"x-kubernetes-patch-strategy": "merge"
|
||||
}
|
||||
}
|
||||
},
|
||||
"io.k8s.kube-aggregator.pkg.apis.apiregistration.v1beta1.ServiceReference": {
|
||||
"description": "ServiceReference holds a reference to Service.legacy.k8s.io",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name is the name of the service",
|
||||
"type": "string"
|
||||
},
|
||||
"namespace": {
|
||||
"description": "Namespace is the namespace of the service",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"io.k8s.kubernetes.pkg.api.v1.AWSElasticBlockStoreVolumeSource": {
|
||||
"description": "Represents a Persistent Disk resource in AWS.\n\nAn AWS EBS disk must exist before mounting to a container. The disk must also be in the same AWS zone as the kubelet. An AWS EBS disk can only be mounted as read/write once. AWS EBS volumes support ownership management and SELinux relabeling.",
|
||||
"required": [
|
||||
|
|
|
@ -50,7 +50,6 @@ func createAggregatorConfig(kubeAPIServerConfig genericapiserver.Config, command
|
|||
|
||||
// the aggregator doesn't wire these up. It just delegates them to the kubeapiserver
|
||||
genericConfig.EnableSwaggerUI = false
|
||||
genericConfig.OpenAPIConfig = nil
|
||||
genericConfig.SwaggerConfig = nil
|
||||
|
||||
// copy the etcd options so we don't mutate originals.
|
||||
|
|
|
@ -48,6 +48,7 @@ openapi_library(
|
|||
"k8s.io/apiserver/pkg/apis/audit/v1alpha1",
|
||||
"k8s.io/apiserver/pkg/apis/example/v1",
|
||||
"k8s.io/client-go/pkg/api/v1",
|
||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1",
|
||||
"k8s.io/metrics/pkg/apis/custom_metrics/v1alpha1",
|
||||
"k8s.io/metrics/pkg/apis/metrics/v1alpha1",
|
||||
],
|
||||
|
|
|
@ -65,11 +65,11 @@ type Config struct {
|
|||
GetDefinitions GetOpenAPIDefinitions
|
||||
|
||||
// GetOperationIDAndTags returns operation id and tags for a restful route. It is an optional function to customize operation IDs.
|
||||
GetOperationIDAndTags func(servePath string, r *restful.Route) (string, []string, error)
|
||||
GetOperationIDAndTags func(r *restful.Route) (string, []string, error)
|
||||
|
||||
// GetDefinitionName returns a friendly name for a definition base on the serving path. parameter `name` is the full name of the definition.
|
||||
// It is an optional function to customize model names.
|
||||
GetDefinitionName func(servePath string, name string) (string, spec.Extensions)
|
||||
GetDefinitionName func(name string) (string, spec.Extensions)
|
||||
|
||||
// PostProcessSpec runs after the spec is ready to serve. It allows a final modification to the spec before serving.
|
||||
PostProcessSpec func(*spec.Swagger) (*spec.Swagger, error)
|
||||
|
|
|
@ -59,7 +59,7 @@ func ToValidOperationID(s string, capitalizeFirstLetter bool) string {
|
|||
}
|
||||
|
||||
// GetOperationIDAndTags returns a customize operation ID and a list of tags for kubernetes API server's OpenAPI spec to prevent duplicate IDs.
|
||||
func GetOperationIDAndTags(servePath string, r *restful.Route) (string, []string, error) {
|
||||
func GetOperationIDAndTags(r *restful.Route) (string, []string, error) {
|
||||
op := r.Operation
|
||||
path := r.Path
|
||||
var tags []string
|
||||
|
@ -67,8 +67,6 @@ func GetOperationIDAndTags(servePath string, r *restful.Route) (string, []string
|
|||
if strings.HasPrefix(path, "/apis/extensions/v1beta1/namespaces/{namespace}/") && strings.HasSuffix(op, "ScaleScale") {
|
||||
op = op[:len(op)-10] + strings.Title(strings.Split(path[48:], "/")[0]) + "Scale"
|
||||
}
|
||||
switch servePath {
|
||||
case "/swagger.json":
|
||||
prefix, exists := verbs.GetPrefix(op)
|
||||
if !exists {
|
||||
return op, tags, fmt.Errorf("operation names should start with a verb. Cannot determine operation verb from %v", op)
|
||||
|
@ -80,8 +78,9 @@ func GetOperationIDAndTags(servePath string, r *restful.Route) (string, []string
|
|||
parts = append([]string{"apis", "core"}, parts[1:]...)
|
||||
}
|
||||
if len(parts) >= 2 && parts[0] == "apis" {
|
||||
prefix = prefix + ToValidOperationID(strings.TrimSuffix(parts[1], ".k8s.io"), prefix != "")
|
||||
tag := ToValidOperationID(strings.TrimSuffix(parts[1], ".k8s.io"), false)
|
||||
trimmed := strings.TrimSuffix(parts[1], ".k8s.io")
|
||||
prefix = prefix + ToValidOperationID(trimmed, prefix != "")
|
||||
tag := ToValidOperationID(trimmed, false)
|
||||
if len(parts) > 2 {
|
||||
prefix = prefix + ToValidOperationID(parts[2], prefix != "")
|
||||
tag = tag + "_" + ToValidOperationID(parts[2], false)
|
||||
|
@ -91,9 +90,6 @@ func GetOperationIDAndTags(servePath string, r *restful.Route) (string, []string
|
|||
tags = append(tags, ToValidOperationID(parts[0], false))
|
||||
}
|
||||
return prefix + ToValidOperationID(op, prefix != ""), tags, nil
|
||||
default:
|
||||
return op, tags, nil
|
||||
}
|
||||
}
|
||||
|
||||
type groupVersionKinds []v1.GroupVersionKind
|
||||
|
@ -161,7 +157,7 @@ func NewDefinitionNamer(s *runtime.Scheme) DefinitionNamer {
|
|||
}
|
||||
|
||||
// GetDefinitionName returns the name and tags for a given definition
|
||||
func (d *DefinitionNamer) GetDefinitionName(servePath string, name string) (string, spec.Extensions) {
|
||||
func (d *DefinitionNamer) GetDefinitionName(name string) (string, spec.Extensions) {
|
||||
if groupVersionKinds, ok := d.typeGroupVersionKinds[name]; ok {
|
||||
return friendlyName(name), spec.Extensions{
|
||||
extensionGVK: []v1.GroupVersionKind(groupVersionKinds),
|
||||
|
|
|
@ -71,7 +71,7 @@ func TestGetDefinitionName(t *testing.T) {
|
|||
s := runtime.NewScheme()
|
||||
s.AddKnownTypeWithName(testType.GroupVersionKind(), &testType)
|
||||
namer := NewDefinitionNamer(s)
|
||||
n, e := namer.GetDefinitionName("", typePkgName)
|
||||
n, e := namer.GetDefinitionName(typePkgName)
|
||||
assertEqual(t, typeFriendlyName, n)
|
||||
assertEqual(t, e["x-kubernetes-group-version-kind"], []v1.GroupVersionKind{
|
||||
{
|
||||
|
@ -80,7 +80,7 @@ func TestGetDefinitionName(t *testing.T) {
|
|||
Kind: "TestType",
|
||||
},
|
||||
})
|
||||
n, e2 := namer.GetDefinitionName("", "test.com/another.Type")
|
||||
n, e2 := namer.GetDefinitionName("test.com/another.Type")
|
||||
assertEqual(t, "com.test.another.Type", n)
|
||||
assertEqual(t, e2, spec.Extensions(nil))
|
||||
}
|
||||
|
|
|
@ -101,6 +101,7 @@ go_library(
|
|||
"//vendor/k8s.io/apiserver/pkg/server/filters:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/healthz:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/mux:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/openapi:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/routes:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//vendor/k8s.io/client-go/informers:go_default_library",
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"github.com/emicklei/go-restful-swagger12"
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
"k8s.io/apimachinery/pkg/apimachinery"
|
||||
"k8s.io/apimachinery/pkg/apimachinery/registered"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -43,6 +44,7 @@ import (
|
|||
apirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/apiserver/pkg/server/openapi"
|
||||
"k8s.io/apiserver/pkg/server/routes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
)
|
||||
|
@ -130,6 +132,9 @@ type GenericAPIServer struct {
|
|||
swaggerConfig *swagger.Config
|
||||
openAPIConfig *openapicommon.Config
|
||||
|
||||
// Enables updating OpenAPI spec using update method.
|
||||
OpenAPIService *openapi.OpenAPIService
|
||||
|
||||
// PostStartHooks are each called after the server has started listening, in a separate go func for each
|
||||
// with no guarantee of ordering between them. The map key is a name used for error reporting.
|
||||
// It may kill the process with a panic if it wishes to by returning an error.
|
||||
|
@ -165,6 +170,9 @@ type DelegationTarget interface {
|
|||
|
||||
// ListedPaths returns the paths for supporting an index
|
||||
ListedPaths() []string
|
||||
|
||||
// OpenAPISpec returns the OpenAPI spec of the delegation target if exists, nil otherwise.
|
||||
OpenAPISpec() *spec.Swagger
|
||||
}
|
||||
|
||||
func (s *GenericAPIServer) UnprotectedHandler() http.Handler {
|
||||
|
@ -180,6 +188,9 @@ func (s *GenericAPIServer) HealthzChecks() []healthz.HealthzChecker {
|
|||
func (s *GenericAPIServer) ListedPaths() []string {
|
||||
return s.listedPathProvider.ListedPaths()
|
||||
}
|
||||
func (s *GenericAPIServer) OpenAPISpec() *spec.Swagger {
|
||||
return s.OpenAPIService.GetSpec()
|
||||
}
|
||||
|
||||
var EmptyDelegate = emptyDelegate{
|
||||
requestContextMapper: apirequest.NewRequestContextMapper(),
|
||||
|
@ -204,6 +215,9 @@ func (s emptyDelegate) ListedPaths() []string {
|
|||
func (s emptyDelegate) RequestContextMapper() apirequest.RequestContextMapper {
|
||||
return s.requestContextMapper
|
||||
}
|
||||
func (s emptyDelegate) OpenAPISpec() *spec.Swagger {
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Send correct mime type for .svg files.
|
||||
|
@ -233,17 +247,22 @@ func (s *GenericAPIServer) PrepareRun() preparedGenericAPIServer {
|
|||
if s.swaggerConfig != nil {
|
||||
routes.Swagger{Config: s.swaggerConfig}.Install(s.Handler.GoRestfulContainer)
|
||||
}
|
||||
if s.openAPIConfig != nil {
|
||||
routes.OpenAPI{
|
||||
Config: s.openAPIConfig,
|
||||
}.Install(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
|
||||
}
|
||||
s.PrepareOpenAPIService()
|
||||
|
||||
s.installHealthz()
|
||||
|
||||
return preparedGenericAPIServer{s}
|
||||
}
|
||||
|
||||
// PrepareOpenAPIService installs OpenAPI handler if it does not exists.
|
||||
func (s *GenericAPIServer) PrepareOpenAPIService() {
|
||||
if s.openAPIConfig != nil && s.OpenAPIService == nil {
|
||||
s.OpenAPIService = routes.OpenAPI{
|
||||
Config: s.openAPIConfig,
|
||||
}.Install(s.Handler.GoRestfulContainer, s.Handler.NonGoRestfulMux)
|
||||
}
|
||||
}
|
||||
|
||||
// Run spawns the secure http server. It only returns if stopCh is closed
|
||||
// or the secure port cannot be listened on initially.
|
||||
func (s preparedGenericAPIServer) Run(stopCh <-chan struct{}) error {
|
||||
|
|
|
@ -10,11 +10,15 @@ load(
|
|||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["openapi_test.go"],
|
||||
srcs = [
|
||||
"openapi_aggregator_test.go",
|
||||
"openapi_test.go",
|
||||
],
|
||||
library = ":go_default_library",
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/emicklei/go-restful:go_default_library",
|
||||
"//vendor/github.com/ghodss/yaml:go_default_library",
|
||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/openapi:go_default_library",
|
||||
|
@ -26,6 +30,8 @@ go_library(
|
|||
srcs = [
|
||||
"doc.go",
|
||||
"openapi.go",
|
||||
"openapi_aggregator.go",
|
||||
"openapi_handler.go",
|
||||
"util.go",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
|
@ -36,6 +42,7 @@ go_library(
|
|||
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
|
||||
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",
|
||||
"//vendor/gopkg.in/yaml.v2:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/openapi:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/mux:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/trie:go_default_library",
|
||||
|
|
|
@ -17,26 +17,17 @@ limitations under the License.
|
|||
package openapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha512"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v2"
|
||||
"mime"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emicklei/go-restful"
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/googleapis/gnostic/OpenAPIv2"
|
||||
"github.com/googleapis/gnostic/compiler"
|
||||
|
||||
"k8s.io/apimachinery/pkg/openapi"
|
||||
genericmux "k8s.io/apiserver/pkg/server/mux"
|
||||
"k8s.io/apiserver/pkg/util/trie"
|
||||
)
|
||||
|
||||
|
@ -55,12 +46,7 @@ const (
|
|||
type openAPI struct {
|
||||
config *openapi.Config
|
||||
swagger *spec.Swagger
|
||||
swaggerBytes []byte
|
||||
swaggerPb []byte
|
||||
swaggerPbGz []byte
|
||||
lastModified time.Time
|
||||
protocolList []string
|
||||
servePath string
|
||||
definitions map[string]openapi.OpenAPIDefinition
|
||||
}
|
||||
|
||||
|
@ -68,18 +54,9 @@ func computeEtag(data []byte) string {
|
|||
return fmt.Sprintf("\"%X\"", sha512.Sum512(data))
|
||||
}
|
||||
|
||||
// RegisterOpenAPIService registers a handler to provides standard OpenAPI specification.
|
||||
func RegisterOpenAPIService(servePath string, webServices []*restful.WebService, config *openapi.Config, mux *genericmux.PathRecorderMux) (err error) {
|
||||
|
||||
if !strings.HasSuffix(servePath, JSON_EXT) {
|
||||
return fmt.Errorf("Serving path must ends with \"%s\".", JSON_EXT)
|
||||
}
|
||||
|
||||
servePathBase := servePath[:len(servePath)-len(JSON_EXT)]
|
||||
|
||||
func BuildSwaggerSpec(webServices []*restful.WebService, config *openapi.Config) (*spec.Swagger, error) {
|
||||
o := openAPI{
|
||||
config: config,
|
||||
servePath: servePath,
|
||||
swagger: &spec.Swagger{
|
||||
SwaggerProps: spec.SwaggerProps{
|
||||
Swagger: OpenAPIVersion,
|
||||
|
@ -90,60 +67,28 @@ func RegisterOpenAPIService(servePath string, webServices []*restful.WebService,
|
|||
},
|
||||
}
|
||||
|
||||
err = o.init(webServices)
|
||||
err := o.init(webServices)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mime.AddExtensionType(".json", MIME_JSON)
|
||||
mime.AddExtensionType(".pb-v1", MIME_PB)
|
||||
mime.AddExtensionType(".gz", MIME_PB_GZ)
|
||||
|
||||
type fileInfo struct {
|
||||
ext string
|
||||
data []byte
|
||||
}
|
||||
|
||||
files := []fileInfo{
|
||||
{".json", o.swaggerBytes},
|
||||
{"-2.0.0.json", o.swaggerBytes},
|
||||
{"-2.0.0.pb-v1", o.swaggerPb},
|
||||
{"-2.0.0.pb-v1.gz", o.swaggerPbGz},
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
path := servePathBase + file.ext
|
||||
data := file.data
|
||||
etag := computeEtag(file.data)
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != path {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("Path not found!"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("Etag", etag)
|
||||
// ServeContent will take care of caching using eTag.
|
||||
http.ServeContent(w, r, path, o.lastModified, bytes.NewReader(data))
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
return o.swagger, nil
|
||||
}
|
||||
|
||||
func (o *openAPI) init(webServices []*restful.WebService) error {
|
||||
if o.config.GetOperationIDAndTags == nil {
|
||||
o.config.GetOperationIDAndTags = func(_ string, r *restful.Route) (string, []string, error) {
|
||||
o.config.GetOperationIDAndTags = func(r *restful.Route) (string, []string, error) {
|
||||
return r.Operation, nil, nil
|
||||
}
|
||||
}
|
||||
if o.config.GetDefinitionName == nil {
|
||||
o.config.GetDefinitionName = func(_, name string) (string, spec.Extensions) {
|
||||
o.config.GetDefinitionName = func(name string) (string, spec.Extensions) {
|
||||
return name[strings.LastIndex(name, "/")+1:], nil
|
||||
}
|
||||
}
|
||||
o.definitions = o.config.GetDefinitions(func(name string) spec.Ref {
|
||||
defName, _ := o.config.GetDefinitionName(o.servePath, name)
|
||||
return spec.MustCreateRef("#/definitions/" + openapi.EscapeJsonPointer(defName))
|
||||
defName, _ := o.config.GetDefinitionName(name)
|
||||
return spec.MustCreateRef(DEFINITION_PREFIX + openapi.EscapeJsonPointer(defName))
|
||||
})
|
||||
if o.config.CommonResponses == nil {
|
||||
o.config.CommonResponses = map[int]spec.Response{}
|
||||
|
@ -163,41 +108,9 @@ func (o *openAPI) init(webServices []*restful.WebService) error {
|
|||
}
|
||||
}
|
||||
|
||||
o.swaggerBytes, err = json.MarshalIndent(o.swagger, " ", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.swaggerPb, err = toProtoBinary(o.swaggerBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.swaggerPbGz = toGzip(o.swaggerPb)
|
||||
o.lastModified = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toProtoBinary(spec []byte) ([]byte, error) {
|
||||
var info yaml.MapSlice
|
||||
err := yaml.Unmarshal(spec, &info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
document, err := openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proto.Marshal(document)
|
||||
}
|
||||
|
||||
func toGzip(data []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
zw := gzip.NewWriter(&buf)
|
||||
zw.Write(data)
|
||||
zw.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func getCanonicalizeTypeName(t reflect.Type) string {
|
||||
if t.PkgPath() == "" {
|
||||
return t.Name()
|
||||
|
@ -210,7 +123,7 @@ func getCanonicalizeTypeName(t reflect.Type) string {
|
|||
}
|
||||
|
||||
func (o *openAPI) buildDefinitionRecursively(name string) error {
|
||||
uniqueName, extensions := o.config.GetDefinitionName(o.servePath, name)
|
||||
uniqueName, extensions := o.config.GetDefinitionName(name)
|
||||
if _, ok := o.swagger.Definitions[uniqueName]; ok {
|
||||
return nil
|
||||
}
|
||||
|
@ -252,8 +165,8 @@ func (o *openAPI) buildDefinitionForType(sample interface{}) (string, error) {
|
|||
if err := o.buildDefinitionRecursively(name); err != nil {
|
||||
return "", err
|
||||
}
|
||||
defName, _ := o.config.GetDefinitionName(o.servePath, name)
|
||||
return "#/definitions/" + openapi.EscapeJsonPointer(defName), nil
|
||||
defName, _ := o.config.GetDefinitionName(name)
|
||||
return DEFINITION_PREFIX + openapi.EscapeJsonPointer(defName), nil
|
||||
}
|
||||
|
||||
// buildPaths builds OpenAPI paths using go-restful's web services.
|
||||
|
@ -356,7 +269,7 @@ func (o *openAPI) buildOperations(route restful.Route, inPathCommonParamsMap map
|
|||
ret.Extensions.Add(k, v)
|
||||
}
|
||||
}
|
||||
if ret.ID, ret.Tags, err = o.config.GetOperationIDAndTags(o.servePath, &route); err != nil {
|
||||
if ret.ID, ret.Tags, err = o.config.GetOperationIDAndTags(&route); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,482 @@
|
|||
/*
|
||||
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"
|
||||
"strings"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/apiserver/pkg/util/trie"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFINITION_PREFIX = "#/definitions/"
|
||||
)
|
||||
|
||||
var cloner = conversion.NewCloner()
|
||||
|
||||
// Run a walkRefCallback method on all references of an OpenAPI spec
|
||||
type walkAllRefs struct {
|
||||
// walkRefCallback will be called on each reference and the return value
|
||||
// will replace that reference. This will allow the callers to change
|
||||
// all/some references of an spec (e.g. useful in renaming definitions).
|
||||
walkRefCallback func(ref spec.Ref) spec.Ref
|
||||
|
||||
// The spec to walk through.
|
||||
root *spec.Swagger
|
||||
}
|
||||
|
||||
func newWalkAllRefs(walkRef func(ref spec.Ref) spec.Ref, sp *spec.Swagger) *walkAllRefs {
|
||||
return &walkAllRefs{
|
||||
walkRefCallback: walkRef,
|
||||
root: sp,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *walkAllRefs) walkRef(ref spec.Ref) spec.Ref {
|
||||
if ref.String() != "" {
|
||||
refStr := ref.String()
|
||||
// References that start with #/definitions/ has a definition
|
||||
// inside the same spec file. If that is the case, walk through
|
||||
// those definitions too.
|
||||
// We do not support external references yet.
|
||||
if strings.HasPrefix(refStr, DEFINITION_PREFIX) {
|
||||
def := s.root.Definitions[refStr[len(DEFINITION_PREFIX):]]
|
||||
s.walkSchema(&def)
|
||||
}
|
||||
}
|
||||
return s.walkRefCallback(ref)
|
||||
}
|
||||
|
||||
func (s *walkAllRefs) walkSchema(schema *spec.Schema) {
|
||||
if schema == nil {
|
||||
return
|
||||
}
|
||||
schema.Ref = s.walkRef(schema.Ref)
|
||||
for _, v := range schema.Definitions {
|
||||
s.walkSchema(&v)
|
||||
}
|
||||
for _, v := range schema.Properties {
|
||||
s.walkSchema(&v)
|
||||
}
|
||||
for _, v := range schema.PatternProperties {
|
||||
s.walkSchema(&v)
|
||||
}
|
||||
for _, v := range schema.AllOf {
|
||||
s.walkSchema(&v)
|
||||
}
|
||||
for _, v := range schema.AnyOf {
|
||||
s.walkSchema(&v)
|
||||
}
|
||||
for _, v := range schema.OneOf {
|
||||
s.walkSchema(&v)
|
||||
}
|
||||
if schema.Not != nil {
|
||||
s.walkSchema(schema.Not)
|
||||
}
|
||||
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil {
|
||||
s.walkSchema(schema.AdditionalProperties.Schema)
|
||||
}
|
||||
if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil {
|
||||
s.walkSchema(schema.AdditionalItems.Schema)
|
||||
}
|
||||
if schema.Items != nil {
|
||||
if schema.Items.Schema != nil {
|
||||
s.walkSchema(schema.Items.Schema)
|
||||
}
|
||||
for _, v := range schema.Items.Schemas {
|
||||
s.walkSchema(&v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *walkAllRefs) walkParams(params []spec.Parameter) {
|
||||
if params == nil {
|
||||
return
|
||||
}
|
||||
for _, param := range params {
|
||||
param.Ref = s.walkRef(param.Ref)
|
||||
s.walkSchema(param.Schema)
|
||||
if param.Items != nil {
|
||||
param.Items.Ref = s.walkRef(param.Items.Ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *walkAllRefs) walkResponse(resp *spec.Response) {
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
resp.Ref = s.walkRef(resp.Ref)
|
||||
s.walkSchema(resp.Schema)
|
||||
}
|
||||
|
||||
func (s *walkAllRefs) walkOperation(op *spec.Operation) {
|
||||
if op == nil {
|
||||
return
|
||||
}
|
||||
s.walkParams(op.Parameters)
|
||||
if op.Responses == nil {
|
||||
return
|
||||
}
|
||||
s.walkResponse(op.Responses.Default)
|
||||
for _, r := range op.Responses.StatusCodeResponses {
|
||||
s.walkResponse(&r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *walkAllRefs) Start() {
|
||||
for _, pathItem := range s.root.Paths.Paths {
|
||||
s.walkParams(pathItem.Parameters)
|
||||
s.walkOperation(pathItem.Delete)
|
||||
s.walkOperation(pathItem.Get)
|
||||
s.walkOperation(pathItem.Head)
|
||||
s.walkOperation(pathItem.Options)
|
||||
s.walkOperation(pathItem.Patch)
|
||||
s.walkOperation(pathItem.Post)
|
||||
s.walkOperation(pathItem.Put)
|
||||
}
|
||||
}
|
||||
|
||||
// FilterSpecByPaths remove unnecessary paths and unused definitions.
|
||||
func FilterSpecByPaths(sp *spec.Swagger, keepPathPrefixes []string) {
|
||||
// First remove unwanted paths
|
||||
prefixes := trie.New(keepPathPrefixes)
|
||||
orgPaths := sp.Paths
|
||||
if orgPaths == nil {
|
||||
return
|
||||
}
|
||||
sp.Paths = &spec.Paths{
|
||||
VendorExtensible: orgPaths.VendorExtensible,
|
||||
Paths: map[string]spec.PathItem{},
|
||||
}
|
||||
for path, pathItem := range orgPaths.Paths {
|
||||
if !prefixes.HasPrefix(path) {
|
||||
continue
|
||||
}
|
||||
sp.Paths.Paths[path] = pathItem
|
||||
}
|
||||
|
||||
// Walk all references to find all definition references.
|
||||
usedDefinitions := map[string]bool{}
|
||||
|
||||
newWalkAllRefs(func(ref spec.Ref) spec.Ref {
|
||||
if ref.String() != "" {
|
||||
refStr := ref.String()
|
||||
if strings.HasPrefix(refStr, DEFINITION_PREFIX) {
|
||||
usedDefinitions[refStr[len(DEFINITION_PREFIX):]] = true
|
||||
}
|
||||
}
|
||||
return ref
|
||||
}, sp).Start()
|
||||
|
||||
// Remove unused definitions
|
||||
orgDefinitions := sp.Definitions
|
||||
sp.Definitions = spec.Definitions{}
|
||||
for k, v := range orgDefinitions {
|
||||
if usedDefinitions[k] {
|
||||
sp.Definitions[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func equalSchemaMap(s1, s2 map[string]spec.Schema) bool {
|
||||
if len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
for k, v := range s1 {
|
||||
v2, found := s2[k]
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
if !EqualSchema(&v, &v2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func equalSchemaArray(s1, s2 []spec.Schema) bool {
|
||||
if s1 == nil || s2 == nil {
|
||||
return s1 == nil && s2 == nil
|
||||
}
|
||||
if len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
for _, v1 := range s1 {
|
||||
found := false
|
||||
for _, v2 := range s2 {
|
||||
if EqualSchema(&v1, &v2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, v2 := range s2 {
|
||||
found := false
|
||||
for _, v1 := range s1 {
|
||||
if EqualSchema(&v1, &v2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func equalSchemaOrBool(s1, s2 *spec.SchemaOrBool) bool {
|
||||
if s1 == nil || s2 == nil {
|
||||
return s1 == s2
|
||||
}
|
||||
if s1.Allows != s2.Allows {
|
||||
return false
|
||||
}
|
||||
if !EqualSchema(s1.Schema, s2.Schema) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func equalSchemaOrArray(s1, s2 *spec.SchemaOrArray) bool {
|
||||
if s1 == nil || s2 == nil {
|
||||
return s1 == s2
|
||||
}
|
||||
if !EqualSchema(s1.Schema, s2.Schema) {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaArray(s1.Schemas, s2.Schemas) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func equalStringArray(s1, s2 []string) bool {
|
||||
if len(s1) != len(s2) {
|
||||
return false
|
||||
}
|
||||
for _, v1 := range s1 {
|
||||
found := false
|
||||
for _, v2 := range s2 {
|
||||
if v1 == v2 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, v2 := range s2 {
|
||||
found := false
|
||||
for _, v1 := range s1 {
|
||||
if v1 == v2 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func equalFloatPointer(s1, s2 *float64) bool {
|
||||
if s1 == nil || s2 == nil {
|
||||
return s1 == s2
|
||||
}
|
||||
return *s1 == *s2
|
||||
}
|
||||
|
||||
func equalIntPointer(s1, s2 *int64) bool {
|
||||
if s1 == nil || s2 == nil {
|
||||
return s1 == s2
|
||||
}
|
||||
return *s1 == *s2
|
||||
}
|
||||
|
||||
// EqualSchema returns true if models have the same properties and references
|
||||
// even if they have different documentation.
|
||||
func EqualSchema(s1, s2 *spec.Schema) bool {
|
||||
if s1 == nil || s2 == nil {
|
||||
return s1 == s2
|
||||
}
|
||||
if s1.Ref.String() != s2.Ref.String() {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaMap(s1.Definitions, s2.Definitions) {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaMap(s1.Properties, s2.Properties) {
|
||||
fmt.Println("Not equal props")
|
||||
return false
|
||||
}
|
||||
if !equalSchemaMap(s1.PatternProperties, s2.PatternProperties) {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaArray(s1.AllOf, s2.AllOf) {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaArray(s1.AnyOf, s2.AnyOf) {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaArray(s1.OneOf, s2.OneOf) {
|
||||
return false
|
||||
}
|
||||
if !EqualSchema(s1.Not, s2.Not) {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaOrBool(s1.AdditionalProperties, s2.AdditionalProperties) {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaOrBool(s1.AdditionalItems, s2.AdditionalItems) {
|
||||
return false
|
||||
}
|
||||
if !equalSchemaOrArray(s1.Items, s2.Items) {
|
||||
return false
|
||||
}
|
||||
if !equalStringArray(s1.Type, s2.Type) {
|
||||
return false
|
||||
}
|
||||
if s1.Format != s2.Format {
|
||||
return false
|
||||
}
|
||||
if !equalFloatPointer(s1.Minimum, s2.Minimum) {
|
||||
return false
|
||||
}
|
||||
if !equalFloatPointer(s1.Maximum, s2.Maximum) {
|
||||
return false
|
||||
}
|
||||
if s1.ExclusiveMaximum != s2.ExclusiveMaximum {
|
||||
return false
|
||||
}
|
||||
if s1.ExclusiveMinimum != s2.ExclusiveMinimum {
|
||||
return false
|
||||
}
|
||||
if !equalFloatPointer(s1.MultipleOf, s2.MultipleOf) {
|
||||
return false
|
||||
}
|
||||
if !equalIntPointer(s1.MaxLength, s2.MaxLength) {
|
||||
return false
|
||||
}
|
||||
if !equalIntPointer(s1.MinLength, s2.MinLength) {
|
||||
return false
|
||||
}
|
||||
if !equalIntPointer(s1.MaxItems, s2.MaxItems) {
|
||||
return false
|
||||
}
|
||||
if !equalIntPointer(s1.MinItems, s2.MinItems) {
|
||||
return false
|
||||
}
|
||||
if s1.Pattern != s2.Pattern {
|
||||
return false
|
||||
}
|
||||
if s1.UniqueItems != s2.UniqueItems {
|
||||
return false
|
||||
}
|
||||
if !equalIntPointer(s1.MaxProperties, s2.MaxProperties) {
|
||||
return false
|
||||
}
|
||||
if !equalIntPointer(s1.MinProperties, s2.MinProperties) {
|
||||
return false
|
||||
}
|
||||
if !equalStringArray(s1.Required, s2.Required) {
|
||||
return false
|
||||
}
|
||||
return len(s1.Enum) == 0 && len(s2.Enum) == 0 && len(s1.Dependencies) == 0 && len(s2.Dependencies) == 0
|
||||
}
|
||||
|
||||
func renameDefinition(s *spec.Swagger, old, new string) {
|
||||
old_ref := DEFINITION_PREFIX + old
|
||||
new_ref := DEFINITION_PREFIX + new
|
||||
newWalkAllRefs(func(ref spec.Ref) spec.Ref {
|
||||
if ref.String() == old_ref {
|
||||
return spec.MustCreateRef(new_ref)
|
||||
}
|
||||
return ref
|
||||
}, s).Start()
|
||||
s.Definitions[new] = s.Definitions[old]
|
||||
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.
|
||||
func MergeSpecs(dest, source *spec.Swagger) error {
|
||||
source, err := CloneSpec(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 source.Definitions {
|
||||
v2, found := dest.Definitions[k]
|
||||
if found || usedNames[k] {
|
||||
if found && EqualSchema(&v, &v2) {
|
||||
continue
|
||||
}
|
||||
i := 2
|
||||
newName := fmt.Sprintf("%s_v%d", k, i)
|
||||
for usedNames[newName] {
|
||||
i += 1
|
||||
newName = fmt.Sprintf("%s_v%d", k, i)
|
||||
}
|
||||
renames = append(renames, Rename{from: k, to: newName})
|
||||
usedNames[newName] = true
|
||||
} else {
|
||||
usedNames[k] = 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
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone OpenAPI spec
|
||||
func CloneSpec(source *spec.Swagger) (*spec.Swagger, error) {
|
||||
if ret, err := cloner.DeepCopy(source); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return ret.(*spec.Swagger), nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,520 @@
|
|||
/*
|
||||
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 (
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilterSpecs(t *testing.T) {
|
||||
var spec1, spec1_filtered *spec.Swagger
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
tags:
|
||||
- "test"
|
||||
summary: "Test API"
|
||||
operationId: "addTest"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
responses:
|
||||
405:
|
||||
description: "Invalid input"
|
||||
$ref: "#/definitions/InvalidInput"
|
||||
/othertest:
|
||||
post:
|
||||
tags:
|
||||
- "test2"
|
||||
summary: "Test2 API"
|
||||
operationId: "addTest2"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/xml"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test2 object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test2"
|
||||
definitions:
|
||||
Test:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Status"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
Test2:
|
||||
type: "object"
|
||||
properties:
|
||||
other:
|
||||
$ref: "#/definitions/Other"
|
||||
Other:
|
||||
type: "string"
|
||||
`), &spec1)
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
tags:
|
||||
- "test"
|
||||
summary: "Test API"
|
||||
operationId: "addTest"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
responses:
|
||||
405:
|
||||
description: "Invalid input"
|
||||
$ref: "#/definitions/InvalidInput"
|
||||
definitions:
|
||||
Test:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Status"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
`), &spec1_filtered)
|
||||
assert := assert.New(t)
|
||||
FilterSpecByPaths(spec1, []string{"/test"})
|
||||
assert.Equal(spec1_filtered, spec1)
|
||||
}
|
||||
|
||||
func TestMergeSpecsSimple(t *testing.T) {
|
||||
var spec1, spec2, expected *spec.Swagger
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
tags:
|
||||
- "test"
|
||||
summary: "Test API"
|
||||
operationId: "addTest"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
responses:
|
||||
405:
|
||||
description: "Invalid input"
|
||||
$ref: "#/definitions/InvalidInput"
|
||||
definitions:
|
||||
Test:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Status"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
`), &spec1)
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/othertest:
|
||||
post:
|
||||
tags:
|
||||
- "test2"
|
||||
summary: "Test2 API"
|
||||
operationId: "addTest2"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/xml"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test2 object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test2"
|
||||
definitions:
|
||||
Test2:
|
||||
type: "object"
|
||||
properties:
|
||||
other:
|
||||
$ref: "#/definitions/Other"
|
||||
Other:
|
||||
type: "string"
|
||||
`), &spec2)
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
tags:
|
||||
- "test"
|
||||
summary: "Test API"
|
||||
operationId: "addTest"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
responses:
|
||||
405:
|
||||
description: "Invalid input"
|
||||
$ref: "#/definitions/InvalidInput"
|
||||
/othertest:
|
||||
post:
|
||||
tags:
|
||||
- "test2"
|
||||
summary: "Test2 API"
|
||||
operationId: "addTest2"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/xml"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test2 object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test2"
|
||||
definitions:
|
||||
Test:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Status"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
Test2:
|
||||
type: "object"
|
||||
properties:
|
||||
other:
|
||||
$ref: "#/definitions/Other"
|
||||
Other:
|
||||
type: "string"
|
||||
`), &expected)
|
||||
assert := assert.New(t)
|
||||
if !assert.NoError(MergeSpecs(spec1, spec2)) {
|
||||
return
|
||||
}
|
||||
assert.Equal(expected, spec1)
|
||||
}
|
||||
|
||||
func TestMergeSpecsReuseModel(t *testing.T) {
|
||||
var spec1, spec2, expected *spec.Swagger
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
tags:
|
||||
- "test"
|
||||
summary: "Test API"
|
||||
operationId: "addTest"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
responses:
|
||||
405:
|
||||
description: "Invalid input"
|
||||
$ref: "#/definitions/InvalidInput"
|
||||
definitions:
|
||||
Test:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Status"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
`), &spec1)
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/othertest:
|
||||
post:
|
||||
tags:
|
||||
- "test2"
|
||||
summary: "Test2 API"
|
||||
operationId: "addTest2"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/xml"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test2 object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
definitions:
|
||||
Test:
|
||||
description: "This Test has a description"
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "This status has another description"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
`), &spec2)
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
tags:
|
||||
- "test"
|
||||
summary: "Test API"
|
||||
operationId: "addTest"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
responses:
|
||||
405:
|
||||
description: "Invalid input"
|
||||
$ref: "#/definitions/InvalidInput"
|
||||
/othertest:
|
||||
post:
|
||||
tags:
|
||||
- "test2"
|
||||
summary: "Test2 API"
|
||||
operationId: "addTest2"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/xml"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test2 object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
definitions:
|
||||
Test:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Status"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
`), &expected)
|
||||
assert := assert.New(t)
|
||||
if !assert.NoError(MergeSpecs(spec1, spec2)) {
|
||||
return
|
||||
}
|
||||
assert.Equal(expected, spec1)
|
||||
}
|
||||
|
||||
func TestMergeSpecsRenameModel(t *testing.T) {
|
||||
var spec1, spec2, expected *spec.Swagger
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
tags:
|
||||
- "test"
|
||||
summary: "Test API"
|
||||
operationId: "addTest"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
responses:
|
||||
405:
|
||||
description: "Invalid input"
|
||||
$ref: "#/definitions/InvalidInput"
|
||||
definitions:
|
||||
Test:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Status"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
`), &spec1)
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/othertest:
|
||||
post:
|
||||
tags:
|
||||
- "test2"
|
||||
summary: "Test2 API"
|
||||
operationId: "addTest2"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/xml"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test2 object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
definitions:
|
||||
Test:
|
||||
description: "This Test has a description"
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
`), &spec2)
|
||||
yaml.Unmarshal([]byte(`
|
||||
swagger: "2.0"
|
||||
paths:
|
||||
/test:
|
||||
post:
|
||||
tags:
|
||||
- "test"
|
||||
summary: "Test API"
|
||||
operationId: "addTest"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test"
|
||||
responses:
|
||||
405:
|
||||
description: "Invalid input"
|
||||
$ref: "#/definitions/InvalidInput"
|
||||
/othertest:
|
||||
post:
|
||||
tags:
|
||||
- "test2"
|
||||
summary: "Test2 API"
|
||||
operationId: "addTest2"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/xml"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "test2 object"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/Test_v2"
|
||||
definitions:
|
||||
Test:
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
status:
|
||||
type: "string"
|
||||
description: "Status"
|
||||
Test_v2:
|
||||
description: "This Test has a description"
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
InvalidInput:
|
||||
type: "string"
|
||||
format: "string"
|
||||
`), &expected)
|
||||
assert := assert.New(t)
|
||||
if !assert.NoError(MergeSpecs(spec1, spec2)) {
|
||||
return
|
||||
}
|
||||
|
||||
expected_yaml, _ := yaml.Marshal(expected)
|
||||
spec1_yaml, _ := yaml.Marshal(spec1)
|
||||
|
||||
assert.Equal(string(expected_yaml), string(spec1_yaml))
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/googleapis/gnostic/OpenAPIv2"
|
||||
"github.com/googleapis/gnostic/compiler"
|
||||
"gopkg.in/yaml.v2"
|
||||
genericmux "k8s.io/apiserver/pkg/server/mux"
|
||||
)
|
||||
|
||||
type OpenAPIService struct {
|
||||
orgSpec *spec.Swagger
|
||||
specBytes []byte
|
||||
specPb []byte
|
||||
specPbGz []byte
|
||||
lastModified time.Time
|
||||
updateHooks []func(*http.Request)
|
||||
}
|
||||
|
||||
// RegisterOpenAPIService registers a handler to provides standard OpenAPI specification.
|
||||
func RegisterOpenAPIService(openapiSpec *spec.Swagger, servePath string, mux *genericmux.PathRecorderMux) (*OpenAPIService, error) {
|
||||
if !strings.HasSuffix(servePath, JSON_EXT) {
|
||||
return nil, fmt.Errorf("Serving path must ends with \"%s\".", JSON_EXT)
|
||||
}
|
||||
|
||||
servePathBase := servePath[:len(servePath)-len(JSON_EXT)]
|
||||
|
||||
o := OpenAPIService{}
|
||||
if err := o.UpdateSpec(openapiSpec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mime.AddExtensionType(".json", MIME_JSON)
|
||||
mime.AddExtensionType(".pb-v1", MIME_PB)
|
||||
mime.AddExtensionType(".gz", MIME_PB_GZ)
|
||||
|
||||
type fileInfo struct {
|
||||
ext string
|
||||
getData func() []byte
|
||||
}
|
||||
|
||||
files := []fileInfo{
|
||||
{".json", o.getSwaggerBytes},
|
||||
{"-2.0.0.json", o.getSwaggerBytes},
|
||||
{"-2.0.0.pb-v1", o.getSwaggerPbBytes},
|
||||
{"-2.0.0.pb-v1.gz", o.getSwaggerPbGzBytes},
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
path := servePathBase + file.ext
|
||||
getData := file.getData
|
||||
mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != path {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("Path not found!"))
|
||||
return
|
||||
}
|
||||
o.update(r)
|
||||
data := getData()
|
||||
etag := computeEtag(data)
|
||||
w.Header().Set("Etag", etag)
|
||||
// ServeContent will take care of caching using eTag.
|
||||
http.ServeContent(w, r, path, o.lastModified, bytes.NewReader(data))
|
||||
})
|
||||
}
|
||||
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
func (o *OpenAPIService) getSwaggerBytes() []byte {
|
||||
return o.specBytes
|
||||
}
|
||||
|
||||
func (o *OpenAPIService) getSwaggerPbBytes() []byte {
|
||||
return o.specPb
|
||||
}
|
||||
|
||||
func (o *OpenAPIService) getSwaggerPbGzBytes() []byte {
|
||||
return o.specPbGz
|
||||
}
|
||||
|
||||
func (o *OpenAPIService) GetSpec() *spec.Swagger {
|
||||
return o.orgSpec
|
||||
}
|
||||
|
||||
func (o *OpenAPIService) UpdateSpec(openapiSpec *spec.Swagger) (err error) {
|
||||
o.orgSpec = openapiSpec
|
||||
o.specBytes, err = json.MarshalIndent(openapiSpec, " ", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.specPb, err = toProtoBinary(o.specBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.specPbGz = toGzip(o.specPb)
|
||||
o.lastModified = time.Now()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toProtoBinary(spec []byte) ([]byte, error) {
|
||||
var info yaml.MapSlice
|
||||
err := yaml.Unmarshal(spec, &info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
document, err := openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return proto.Marshal(document)
|
||||
}
|
||||
|
||||
func toGzip(data []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
zw := gzip.NewWriter(&buf)
|
||||
zw.Write(data)
|
||||
zw.Close()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// Adds an update hook to be called on each spec request. The hook is responsible
|
||||
// to call UpdateSpec method.
|
||||
func (o *OpenAPIService) AddUpdateHook(hook func(*http.Request)) {
|
||||
o.updateHooks = append(o.updateHooks, hook)
|
||||
}
|
||||
|
||||
func (o *OpenAPIService) update(r *http.Request) {
|
||||
for _, h := range o.updateHooks {
|
||||
h(r)
|
||||
}
|
||||
}
|
|
@ -200,7 +200,7 @@ func getConfig(fullMethods bool) (*openapi.Config, *restful.Container) {
|
|||
"k8s.io/apiserver/pkg/server/openapi/go_default_test.TestOutput": *TestOutput{}.OpenAPIDefinition(),
|
||||
}
|
||||
},
|
||||
GetDefinitionName: func(_ string, name string) (string, spec.Extensions) {
|
||||
GetDefinitionName: func(name string) (string, spec.Extensions) {
|
||||
friendlyName := name[strings.LastIndex(name, "/")+1:]
|
||||
if strings.HasPrefix(friendlyName, "go_default_test") {
|
||||
friendlyName = "openapi" + friendlyName[len("go_default_test"):]
|
||||
|
|
|
@ -32,9 +32,16 @@ type OpenAPI struct {
|
|||
}
|
||||
|
||||
// Install adds the SwaggerUI webservice to the given mux.
|
||||
func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) {
|
||||
err := apiserveropenapi.RegisterOpenAPIService("/swagger.json", c.RegisteredWebServices(), oa.Config, mux)
|
||||
func (oa OpenAPI) Install(c *restful.Container, mux *mux.PathRecorderMux) *apiserveropenapi.OpenAPIService {
|
||||
openapiSpec, err := apiserveropenapi.BuildSwaggerSpec(c.RegisteredWebServices(), oa.Config)
|
||||
if err != nil {
|
||||
glog.Fatalf("Failed to register open api spec for root: %v", err)
|
||||
return nil
|
||||
}
|
||||
service, err := apiserveropenapi.RegisterOpenAPIService(openapiSpec, "/swagger.json", mux)
|
||||
if err != nil {
|
||||
glog.Fatalf("Failed to register open api spec for root: %v", err)
|
||||
return nil
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
// +k8s:deepcopy-gen=package,register
|
||||
// +k8s:conversion-gen=k8s.io/kubernetes/vendor/k8s.io/kube-aggregator/pkg/apis/apiregistration
|
||||
// +k8s:openapi-gen=true
|
||||
|
||||
// Package v1beta1 contains the API Registration API, which is responsible for
|
||||
// registering an API `Group`/`Version` with another kubernetes like API server.
|
||||
|
|
|
@ -105,6 +105,8 @@ message APIServiceSpec {
|
|||
message APIServiceStatus {
|
||||
// Current service state of apiService.
|
||||
// +optional
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
repeated APIServiceCondition conditions = 1;
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,8 @@ type APIServiceCondition struct {
|
|||
type APIServiceStatus struct {
|
||||
// Current service state of apiService.
|
||||
// +optional
|
||||
// +patchMergeKey=type
|
||||
// +patchStrategy=merge
|
||||
Conditions []APIServiceCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,9 @@ go_library(
|
|||
],
|
||||
tags = ["automanaged"],
|
||||
deps = [
|
||||
"//vendor/github.com/go-openapi/spec:go_default_library",
|
||||
"//vendor/github.com/golang/glog:go_default_library",
|
||||
"//vendor/github.com/pkg/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apimachinery/announced:go_default_library",
|
||||
"//vendor/k8s.io/apimachinery/pkg/apimachinery/registered:go_default_library",
|
||||
|
@ -61,6 +63,7 @@ go_library(
|
|||
"//vendor/k8s.io/apiserver/pkg/registry/generic/rest:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/server/openapi:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
|
||||
"//vendor/k8s.io/apiserver/pkg/util/proxy:go_default_library",
|
||||
"//vendor/k8s.io/client-go/informers:go_default_library",
|
||||
|
|
|
@ -17,8 +17,11 @@ limitations under the License.
|
|||
package apiserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apimachinery/announced"
|
||||
|
@ -37,6 +40,14 @@ import (
|
|||
listersv1 "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/pkg/version"
|
||||
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/go-openapi/spec"
|
||||
"github.com/golang/glog"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"k8s.io/apiserver/pkg/server/openapi"
|
||||
"k8s.io/client-go/transport"
|
||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration"
|
||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration/install"
|
||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
|
||||
|
@ -54,6 +65,10 @@ var (
|
|||
Codecs = serializer.NewCodecFactory(Scheme)
|
||||
)
|
||||
|
||||
const (
|
||||
LOAD_OPENAPI_SPEC_MAX_RETRIES = 10
|
||||
)
|
||||
|
||||
func init() {
|
||||
install.Install(groupFactoryRegistry, registry, Scheme)
|
||||
|
||||
|
@ -118,6 +133,24 @@ type APIAggregator struct {
|
|||
// handledGroups are the groups that already have routes
|
||||
handledGroups sets.String
|
||||
|
||||
// Swagger spec for each api service
|
||||
apiServiceSpecs map[string]*spec.Swagger
|
||||
|
||||
// List of the specs that needs to be loaded. When a spec is successfully loaded
|
||||
// it will be removed from this list and added to apiServiceSpecs.
|
||||
// Map values are retry counts. After a preset retries, it will stop
|
||||
// trying.
|
||||
toLoadAPISpec map[string]int
|
||||
|
||||
// protecting toLoadAPISpec and apiServiceSpecs
|
||||
specMutex sync.Mutex
|
||||
|
||||
// rootSpec is the OpenAPI spec of the Aggregator server.
|
||||
rootSpec *spec.Swagger
|
||||
|
||||
// delegationSpec is the delegation API Server's spec (most of API groups are in this spec).
|
||||
delegationSpec *spec.Swagger
|
||||
|
||||
// lister is used to add group handling for /apis/<group> aggregator lookups based on
|
||||
// controller state
|
||||
lister listers.APIServiceLister
|
||||
|
@ -191,11 +224,14 @@ func (c completedConfig) NewWithDelegate(delegationTarget genericapiserver.Deleg
|
|||
s := &APIAggregator{
|
||||
GenericAPIServer: genericServer,
|
||||
delegateHandler: delegationTarget.UnprotectedHandler(),
|
||||
delegationSpec: delegationTarget.OpenAPISpec(),
|
||||
contextMapper: c.GenericConfig.RequestContextMapper,
|
||||
proxyClientCert: c.ProxyClientCert,
|
||||
proxyClientKey: c.ProxyClientKey,
|
||||
proxyTransport: proxyTransport,
|
||||
proxyHandlers: map[string]*proxyHandler{},
|
||||
apiServiceSpecs: map[string]*spec.Swagger{},
|
||||
toLoadAPISpec: map[string]int{},
|
||||
handledGroups: sets.String{},
|
||||
lister: informerFactory.Apiregistration().InternalVersion().APIServices().Lister(),
|
||||
APIRegistrationInformers: informerFactory,
|
||||
|
@ -244,6 +280,20 @@ func (c completedConfig) NewWithDelegate(delegationTarget genericapiserver.Deleg
|
|||
return nil
|
||||
})
|
||||
|
||||
s.GenericAPIServer.PrepareOpenAPIService()
|
||||
|
||||
if s.GenericAPIServer.OpenAPIService != nil {
|
||||
s.rootSpec = s.GenericAPIServer.OpenAPIService.GetSpec()
|
||||
if err := s.updateOpenAPISpec(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.GenericAPIServer.OpenAPIService.AddUpdateHook(func(r *http.Request) {
|
||||
if s.tryLoadingOpenAPISpecs(r) {
|
||||
s.updateOpenAPISpec()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
@ -274,6 +324,9 @@ func (s *APIAggregator) AddAPIService(apiService *apiregistration.APIService) {
|
|||
}
|
||||
proxyHandler.updateAPIService(apiService)
|
||||
s.proxyHandlers[apiService.Name] = proxyHandler
|
||||
|
||||
s.deferLoadAPISpec(apiService.Name)
|
||||
|
||||
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(proxyPath, proxyHandler)
|
||||
s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandlePrefix(proxyPath+"/", proxyHandler)
|
||||
|
||||
|
@ -302,6 +355,19 @@ func (s *APIAggregator) AddAPIService(apiService *apiregistration.APIService) {
|
|||
s.handledGroups.Insert(apiService.Spec.Group)
|
||||
}
|
||||
|
||||
func (s *APIAggregator) deferLoadAPISpec(name string) {
|
||||
s.specMutex.Lock()
|
||||
defer s.specMutex.Unlock()
|
||||
s.toLoadAPISpec[name] = 0
|
||||
}
|
||||
|
||||
func (s *APIAggregator) deleteApiSpec(name string) {
|
||||
s.specMutex.Lock()
|
||||
defer s.specMutex.Unlock()
|
||||
delete(s.apiServiceSpecs, name)
|
||||
delete(s.toLoadAPISpec, name)
|
||||
}
|
||||
|
||||
// RemoveAPIService removes the APIService from being handled. It is not thread-safe, so only call it on one thread at a time please.
|
||||
// It's a slow moving API, so its ok to run the controller on a single thread.
|
||||
func (s *APIAggregator) RemoveAPIService(apiServiceName string) {
|
||||
|
@ -315,7 +381,124 @@ func (s *APIAggregator) RemoveAPIService(apiServiceName string) {
|
|||
s.GenericAPIServer.Handler.NonGoRestfulMux.Unregister(proxyPath)
|
||||
s.GenericAPIServer.Handler.NonGoRestfulMux.Unregister(proxyPath + "/")
|
||||
delete(s.proxyHandlers, apiServiceName)
|
||||
s.deleteApiSpec(apiServiceName)
|
||||
s.updateOpenAPISpec()
|
||||
|
||||
// TODO unregister group level discovery when there are no more versions for the group
|
||||
// We don't need this right away because the handler properly delegates when no versions are present
|
||||
}
|
||||
|
||||
func (_ *APIAggregator) loadOpenAPISpec(p *proxyHandler, r *http.Request) (*spec.Swagger, error) {
|
||||
value := p.handlingInfo.Load()
|
||||
if value == nil {
|
||||
return nil, nil
|
||||
}
|
||||
handlingInfo := value.(proxyHandlingInfo)
|
||||
if handlingInfo.local {
|
||||
return nil, nil
|
||||
}
|
||||
loc, err := p.routing.ResolveEndpoint(handlingInfo.serviceNamespace, handlingInfo.serviceName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("missing route")
|
||||
}
|
||||
host := loc.Host
|
||||
|
||||
var w io.Reader
|
||||
req, err := http.NewRequest("GET", "/swagger.json", w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.URL.Scheme = "https"
|
||||
req.URL.Host = host
|
||||
|
||||
req = req.WithContext(context.Background())
|
||||
// Get user from the original request
|
||||
ctx, ok := p.contextMapper.Get(r)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing context")
|
||||
}
|
||||
user, ok := genericapirequest.UserFrom(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing user")
|
||||
}
|
||||
proxyRoundTripper := transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), handlingInfo.proxyRoundTripper)
|
||||
res, err := proxyRoundTripper.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(res.Body)
|
||||
bytes := buf.Bytes()
|
||||
var s spec.Swagger
|
||||
if err := json.Unmarshal(bytes, &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Returns true if any Spec is loaded
|
||||
func (s *APIAggregator) tryLoadingOpenAPISpecs(r *http.Request) bool {
|
||||
s.specMutex.Lock()
|
||||
defer s.specMutex.Unlock()
|
||||
if len(s.toLoadAPISpec) == 0 {
|
||||
return false
|
||||
}
|
||||
loaded := false
|
||||
newList := map[string]int{}
|
||||
for name, retries := range s.toLoadAPISpec {
|
||||
if retries >= LOAD_OPENAPI_SPEC_MAX_RETRIES {
|
||||
continue
|
||||
}
|
||||
proxyHandler := s.proxyHandlers[name]
|
||||
if spec, err := s.loadOpenAPISpec(proxyHandler, r); err != nil {
|
||||
glog.Warningf("Failed to Load OpenAPI spec (try %d of %d) for %s, err=%s", retries+1, LOAD_OPENAPI_SPEC_MAX_RETRIES, name, err)
|
||||
newList[name] = retries + 1
|
||||
} else if spec != nil {
|
||||
s.apiServiceSpecs[name] = spec
|
||||
loaded = true
|
||||
}
|
||||
s.toLoadAPISpec = newList
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
func (s *APIAggregator) updateOpenAPISpec() error {
|
||||
s.specMutex.Lock()
|
||||
defer s.specMutex.Unlock()
|
||||
if s.GenericAPIServer.OpenAPIService == nil {
|
||||
return nil
|
||||
}
|
||||
sp, err := openapi.CloneSpec(s.rootSpec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
openapi.FilterSpecByPaths(sp, []string{"/apis/apiregistration.k8s.io/"})
|
||||
if _, found := sp.Paths.Paths["/version/"]; found {
|
||||
return fmt.Errorf("Cleanup didn't work")
|
||||
}
|
||||
if err := openapi.MergeSpecs(sp, s.delegationSpec); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k, v := range s.apiServiceSpecs {
|
||||
version := apiregistration.APIServiceNameToGroupVersion(k)
|
||||
|
||||
proxyPath := "/apis/" + version.Group + "/"
|
||||
// v1. is a special case for the legacy API. It proxies to a wider set of endpoints.
|
||||
if k == legacyAPIServiceName {
|
||||
proxyPath = "/api/"
|
||||
}
|
||||
spc, err := openapi.CloneSpec(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
openapi.FilterSpecByPaths(spc, []string{proxyPath})
|
||||
if err := openapi.MergeSpecs(sp, spc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.GenericAPIServer.OpenAPIService.UpdateSpec(sp)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue