1
16
17 package handler
18
19 import (
20 "bytes"
21 "crypto/sha512"
22 "fmt"
23 "net/http"
24 "strconv"
25 "time"
26
27 "github.com/NYTimes/gziphandler"
28 "github.com/emicklei/go-restful/v3"
29 "github.com/golang/protobuf/proto"
30 openapi_v2 "github.com/google/gnostic-models/openapiv2"
31 "github.com/google/uuid"
32 "github.com/munnerz/goautoneg"
33
34 klog "k8s.io/klog/v2"
35 "k8s.io/kube-openapi/pkg/builder"
36 "k8s.io/kube-openapi/pkg/cached"
37 "k8s.io/kube-openapi/pkg/common"
38 "k8s.io/kube-openapi/pkg/common/restfuladapter"
39 "k8s.io/kube-openapi/pkg/validation/spec"
40 )
41
42 const (
43 subTypeProtobufDeprecated = "com.github.proto-openapi.spec.v2@v1.0+protobuf"
44 subTypeProtobuf = "com.github.proto-openapi.spec.v2.v1.0+protobuf"
45 subTypeJSON = "json"
46 )
47
48 func computeETag(data []byte) string {
49 if data == nil {
50 return ""
51 }
52 return fmt.Sprintf("%X", sha512.Sum512(data))
53 }
54
55 type timedSpec struct {
56 spec []byte
57 lastModified time.Time
58 }
59
60
61
62 type OpenAPIService struct {
63 specCache cached.LastSuccess[*spec.Swagger]
64 jsonCache cached.Value[timedSpec]
65 protoCache cached.Value[timedSpec]
66 }
67
68
69 func NewOpenAPIService(swagger *spec.Swagger) *OpenAPIService {
70 return NewOpenAPIServiceLazy(cached.Static(swagger, uuid.New().String()))
71 }
72
73
74 func NewOpenAPIServiceLazy(swagger cached.Value[*spec.Swagger]) *OpenAPIService {
75 o := &OpenAPIService{}
76 o.UpdateSpecLazy(swagger)
77
78 o.jsonCache = cached.Transform[*spec.Swagger](func(spec *spec.Swagger, etag string, err error) (timedSpec, string, error) {
79 if err != nil {
80 return timedSpec{}, "", err
81 }
82 json, err := spec.MarshalJSON()
83 if err != nil {
84 return timedSpec{}, "", err
85 }
86 return timedSpec{spec: json, lastModified: time.Now()}, computeETag(json), nil
87 }, &o.specCache)
88 o.protoCache = cached.Transform(func(ts timedSpec, etag string, err error) (timedSpec, string, error) {
89 if err != nil {
90 return timedSpec{}, "", err
91 }
92 proto, err := ToProtoBinary(ts.spec)
93 if err != nil {
94 return timedSpec{}, "", err
95 }
96
97 return timedSpec{spec: proto, lastModified: ts.lastModified}, etag, nil
98 }, o.jsonCache)
99 return o
100 }
101
102 func (o *OpenAPIService) UpdateSpec(swagger *spec.Swagger) error {
103 o.UpdateSpecLazy(cached.Static(swagger, uuid.New().String()))
104 return nil
105 }
106
107 func (o *OpenAPIService) UpdateSpecLazy(swagger cached.Value[*spec.Swagger]) {
108 o.specCache.Store(swagger)
109 }
110
111 func ToProtoBinary(json []byte) ([]byte, error) {
112 document, err := openapi_v2.ParseDocument(json)
113 if err != nil {
114 return nil, err
115 }
116 return proto.Marshal(document)
117 }
118
119
120
121
122 func RegisterOpenAPIVersionedService(spec *spec.Swagger, servePath string, handler common.PathHandler) *OpenAPIService {
123 o := NewOpenAPIService(spec)
124 o.RegisterOpenAPIVersionedService(servePath, handler)
125 return o
126 }
127
128
129 func (o *OpenAPIService) RegisterOpenAPIVersionedService(servePath string, handler common.PathHandler) {
130 accepted := []struct {
131 Type string
132 SubType string
133 ReturnedContentType string
134 GetDataAndEtag cached.Value[timedSpec]
135 }{
136 {"application", subTypeJSON, "application/" + subTypeJSON, o.jsonCache},
137 {"application", subTypeProtobufDeprecated, "application/" + subTypeProtobuf, o.protoCache},
138 {"application", subTypeProtobuf, "application/" + subTypeProtobuf, o.protoCache},
139 }
140
141 handler.Handle(servePath, gziphandler.GzipHandler(http.HandlerFunc(
142 func(w http.ResponseWriter, r *http.Request) {
143 decipherableFormats := r.Header.Get("Accept")
144 if decipherableFormats == "" {
145 decipherableFormats = "*/*"
146 }
147 clauses := goautoneg.ParseAccept(decipherableFormats)
148 w.Header().Add("Vary", "Accept")
149 for _, clause := range clauses {
150 for _, accepts := range accepted {
151 if clause.Type != accepts.Type && clause.Type != "*" {
152 continue
153 }
154 if clause.SubType != accepts.SubType && clause.SubType != "*" {
155 continue
156 }
157
158 ts, etag, err := accepts.GetDataAndEtag.Get()
159 if err != nil {
160 klog.Errorf("Error in OpenAPI handler: %s", err)
161
162 if ts.spec == nil {
163 w.WriteHeader(http.StatusServiceUnavailable)
164 return
165 }
166 }
167
168 w.Header().Set("Content-Type", accepts.ReturnedContentType)
169
170
171 w.Header().Set("Etag", strconv.Quote(etag))
172
173 http.ServeContent(w, r, servePath, ts.lastModified, bytes.NewReader(ts.spec))
174 return
175 }
176 }
177
178 w.WriteHeader(406)
179 return
180 }),
181 ))
182 }
183
184
185
186
187
188 func BuildAndRegisterOpenAPIVersionedService(servePath string, webServices []*restful.WebService, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
189 return BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath, restfuladapter.AdaptWebServices(webServices), config, handler)
190 }
191
192
193
194 func BuildAndRegisterOpenAPIVersionedServiceFromRoutes(servePath string, routeContainers []common.RouteContainer, config *common.Config, handler common.PathHandler) (*OpenAPIService, error) {
195 spec, err := builder.BuildOpenAPISpecFromRoutes(routeContainers, config)
196 if err != nil {
197 return nil, err
198 }
199 o := NewOpenAPIService(spec)
200 o.RegisterOpenAPIVersionedService(servePath, handler)
201 return o, nil
202 }
203
View as plain text