Matthias Andreas Benkard | 832a54e | 2019-01-29 09:27:38 +0100 | [diff] [blame^] | 1 | /* |
| 2 | Copyright 2017 The Kubernetes Authors. |
| 3 | |
| 4 | Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | you may not use this file except in compliance with the License. |
| 6 | You may obtain a copy of the License at |
| 7 | |
| 8 | http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | |
| 10 | Unless required by applicable law or agreed to in writing, software |
| 11 | distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | See the License for the specific language governing permissions and |
| 14 | limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package handler |
| 18 | |
| 19 | import ( |
| 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 | |
| 46 | const ( |
| 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. |
| 57 | type 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 | |
| 72 | func init() { |
| 73 | mime.AddExtensionType(".json", mimeJson) |
| 74 | mime.AddExtensionType(".pb-v1", mimePb) |
| 75 | mime.AddExtensionType(".gz", mimePbGz) |
| 76 | } |
| 77 | |
| 78 | func 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. |
| 88 | func 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. |
| 103 | func 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 | |
| 144 | func (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 | |
| 150 | func (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 | |
| 156 | func (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 | |
| 162 | func (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 | |
| 193 | func 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 | |
| 206 | func 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. |
| 215 | func 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. |
| 266 | func 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 | } |