|
|
|
@@ -17,8 +17,11 @@ limitations under the License.
|
|
|
|
|
package aggregator
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
@@ -26,15 +29,10 @@ import (
|
|
|
|
|
"k8s.io/apiserver/pkg/server"
|
|
|
|
|
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
|
|
|
|
"k8s.io/kube-openapi/pkg/common"
|
|
|
|
|
"k8s.io/kube-openapi/pkg/handler3"
|
|
|
|
|
"k8s.io/kube-openapi/pkg/spec3"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// SpecAggregator calls out to http handlers of APIServices and caches specs. It keeps state of the last
|
|
|
|
|
// known specs including the http etag.
|
|
|
|
|
// TODO(jefftree): remove the downloading and caching and proxy directly to the APIServices. This is possible because we
|
|
|
|
|
// don't have to merge here, which is cpu intensive in v2
|
|
|
|
|
type SpecAggregator interface {
|
|
|
|
|
// SpecProxier proxies OpenAPI V3 requests to their respective APIService
|
|
|
|
|
type SpecProxier interface {
|
|
|
|
|
AddUpdateAPIService(handler http.Handler, apiService *v1.APIService)
|
|
|
|
|
UpdateAPIServiceSpec(apiServiceName string) error
|
|
|
|
|
RemoveAPIServiceSpec(apiServiceName string)
|
|
|
|
@@ -53,37 +51,27 @@ func IsLocalAPIService(apiServiceName string) bool {
|
|
|
|
|
return strings.HasPrefix(apiServiceName, localDelegateChainNamePrefix)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetAPIServicesName returns the names of APIServices recorded in openAPIV3Specs.
|
|
|
|
|
// GetAPIServicesName returns the names of APIServices recorded in apiServiceInfo.
|
|
|
|
|
// We use this function to pass the names of local APIServices to the controller in this package,
|
|
|
|
|
// so that the controller can periodically sync the OpenAPI spec from delegation API servers.
|
|
|
|
|
func (s *specAggregator) GetAPIServiceNames() []string {
|
|
|
|
|
s.rwMutex.Lock()
|
|
|
|
|
defer s.rwMutex.Unlock()
|
|
|
|
|
func (s *specProxier) GetAPIServiceNames() []string {
|
|
|
|
|
s.rwMutex.RLock()
|
|
|
|
|
defer s.rwMutex.RUnlock()
|
|
|
|
|
|
|
|
|
|
names := make([]string, len(s.openAPIV3Specs))
|
|
|
|
|
for key := range s.openAPIV3Specs {
|
|
|
|
|
names := make([]string, len(s.apiServiceInfo))
|
|
|
|
|
for key := range s.apiServiceInfo {
|
|
|
|
|
names = append(names, key)
|
|
|
|
|
}
|
|
|
|
|
return names
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BuildAndRegisterAggregator registered OpenAPI aggregator handler. This function is not thread safe as it only being called on startup.
|
|
|
|
|
func BuildAndRegisterAggregator(downloader Downloader, delegationTarget server.DelegationTarget, pathHandler common.PathHandlerByGroupVersion) (SpecAggregator, error) {
|
|
|
|
|
var err error
|
|
|
|
|
s := &specAggregator{
|
|
|
|
|
openAPIV3Specs: map[string]*openAPIV3APIServiceInfo{},
|
|
|
|
|
func BuildAndRegisterAggregator(downloader Downloader, delegationTarget server.DelegationTarget, pathHandler common.PathHandlerByGroupVersion) (SpecProxier, error) {
|
|
|
|
|
s := &specProxier{
|
|
|
|
|
apiServiceInfo: map[string]*openAPIV3APIServiceInfo{},
|
|
|
|
|
downloader: downloader,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.openAPIV3VersionedService, err = handler3.NewOpenAPIService(nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
err = s.openAPIV3VersionedService.RegisterOpenAPIV3VersionedService("/openapi/v3", pathHandler)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
i := 1
|
|
|
|
|
for delegate := delegationTarget; delegate != nil; delegate = delegate.NextDelegate() {
|
|
|
|
|
handler := delegate.UnprotectedHandler()
|
|
|
|
@@ -98,109 +86,126 @@ func BuildAndRegisterAggregator(downloader Downloader, delegationTarget server.D
|
|
|
|
|
s.UpdateAPIServiceSpec(apiServiceName)
|
|
|
|
|
i++
|
|
|
|
|
}
|
|
|
|
|
s.register(pathHandler)
|
|
|
|
|
|
|
|
|
|
return s, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AddUpdateAPIService adds or updates the api service. It is thread safe.
|
|
|
|
|
func (s *specAggregator) AddUpdateAPIService(handler http.Handler, apiservice *v1.APIService) {
|
|
|
|
|
func (s *specProxier) AddUpdateAPIService(handler http.Handler, apiservice *v1.APIService) {
|
|
|
|
|
s.rwMutex.Lock()
|
|
|
|
|
defer s.rwMutex.Unlock()
|
|
|
|
|
// If the APIService is being updated, use the existing struct.
|
|
|
|
|
if apiServiceInfo, ok := s.openAPIV3Specs[apiservice.Name]; ok {
|
|
|
|
|
if apiServiceInfo, ok := s.apiServiceInfo[apiservice.Name]; ok {
|
|
|
|
|
apiServiceInfo.apiService = *apiservice
|
|
|
|
|
apiServiceInfo.handler = handler
|
|
|
|
|
}
|
|
|
|
|
s.openAPIV3Specs[apiservice.Name] = &openAPIV3APIServiceInfo{
|
|
|
|
|
s.apiServiceInfo[apiservice.Name] = &openAPIV3APIServiceInfo{
|
|
|
|
|
apiService: *apiservice,
|
|
|
|
|
handler: handler,
|
|
|
|
|
specs: make(map[string]*openAPIV3SpecInfo),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdateAPIServiceSpec updates all the OpenAPI v3 specs that the APIService serves.
|
|
|
|
|
// It is thread safe.
|
|
|
|
|
func (s *specAggregator) UpdateAPIServiceSpec(apiServiceName string) error {
|
|
|
|
|
func (s *specProxier) UpdateAPIServiceSpec(apiServiceName string) error {
|
|
|
|
|
s.rwMutex.Lock()
|
|
|
|
|
defer s.rwMutex.Unlock()
|
|
|
|
|
|
|
|
|
|
apiService, exists := s.openAPIV3Specs[apiServiceName]
|
|
|
|
|
apiService, exists := s.apiServiceInfo[apiServiceName]
|
|
|
|
|
if !exists {
|
|
|
|
|
return fmt.Errorf("APIService %s does not exist for update", apiServiceName)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pass a list of old etags to the Downloader to prevent transfers if etags match
|
|
|
|
|
etagList := make(map[string]string)
|
|
|
|
|
for gv, specInfo := range apiService.specs {
|
|
|
|
|
etagList[gv] = specInfo.etag
|
|
|
|
|
}
|
|
|
|
|
groups, err := s.downloader.Download(apiService.handler, etagList)
|
|
|
|
|
gv, err := s.downloader.OpenAPIV3Root(apiService.handler)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove any groups that do not exist anymore
|
|
|
|
|
for group := range s.openAPIV3Specs[apiServiceName].specs {
|
|
|
|
|
if _, exists := groups[group]; !exists {
|
|
|
|
|
s.openAPIV3VersionedService.DeleteGroupVersion(group)
|
|
|
|
|
delete(s.openAPIV3Specs[apiServiceName].specs, group)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for group, info := range groups {
|
|
|
|
|
if info.spec == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If ETag has not changed, no update is necessary
|
|
|
|
|
oldInfo, exists := s.openAPIV3Specs[apiServiceName].specs[group]
|
|
|
|
|
if exists && oldInfo.etag == info.etag {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
s.openAPIV3Specs[apiServiceName].specs[group] = &openAPIV3SpecInfo{
|
|
|
|
|
spec: info.spec,
|
|
|
|
|
etag: info.etag,
|
|
|
|
|
}
|
|
|
|
|
s.openAPIV3VersionedService.UpdateGroupVersion(group, info.spec)
|
|
|
|
|
}
|
|
|
|
|
s.apiServiceInfo[apiServiceName].gvList = gv
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type specAggregator struct {
|
|
|
|
|
type specProxier struct {
|
|
|
|
|
// mutex protects all members of this struct.
|
|
|
|
|
rwMutex sync.RWMutex
|
|
|
|
|
|
|
|
|
|
// OpenAPI V3 specs by APIService name
|
|
|
|
|
openAPIV3Specs map[string]*openAPIV3APIServiceInfo
|
|
|
|
|
// provided for dynamic OpenAPI spec
|
|
|
|
|
openAPIV3VersionedService *handler3.OpenAPIService
|
|
|
|
|
apiServiceInfo map[string]*openAPIV3APIServiceInfo
|
|
|
|
|
|
|
|
|
|
// For downloading the OpenAPI v3 specs from apiservices
|
|
|
|
|
downloader Downloader
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var _ SpecAggregator = &specAggregator{}
|
|
|
|
|
var _ SpecProxier = &specProxier{}
|
|
|
|
|
|
|
|
|
|
type openAPIV3APIServiceInfo struct {
|
|
|
|
|
apiService v1.APIService
|
|
|
|
|
handler http.Handler
|
|
|
|
|
specs map[string]*openAPIV3SpecInfo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type openAPIV3SpecInfo struct {
|
|
|
|
|
spec *spec3.OpenAPI
|
|
|
|
|
etag string
|
|
|
|
|
gvList []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RemoveAPIServiceSpec removes an api service from the OpenAPI map. If it does not exist, no error is returned.
|
|
|
|
|
// It is thread safe.
|
|
|
|
|
func (s *specAggregator) RemoveAPIServiceSpec(apiServiceName string) {
|
|
|
|
|
func (s *specProxier) RemoveAPIServiceSpec(apiServiceName string) {
|
|
|
|
|
s.rwMutex.Lock()
|
|
|
|
|
defer s.rwMutex.Unlock()
|
|
|
|
|
if apiServiceInfo, ok := s.openAPIV3Specs[apiServiceName]; ok {
|
|
|
|
|
for gv := range apiServiceInfo.specs {
|
|
|
|
|
s.openAPIV3VersionedService.DeleteGroupVersion(gv)
|
|
|
|
|
}
|
|
|
|
|
delete(s.openAPIV3Specs, apiServiceName)
|
|
|
|
|
if _, ok := s.apiServiceInfo[apiServiceName]; ok {
|
|
|
|
|
delete(s.apiServiceInfo, apiServiceName)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleDiscovery is the handler for OpenAPI V3 Discovery
|
|
|
|
|
func (s *specProxier) handleDiscovery(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
s.rwMutex.RLock()
|
|
|
|
|
defer s.rwMutex.RUnlock()
|
|
|
|
|
|
|
|
|
|
gvList := make(map[string]bool)
|
|
|
|
|
for _, apiServiceInfo := range s.apiServiceInfo {
|
|
|
|
|
for _, gv := range apiServiceInfo.gvList {
|
|
|
|
|
gvList[gv] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
keys := make([]string, 0, len(gvList))
|
|
|
|
|
for k := range gvList {
|
|
|
|
|
keys = append(keys, k)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sort.Strings(keys)
|
|
|
|
|
output := map[string][]string{"Paths": keys}
|
|
|
|
|
j, err := json.Marshal(output)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
http.ServeContent(w, r, "/openapi/v3", time.Now(), bytes.NewReader(j))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleGroupVersion is the OpenAPI V3 handler for a specified group/version
|
|
|
|
|
func (s *specProxier) handleGroupVersion(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
s.rwMutex.RLock()
|
|
|
|
|
defer s.rwMutex.RUnlock()
|
|
|
|
|
|
|
|
|
|
// TODO: Import this logic from kube-openapi instead of duplicating
|
|
|
|
|
// URLs for OpenAPI V3 have the format /openapi/v3/<groupversionpath>
|
|
|
|
|
// SplitAfterN with 4 yields ["", "openapi", "v3", <groupversionpath>]
|
|
|
|
|
url := strings.SplitAfterN(r.URL.Path, "/", 4)
|
|
|
|
|
targetGV := url[3]
|
|
|
|
|
|
|
|
|
|
for _, apiServiceInfo := range s.apiServiceInfo {
|
|
|
|
|
for _, gv := range apiServiceInfo.gvList {
|
|
|
|
|
if targetGV == gv {
|
|
|
|
|
apiServiceInfo.handler.ServeHTTP(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// No group-versions match the desired request
|
|
|
|
|
w.WriteHeader(404)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register registers the OpenAPI V3 Discovery and GroupVersion handlers
|
|
|
|
|
func (s *specProxier) register(handler common.PathHandlerByGroupVersion) {
|
|
|
|
|
handler.Handle("/openapi/v3", http.HandlerFunc(s.handleDiscovery))
|
|
|
|
|
handler.HandlePrefix("/openapi/v3/", http.HandlerFunc(s.handleGroupVersion))
|
|
|
|
|
}
|
|
|
|
|