k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/handler/handler.go (about)

     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  	"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  // OpenAPIService is the service responsible for serving OpenAPI spec. It has
    61  // the ability to safely change the spec while serving it.
    62  type OpenAPIService struct {
    63  	specCache  cached.LastSuccess[*spec.Swagger]
    64  	jsonCache  cached.Value[timedSpec]
    65  	protoCache cached.Value[timedSpec]
    66  }
    67  
    68  // NewOpenAPIService builds an OpenAPIService starting with the given spec.
    69  func NewOpenAPIService(swagger *spec.Swagger) *OpenAPIService {
    70  	return NewOpenAPIServiceLazy(cached.Static(swagger, uuid.New().String()))
    71  }
    72  
    73  // NewOpenAPIServiceLazy builds an OpenAPIService from lazy spec.
    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  		// We can re-use the same etag as json because of the Vary header.
    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  // RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec.
   120  //
   121  // Deprecated: use OpenAPIService.RegisterOpenAPIVersionedService instead.
   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  // RegisterOpenAPIVersionedService registers a handler to provide access to provided swagger spec.
   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  					// serve the first matching media type in the sorted clause list
   158  					ts, etag, err := accepts.GetDataAndEtag.Get()
   159  					if err != nil {
   160  						klog.Errorf("Error in OpenAPI handler: %s", err)
   161  						// only return a 503 if we have no older cache data to serve
   162  						if ts.spec == nil {
   163  							w.WriteHeader(http.StatusServiceUnavailable)
   164  							return
   165  						}
   166  					}
   167  					// Set Content-Type header in the reponse
   168  					w.Header().Set("Content-Type", accepts.ReturnedContentType)
   169  
   170  					// ETag must be enclosed in double quotes: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
   171  					w.Header().Set("Etag", strconv.Quote(etag))
   172  					// ServeContent will take care of caching using eTag.
   173  					http.ServeContent(w, r, servePath, ts.lastModified, bytes.NewReader(ts.spec))
   174  					return
   175  				}
   176  			}
   177  			// Return 406 for not acceptable format
   178  			w.WriteHeader(406)
   179  			return
   180  		}),
   181  	))
   182  }
   183  
   184  // BuildAndRegisterOpenAPIVersionedService builds the spec and registers a handler to provide access to it.
   185  // Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService.
   186  //
   187  // Deprecated: BuildAndRegisterOpenAPIVersionedServiceFromRoutes should be used instead.
   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  // BuildAndRegisterOpenAPIVersionedServiceFromRoutes builds the spec and registers a handler to provide access to it.
   193  // Use this method if your OpenAPI spec is static. If you want to update the spec, use BuildOpenAPISpec then RegisterOpenAPIVersionedService.
   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  }