blob: c4bbc0bff34fed043331ca3e1d657e826ef6be1d [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
2Copyright 2017 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package handler
18
19import (
20 "bytes"
21 "compress/gzip"
22 "crypto/sha512"
23 "encoding/json"
24 "fmt"
25 "mime"
26 "net/http"
27 "strings"
28 "sync"
29 "time"
30
31 "bitbucket.org/ww/goautoneg"
32
33 yaml "gopkg.in/yaml.v2"
34
35 "github.com/NYTimes/gziphandler"
36 restful "github.com/emicklei/go-restful"
37 "github.com/go-openapi/spec"
38 "github.com/golang/protobuf/proto"
39 openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
40 "github.com/googleapis/gnostic/compiler"
41
42 "k8s.io/kube-openapi/pkg/builder"
43 "k8s.io/kube-openapi/pkg/common"
44)
45
46const (
47 jsonExt = ".json"
48
49 mimeJson = "application/json"
50 // TODO(mehdy): change @68f4ded to a version tag when gnostic add version tags.
51 mimePb = "application/com.github.googleapis.gnostic.OpenAPIv2@68f4ded+protobuf"
52 mimePbGz = "application/x-gzip"
53)
54
55// OpenAPIService is the service responsible for serving OpenAPI spec. It has
56// the ability to safely change the spec while serving it.
57type OpenAPIService struct {
58 // rwMutex protects All members of this service.
59 rwMutex sync.RWMutex
60
61 lastModified time.Time
62
63 specBytes []byte
64 specPb []byte
65 specPbGz []byte
66
67 specBytesETag string
68 specPbETag string
69 specPbGzETag string
70}
71
72func init() {
73 mime.AddExtensionType(".json", mimeJson)
74 mime.AddExtensionType(".pb-v1", mimePb)
75 mime.AddExtensionType(".gz", mimePbGz)
76}
77
78func computeETag(data []byte) string {
79 return fmt.Sprintf("\"%X\"", sha512.Sum512(data))
80}
81
82// NOTE: [DEPRECATION] We will announce deprecation for format-separated endpoints for OpenAPI spec,
83// and switch to a single /openapi/v2 endpoint in Kubernetes 1.10. The design doc and deprecation process
84// are tracked at: https://docs.google.com/document/d/19lEqE9lc4yHJ3WJAJxS_G7TcORIJXGHyq3wpwcH28nU.
85//
86// BuildAndRegisterOpenAPIService builds the spec and registers a handler to provide access to it.
87// Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIService.
88func BuildAndRegisterOpenAPIService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
89 spec, err := builder.BuildOpenAPISpec(webServices, config)
90 if err != nil {
91 return nil, err
92 }
93 return RegisterOpenAPIService(spec, servePath, handler)
94}
95
96// NOTE: [DEPRECATION] We will announce deprecation for format-separated endpoints for OpenAPI spec,
97// and switch to a single /openapi/v2 endpoint in Kubernetes 1.10. The design doc and deprecation process
98// are tracked at: https://docs.google.com/document/d/19lEqE9lc4yHJ3WJAJxS_G7TcORIJXGHyq3wpwcH28nU.
99//
100// RegisterOpenAPIService registers a handler to provide access to provided swagger spec.
101// Note: servePath should end with ".json" as the RegisterOpenAPIService assume it is serving a
102// json file and will also serve .pb and .gz files.
103func RegisterOpenAPIService(openapiSpec *spec.Swagger, servePath string, handler common.PathHandler) (*OpenAPIService, error) {
104 if !strings.HasSuffix(servePath, jsonExt) {
105 return nil, fmt.Errorf("serving path must end with \"%s\"", jsonExt)
106 }
107
108 servePathBase := strings.TrimSuffix(servePath, jsonExt)
109
110 o := OpenAPIService{}
111 if err := o.UpdateSpec(openapiSpec); err != nil {
112 return nil, err
113 }
114
115 type fileInfo struct {
116 ext string
117 getDataAndETag func() ([]byte, string, time.Time)
118 }
119
120 files := []fileInfo{
121 {".json", o.getSwaggerBytes},
122 {"-2.0.0.json", o.getSwaggerBytes},
123 {"-2.0.0.pb-v1", o.getSwaggerPbBytes},
124 {"-2.0.0.pb-v1.gz", o.getSwaggerPbGzBytes},
125 }
126
127 for _, file := range files {
128 path := servePathBase + file.ext
129 getDataAndETag := file.getDataAndETag
130 handler.Handle(path, gziphandler.GzipHandler(http.HandlerFunc(
131 func(w http.ResponseWriter, r *http.Request) {
132 data, etag, lastModified := getDataAndETag()
133 w.Header().Set("Etag", etag)
134
135 // ServeContent will take care of caching using eTag.
136 http.ServeContent(w, r, path, lastModified, bytes.NewReader(data))
137 }),
138 ))
139 }
140
141 return &o, nil
142}
143
144func (o *OpenAPIService) getSwaggerBytes() ([]byte, string, time.Time) {
145 o.rwMutex.RLock()
146 defer o.rwMutex.RUnlock()
147 return o.specBytes, o.specBytesETag, o.lastModified
148}
149
150func (o *OpenAPIService) getSwaggerPbBytes() ([]byte, string, time.Time) {
151 o.rwMutex.RLock()
152 defer o.rwMutex.RUnlock()
153 return o.specPb, o.specPbETag, o.lastModified
154}
155
156func (o *OpenAPIService) getSwaggerPbGzBytes() ([]byte, string, time.Time) {
157 o.rwMutex.RLock()
158 defer o.rwMutex.RUnlock()
159 return o.specPbGz, o.specPbGzETag, o.lastModified
160}
161
162func (o *OpenAPIService) UpdateSpec(openapiSpec *spec.Swagger) (err error) {
163 specBytes, err := json.MarshalIndent(openapiSpec, " ", " ")
164 if err != nil {
165 return err
166 }
167 specPb, err := toProtoBinary(specBytes)
168 if err != nil {
169 return err
170 }
171 specPbGz := toGzip(specPb)
172
173 specBytesETag := computeETag(specBytes)
174 specPbETag := computeETag(specPb)
175 specPbGzETag := computeETag(specPbGz)
176
177 lastModified := time.Now()
178
179 o.rwMutex.Lock()
180 defer o.rwMutex.Unlock()
181
182 o.specBytes = specBytes
183 o.specPb = specPb
184 o.specPbGz = specPbGz
185 o.specBytesETag = specBytesETag
186 o.specPbETag = specPbETag
187 o.specPbGzETag = specPbGzETag
188 o.lastModified = lastModified
189
190 return nil
191}
192
193func toProtoBinary(spec []byte) ([]byte, error) {
194 var info yaml.MapSlice
195 err := yaml.Unmarshal(spec, &info)
196 if err != nil {
197 return nil, err
198 }
199 document, err := openapi_v2.NewDocument(info, compiler.NewContext("$root", nil))
200 if err != nil {
201 return nil, err
202 }
203 return proto.Marshal(document)
204}
205
206func toGzip(data []byte) []byte {
207 var buf bytes.Buffer
208 zw := gzip.NewWriter(&buf)
209 zw.Write(data)
210 zw.Close()
211 return buf.Bytes()
212}
213
214// RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec.
215func RegisterOpenAPIVersionedService(openapiSpec *spec.Swagger, servePath string, handler common.PathHandler) (*OpenAPIService, error) {
216 o := OpenAPIService{}
217 if err := o.UpdateSpec(openapiSpec); err != nil {
218 return nil, err
219 }
220
221 accepted := []struct {
222 Type string
223 SubType string
224 GetDataAndETag func() ([]byte, string, time.Time)
225 }{
226 {"application", "json", o.getSwaggerBytes},
227 {"application", "com.github.proto-openapi.spec.v2@v1.0+protobuf", o.getSwaggerPbBytes},
228 }
229
230 handler.Handle(servePath, gziphandler.GzipHandler(http.HandlerFunc(
231 func(w http.ResponseWriter, r *http.Request) {
232 decipherableFormats := r.Header.Get("Accept")
233 if decipherableFormats == "" {
234 decipherableFormats = "*/*"
235 }
236 clauses := goautoneg.ParseAccept(decipherableFormats)
237 w.Header().Add("Vary", "Accept")
238 for _, clause := range clauses {
239 for _, accepts := range accepted {
240 if clause.Type != accepts.Type && clause.Type != "*" {
241 continue
242 }
243 if clause.SubType != accepts.SubType && clause.SubType != "*" {
244 continue
245 }
246
247 // serve the first matching media type in the sorted clause list
248 data, etag, lastModified := accepts.GetDataAndETag()
249 w.Header().Set("Etag", etag)
250 // ServeContent will take care of caching using eTag.
251 http.ServeContent(w, r, servePath, lastModified, bytes.NewReader(data))
252 return
253 }
254 }
255 // Return 406 for not acceptable format
256 w.WriteHeader(406)
257 return
258 }),
259 ))
260
261 return &o, nil
262}
263
264// BuildAndRegisterOpenAPIVersionedService builds the spec and registers a handler to provide access to it.
265// Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService.
266func BuildAndRegisterOpenAPIVersionedService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
267 spec, err := builder.BuildOpenAPISpec(webServices, config)
268 if err != nil {
269 return nil, err
270 }
271 return RegisterOpenAPIVersionedService(spec, servePath, handler)
272}