@@ -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 ) {
@@ -157,6 +193,21 @@ func (o *OpenAPIService) HandleDiscovery(w http.ResponseWriter, r *http.Request)
157193	http .ServeContent (w , r , "/openapi/v3" , time .Now (), bytes .NewReader (data ))
158194}
159195
196+ func  applyCacheBusting (w  http.ResponseWriter , r  * http.Request , etag  string ) {
197+ 	hash  :=  r .URL .Query ().Get ("hash" )
198+ 	// Only apply cache busting on requests where the hash is provided. 
199+ 	if  hash  !=  ""  {
200+ 		// The Vary header is required because the Accept header can change the contents returned. This prevents clients from caching protobuf as JSON and vice versa. 
201+ 		w .Header ().Set ("Vary" , "*" )
202+ 
203+ 		if  hash  ==  etag  {
204+ 			// Set the Expires directive to one year from the request, effectively indicating that the cache never expires. 
205+ 			w .Header ().Set ("Expires" , time .Now ().AddDate (1 , 0 , 0 ).Format (time .RFC1123 ))
206+ 			w .Header ().Set ("Cache-Control" , "public, immutable" )
207+ 		}
208+ 	}
209+ }
210+ 
160211func  (o  * OpenAPIService ) HandleGroupVersion (w  http.ResponseWriter , r  * http.Request ) {
161212	url  :=  strings .SplitAfterN (r .URL .Path , "/" , 4 )
162213	group  :=  url [3 ]
@@ -192,7 +243,15 @@ func (o *OpenAPIService) HandleGroupVersion(w http.ResponseWriter, r *http.Reque
192243			if  err  !=  nil  {
193244				return 
194245			}
195- 			w .Header ().Set ("Etag" , etag )
246+ 			// ETag must be enclosed in double quotes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag 
247+ 			w .Header ().Set ("Etag" , strconv .Quote (etag ))
248+ 
249+ 			if  hash  :=  r .URL .Query ().Get ("hash" ); hash  !=  ""  &&  hash  !=  etag  {
250+ 				u  :=  constructURL (group , etag )
251+ 				http .Redirect (w , r , u , 301 )
252+ 				return 
253+ 			}
254+ 			applyCacheBusting (w , r , etag )
196255			http .ServeContent (w , r , "" , lastModified , bytes .NewReader (data ))
197256			return 
198257		}
@@ -207,16 +266,26 @@ func (o *OpenAPIService) RegisterOpenAPIV3VersionedService(servePath string, han
207266	return  nil 
208267}
209268
210- func  (o  * OpenAPIV3Group ) UpdateSpec (specBytes  [] byte ) (err  error ) {
269+ func  (o  * OpenAPIV3Group ) UpdateSpec (openapi   * spec3. OpenAPI ) (err  error ) {
211270	o .rwMutex .Lock ()
212271	defer  o .rwMutex .Unlock ()
213272
214273	o .pbCache  =  o .pbCache .New (func () ([]byte , error ) {
215- 		return  ToV3ProtoBinary (specBytes )
274+ 		json , err  :=  o .jsonCache .Get ()
275+ 		if  err  !=  nil  {
276+ 			return  nil , err 
277+ 		}
278+ 		return  ToV3ProtoBinary (json )
216279	})
217- 
218280	o .jsonCache  =  o .jsonCache .New (func () ([]byte , error ) {
219- 		return  specBytes , nil 
281+ 		return  json .Marshal (openapi )
282+ 	})
283+ 	o .etagCache  =  o .etagCache .New (func () ([]byte , error ) {
284+ 		json , err  :=  o .jsonCache .Get ()
285+ 		if  err  !=  nil  {
286+ 			return  nil , err 
287+ 		}
288+ 		return  []byte (computeETag (json )), nil 
220289	})
221290	o .lastModified  =  time .Now ()
222291	return  nil 
0 commit comments