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: #43717
pull/6/head
Kubernetes Submit Queue 2017-06-05 06:51:20 -07:00 committed by GitHub
commit a72967454d
20 changed files with 2339 additions and 142 deletions

View File

@ -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": [

View File

@ -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.

View File

@ -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",
],

View File

@ -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)

View File

@ -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),

View File

@ -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))
}

View File

@ -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",

View File

@ -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 {

View File

@ -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",

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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))
}

View File

@ -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)
}
}

View File

@ -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"):]

View File

@ -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
}

View File

@ -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.

View File

@ -105,6 +105,8 @@ message APIServiceSpec {
message APIServiceStatus {
// Current service state of apiService.
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
repeated APIServiceCondition conditions = 1;
}

View File

@ -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"`
}

View File

@ -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",

View File

@ -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)
}