handler.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package handler
  14. import (
  15. "bytes"
  16. "compress/gzip"
  17. "crypto/sha512"
  18. "fmt"
  19. "mime"
  20. "net/http"
  21. "sync"
  22. "time"
  23. "github.com/NYTimes/gziphandler"
  24. "github.com/emicklei/go-restful"
  25. "github.com/go-openapi/spec"
  26. "github.com/golang/protobuf/proto"
  27. "github.com/googleapis/gnostic/OpenAPIv2"
  28. "github.com/googleapis/gnostic/compiler"
  29. "github.com/json-iterator/go"
  30. "github.com/munnerz/goautoneg"
  31. "gopkg.in/yaml.v2"
  32. "k8s.io/kube-openapi/pkg/builder"
  33. "k8s.io/kube-openapi/pkg/common"
  34. )
  35. const (
  36. jsonExt = ".json"
  37. mimeJson = "application/json"
  38. // TODO(mehdy): change @68f4ded to a version tag when gnostic add version tags.
  39. mimePb = "application/com.github.googleapis.gnostic.OpenAPIv2@68f4ded+protobuf"
  40. mimePbGz = "application/x-gzip"
  41. )
  42. // OpenAPIService is the service responsible for serving OpenAPI spec. It has
  43. // the ability to safely change the spec while serving it.
  44. type OpenAPIService struct {
  45. // rwMutex protects All members of this service.
  46. rwMutex sync.RWMutex
  47. lastModified time.Time
  48. specBytes []byte
  49. specPb []byte
  50. specPbGz []byte
  51. specBytesETag string
  52. specPbETag string
  53. specPbGzETag string
  54. }
  55. func init() {
  56. mime.AddExtensionType(".json", mimeJson)
  57. mime.AddExtensionType(".pb-v1", mimePb)
  58. mime.AddExtensionType(".gz", mimePbGz)
  59. }
  60. func computeETag(data []byte) string {
  61. return fmt.Sprintf("\"%X\"", sha512.Sum512(data))
  62. }
  63. // NewOpenAPIService builds an OpenAPIService starting with the given spec.
  64. func NewOpenAPIService(spec *spec.Swagger) (*OpenAPIService, error) {
  65. o := &OpenAPIService{}
  66. if err := o.UpdateSpec(spec); err != nil {
  67. return nil, err
  68. }
  69. return o, nil
  70. }
  71. func (o *OpenAPIService) getSwaggerBytes() ([]byte, string, time.Time) {
  72. o.rwMutex.RLock()
  73. defer o.rwMutex.RUnlock()
  74. return o.specBytes, o.specBytesETag, o.lastModified
  75. }
  76. func (o *OpenAPIService) getSwaggerPbBytes() ([]byte, string, time.Time) {
  77. o.rwMutex.RLock()
  78. defer o.rwMutex.RUnlock()
  79. return o.specPb, o.specPbETag, o.lastModified
  80. }
  81. func (o *OpenAPIService) getSwaggerPbGzBytes() ([]byte, string, time.Time) {
  82. o.rwMutex.RLock()
  83. defer o.rwMutex.RUnlock()
  84. return o.specPbGz, o.specPbGzETag, o.lastModified
  85. }
  86. func (o *OpenAPIService) UpdateSpec(openapiSpec *spec.Swagger) (err error) {
  87. specBytes, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(openapiSpec)
  88. if err != nil {
  89. return err
  90. }
  91. var json map[string]interface{}
  92. if err := jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal(specBytes, &json); err != nil {
  93. return err
  94. }
  95. specPb, err := ToProtoBinary(json)
  96. if err != nil {
  97. return err
  98. }
  99. specPbGz := toGzip(specPb)
  100. specBytesETag := computeETag(specBytes)
  101. specPbETag := computeETag(specPb)
  102. specPbGzETag := computeETag(specPbGz)
  103. lastModified := time.Now()
  104. o.rwMutex.Lock()
  105. defer o.rwMutex.Unlock()
  106. o.specBytes = specBytes
  107. o.specPb = specPb
  108. o.specPbGz = specPbGz
  109. o.specBytesETag = specBytesETag
  110. o.specPbETag = specPbETag
  111. o.specPbGzETag = specPbGzETag
  112. o.lastModified = lastModified
  113. return nil
  114. }
  115. func jsonToYAML(j map[string]interface{}) yaml.MapSlice {
  116. if j == nil {
  117. return nil
  118. }
  119. ret := make(yaml.MapSlice, 0, len(j))
  120. for k, v := range j {
  121. ret = append(ret, yaml.MapItem{k, jsonToYAMLValue(v)})
  122. }
  123. return ret
  124. }
  125. func jsonToYAMLValue(j interface{}) interface{} {
  126. switch j := j.(type) {
  127. case map[string]interface{}:
  128. return jsonToYAML(j)
  129. case []interface{}:
  130. ret := make([]interface{}, len(j))
  131. for i := range j {
  132. ret[i] = jsonToYAMLValue(j[i])
  133. }
  134. return ret
  135. case float64:
  136. // replicate the logic in https://github.com/go-yaml/yaml/blob/51d6538a90f86fe93ac480b35f37b2be17fef232/resolve.go#L151
  137. if i64 := int64(j); j == float64(i64) {
  138. if i := int(i64); i64 == int64(i) {
  139. return i
  140. }
  141. return i64
  142. }
  143. if ui64 := uint64(j); j == float64(ui64) {
  144. return ui64
  145. }
  146. return j
  147. case int64:
  148. if i := int(j); j == int64(i) {
  149. return i
  150. }
  151. return j
  152. }
  153. return j
  154. }
  155. func ToProtoBinary(json map[string]interface{}) ([]byte, error) {
  156. document, err := openapi_v2.NewDocument(jsonToYAML(json), compiler.NewContext("$root", nil))
  157. if err != nil {
  158. return nil, err
  159. }
  160. return proto.Marshal(document)
  161. }
  162. func toGzip(data []byte) []byte {
  163. var buf bytes.Buffer
  164. zw := gzip.NewWriter(&buf)
  165. zw.Write(data)
  166. zw.Close()
  167. return buf.Bytes()
  168. }
  169. // RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec.
  170. //
  171. // Deprecated: use OpenAPIService.RegisterOpenAPIVersionedService instead.
  172. func RegisterOpenAPIVersionedService(spec *spec.Swagger, servePath string, handler common.PathHandler) (*OpenAPIService, error) {
  173. o, err := NewOpenAPIService(spec)
  174. if err != nil {
  175. return nil, err
  176. }
  177. return o, o.RegisterOpenAPIVersionedService(servePath, handler)
  178. }
  179. // RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec.
  180. func (o *OpenAPIService) RegisterOpenAPIVersionedService(servePath string, handler common.PathHandler) error {
  181. accepted := []struct {
  182. Type string
  183. SubType string
  184. GetDataAndETag func() ([]byte, string, time.Time)
  185. }{
  186. {"application", "json", o.getSwaggerBytes},
  187. {"application", "com.github.proto-openapi.spec.v2@v1.0+protobuf", o.getSwaggerPbBytes},
  188. }
  189. handler.Handle(servePath, gziphandler.GzipHandler(http.HandlerFunc(
  190. func(w http.ResponseWriter, r *http.Request) {
  191. decipherableFormats := r.Header.Get("Accept")
  192. if decipherableFormats == "" {
  193. decipherableFormats = "*/*"
  194. }
  195. clauses := goautoneg.ParseAccept(decipherableFormats)
  196. w.Header().Add("Vary", "Accept")
  197. for _, clause := range clauses {
  198. for _, accepts := range accepted {
  199. if clause.Type != accepts.Type && clause.Type != "*" {
  200. continue
  201. }
  202. if clause.SubType != accepts.SubType && clause.SubType != "*" {
  203. continue
  204. }
  205. // serve the first matching media type in the sorted clause list
  206. data, etag, lastModified := accepts.GetDataAndETag()
  207. w.Header().Set("Etag", etag)
  208. // ServeContent will take care of caching using eTag.
  209. http.ServeContent(w, r, servePath, lastModified, bytes.NewReader(data))
  210. return
  211. }
  212. }
  213. // Return 406 for not acceptable format
  214. w.WriteHeader(406)
  215. return
  216. }),
  217. ))
  218. return nil
  219. }
  220. // BuildAndRegisterOpenAPIVersionedService builds the spec and registers a handler to provide access to it.
  221. // Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService.
  222. func BuildAndRegisterOpenAPIVersionedService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
  223. spec, err := builder.BuildOpenAPISpec(webServices, config)
  224. if err != nil {
  225. return nil, err
  226. }
  227. o, err := NewOpenAPIService(spec)
  228. if err != nil {
  229. return nil, err
  230. }
  231. return o, o.RegisterOpenAPIVersionedService(servePath, handler)
  232. }