@@ -23,7 +23,10 @@ import (
2323 "fmt"
2424 "mime"
2525 "net/http"
26+ "net/url"
27+ "path"
2628 "sort"
29+ "strconv"
2730 "strings"
2831 "sync"
2932 "time"
@@ -49,9 +52,20 @@ const (
4952 subTypeJSON = "json"
5053)
5154
55+ // OpenAPIV3Discovery is the format of the Discovery document for OpenAPI V3
56+ // It maps Discovery paths to their corresponding URLs with a hash parameter included
57+ type OpenAPIV3Discovery struct {
58+ Paths map [string ]OpenAPIV3DiscoveryGroupVersion
59+ }
60+
61+ // OpenAPIV3DiscoveryGroupVersion includes information about a group version and URL
62+ // for accessing the OpenAPI. The URL includes a hash parameter to support client side caching
63+ type OpenAPIV3DiscoveryGroupVersion struct {
64+ URL string
65+ }
66+
5267// OpenAPIService is the service responsible for serving OpenAPI spec. It has
5368// the ability to safely change the spec while serving it.
54- // OpenAPI V3 currently does not use the lazy marshaling strategy that OpenAPI V2 is using
5569type OpenAPIService struct {
5670 // rwMutex protects All members of this service.
5771 rwMutex sync.RWMutex
@@ -66,6 +80,7 @@ type OpenAPIV3Group struct {
6680
6781 pbCache handler.HandlerCache
6882 jsonCache handler.HandlerCache
83+ etagCache handler.HandlerCache
6984}
7085
7186func init () {
@@ -75,7 +90,18 @@ func init() {
7590}
7691
7792func computeETag (data []byte ) string {
78- return fmt .Sprintf ("\" %X\" " , sha512 .Sum512 (data ))
93+ if data == nil {
94+ return ""
95+ }
96+ return fmt .Sprintf ("%X" , sha512 .Sum512 (data ))
97+ }
98+
99+ func constructURL (gvString , etag string ) string {
100+ u := url.URL {Path : path .Join ("/openapi/v3" , gvString )}
101+ query := url.Values {}
102+ query .Set ("hash" , etag )
103+ u .RawQuery = query .Encode ()
104+ return u .String ()
79105}
80106
81107// NewOpenAPIService builds an OpenAPIService starting with the given spec.
@@ -96,10 +122,17 @@ func (o *OpenAPIService) getGroupBytes() ([]byte, error) {
96122 }
97123
98124 sort .Strings (keys )
99- group := make (map [string ][]string )
100- group ["Paths" ] = keys
101-
102- j , err := json .Marshal (group )
125+ discovery := & OpenAPIV3Discovery {Paths : make (map [string ]OpenAPIV3DiscoveryGroupVersion )}
126+ for gvString , groupVersion := range o .v3Schema {
127+ etagBytes , err := groupVersion .etagCache .Get ()
128+ if err != nil {
129+ return nil , err
130+ }
131+ discovery .Paths [gvString ] = OpenAPIV3DiscoveryGroupVersion {
132+ URL : constructURL (gvString , string (etagBytes )),
133+ }
134+ }
135+ j , err := json .Marshal (discovery )
103136 if err != nil {
104137 return nil , err
105138 }
@@ -114,11 +147,19 @@ func (o *OpenAPIService) getSingleGroupBytes(getType string, group string) ([]by
114147 return nil , "" , time .Now (), fmt .Errorf ("Cannot find CRD group %s" , group )
115148 }
116149 if getType == subTypeJSON {
117- specBytes , etag , err := v .jsonCache .Get ()
118- return specBytes , etag , v .lastModified , err
150+ specBytes , err := v .jsonCache .Get ()
151+ if err != nil {
152+ return nil , "" , v .lastModified , err
153+ }
154+ etagBytes , err := v .etagCache .Get ()
155+ return specBytes , string (etagBytes ), v .lastModified , err
119156 } else if getType == subTypeProtobuf {
120- specPb , etag , err := v .pbCache .Get ()
121- return specPb , etag , v .lastModified , err
157+ specPb , err := v .pbCache .Get ()
158+ if err != nil {
159+ return nil , "" , v .lastModified , err
160+ }
161+ etagBytes , err := v .etagCache .Get ()
162+ return specPb , string (etagBytes ), v .lastModified , err
122163 }
123164 return nil , "" , time .Now (), fmt .Errorf ("Invalid accept clause %s" , getType )
124165}
@@ -127,15 +168,10 @@ func (o *OpenAPIService) UpdateGroupVersion(group string, openapi *spec3.OpenAPI
127168 o .rwMutex .Lock ()
128169 defer o .rwMutex .Unlock ()
129170
130- specBytes , err := json .Marshal (openapi )
131- if err != nil {
132- return err
133- }
134-
135171 if _ , ok := o .v3Schema [group ]; ! ok {
136172 o .v3Schema [group ] = & OpenAPIV3Group {}
137173 }
138- return o .v3Schema [group ].UpdateSpec (specBytes )
174+ return o .v3Schema [group ].UpdateSpec (openapi )
139175}
140176
141177func (o * OpenAPIService ) DeleteGroupVersion (group string ) {
@@ -192,7 +228,26 @@ func (o *OpenAPIService) HandleGroupVersion(w http.ResponseWriter, r *http.Reque
192228 if err != nil {
193229 return
194230 }
195- w .Header ().Set ("Etag" , etag )
231+ // ETag must be enclosed in double quotes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
232+ w .Header ().Set ("Etag" , strconv .Quote (etag ))
233+
234+ if hash := r .URL .Query ().Get ("hash" ); hash != "" {
235+ if hash != etag {
236+ u := constructURL (group , etag )
237+ http .Redirect (w , r , u , 301 )
238+ return
239+ }
240+ // The Vary header is required because the Accept header can
241+ // change the contents returned. This prevents clients from caching
242+ // protobuf as JSON and vice versa.
243+ w .Header ().Set ("Vary" , "Accept" )
244+
245+ // Only set these headers when a hash is given.
246+ w .Header ().Set ("Cache-Control" , "public, immutable" )
247+ // Set the Expires directive to the maximum value of one year from the request,
248+ // effectively indicating that the cache never expires.
249+ w .Header ().Set ("Expires" , time .Now ().AddDate (1 , 0 , 0 ).Format (time .RFC1123 ))
250+ }
196251 http .ServeContent (w , r , "" , lastModified , bytes .NewReader (data ))
197252 return
198253 }
@@ -207,16 +262,28 @@ func (o *OpenAPIService) RegisterOpenAPIV3VersionedService(servePath string, han
207262 return nil
208263}
209264
210- func (o * OpenAPIV3Group ) UpdateSpec (specBytes [] byte ) (err error ) {
265+ func (o * OpenAPIV3Group ) UpdateSpec (openapi * spec3. OpenAPI ) (err error ) {
211266 o .rwMutex .Lock ()
212267 defer o .rwMutex .Unlock ()
213268
269+ o .jsonCache = o .jsonCache .New (func () ([]byte , error ) {
270+ return json .Marshal (openapi )
271+ })
214272 o .pbCache = o .pbCache .New (func () ([]byte , error ) {
215- return ToV3ProtoBinary (specBytes )
273+ json , err := o .jsonCache .Get ()
274+ if err != nil {
275+ return nil , err
276+ }
277+ return ToV3ProtoBinary (json )
216278 })
217-
218- o .jsonCache = o .jsonCache .New (func () ([]byte , error ) {
219- return specBytes , nil
279+ // TODO: This forces a json marshal of corresponding group-versions.
280+ // We should look to replace this with a faster hashing mechanism.
281+ o .etagCache = o .etagCache .New (func () ([]byte , error ) {
282+ json , err := o .jsonCache .Get ()
283+ if err != nil {
284+ return nil , err
285+ }
286+ return []byte (computeETag (json )), nil
220287 })
221288 o .lastModified = time .Now ()
222289 return nil
0 commit comments