mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 03:11:40 +00:00
add new aggregated resourcemanager to genericapiserver
Co-authored-by: Jeffrey Ying <jeffrey.ying86@live.com>
This commit is contained in:
parent
76f056867a
commit
6e83f67505
@ -40,6 +40,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||||
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
|
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
|
||||||
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
|
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
|
||||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
@ -481,6 +482,9 @@ func buildGenericConfig(
|
|||||||
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIPriorityAndFairness) && s.GenericServerRunOptions.EnablePriorityAndFairness {
|
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIPriorityAndFairness) && s.GenericServerRunOptions.EnablePriorityAndFairness {
|
||||||
genericConfig.FlowControl, lastErr = BuildPriorityAndFairness(s, clientgoExternalClient, versionedInformers)
|
genericConfig.FlowControl, lastErr = BuildPriorityAndFairness(s, clientgoExternalClient, versionedInformers)
|
||||||
}
|
}
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryEndpoint) {
|
||||||
|
genericConfig.AggregatedDiscoveryGroupManager = aggregated.NewResourceManager()
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -254,7 +254,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
|
|||||||
group.GroupVersion = grouplessGroupVersion
|
group.GroupVersion = grouplessGroupVersion
|
||||||
group.OptionsExternalVersion = &grouplessGroupVersion
|
group.OptionsExternalVersion = &grouplessGroupVersion
|
||||||
group.Serializer = codecs
|
group.Serializer = codecs
|
||||||
if _, err := (&group).InstallREST(container); err != nil {
|
if _, _, err := (&group).InstallREST(container); err != nil {
|
||||||
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
|
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,7 +266,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
|
|||||||
group.GroupVersion = testGroupVersion
|
group.GroupVersion = testGroupVersion
|
||||||
group.OptionsExternalVersion = &testGroupVersion
|
group.OptionsExternalVersion = &testGroupVersion
|
||||||
group.Serializer = codecs
|
group.Serializer = codecs
|
||||||
if _, err := (&group).InstallREST(container); err != nil {
|
if _, _, err := (&group).InstallREST(container); err != nil {
|
||||||
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
|
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,7 +278,7 @@ func handleInternal(storage map[string]rest.Storage, admissionControl admission.
|
|||||||
group.GroupVersion = newGroupVersion
|
group.GroupVersion = newGroupVersion
|
||||||
group.OptionsExternalVersion = &newGroupVersion
|
group.OptionsExternalVersion = &newGroupVersion
|
||||||
group.Serializer = codecs
|
group.Serializer = codecs
|
||||||
if _, err := (&group).InstallREST(container); err != nil {
|
if _, _, err := (&group).InstallREST(container); err != nil {
|
||||||
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
|
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3311,7 +3311,7 @@ func TestParentResourceIsRequired(t *testing.T) {
|
|||||||
ParameterCodec: parameterCodec,
|
ParameterCodec: parameterCodec,
|
||||||
}
|
}
|
||||||
container := restful.NewContainer()
|
container := restful.NewContainer()
|
||||||
if _, err := group.InstallREST(container); err == nil {
|
if _, _, err := group.InstallREST(container); err == nil {
|
||||||
t.Fatal("expected error")
|
t.Fatal("expected error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3343,7 +3343,7 @@ func TestParentResourceIsRequired(t *testing.T) {
|
|||||||
ParameterCodec: parameterCodec,
|
ParameterCodec: parameterCodec,
|
||||||
}
|
}
|
||||||
container = restful.NewContainer()
|
container = restful.NewContainer()
|
||||||
if _, err := group.InstallREST(container); err != nil {
|
if _, _, err := group.InstallREST(container); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4328,7 +4328,7 @@ func TestXGSubresource(t *testing.T) {
|
|||||||
Serializer: codecs,
|
Serializer: codecs,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := (&group).InstallREST(container); err != nil {
|
if _, _, err := (&group).InstallREST(container); err != nil {
|
||||||
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
|
panic(fmt.Sprintf("unable to install container %s: %v", group.GroupVersion, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aggregated
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha512"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This file exposes helper functions used for calculating the E-Tag header
|
||||||
|
// used in discovery endpoint responses
|
||||||
|
|
||||||
|
// Attaches Cache-Busting functionality to an endpoint
|
||||||
|
// - Sets ETag header to provided hash
|
||||||
|
// - Replies with 304 Not Modified, if If-None-Match header matches hash
|
||||||
|
//
|
||||||
|
// hash should be the value of calculateETag on object. If hash is empty, then
|
||||||
|
//
|
||||||
|
// the object is simply serialized without E-Tag functionality
|
||||||
|
func ServeHTTPWithETag(
|
||||||
|
object runtime.Object,
|
||||||
|
hash string,
|
||||||
|
serializer runtime.NegotiatedSerializer,
|
||||||
|
w http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
) {
|
||||||
|
// ETag must be enclosed in double quotes:
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
|
||||||
|
quotedHash := strconv.Quote(hash)
|
||||||
|
w.Header().Set("ETag", quotedHash)
|
||||||
|
w.Header().Set("Vary", "Accept")
|
||||||
|
w.Header().Set("Cache-Control", "public")
|
||||||
|
|
||||||
|
// If Request includes If-None-Match and matches hash, reply with 304
|
||||||
|
// Otherwise, we delegate to the handler for actual content
|
||||||
|
//
|
||||||
|
// According to documentation, An Etag within an If-None-Match
|
||||||
|
// header will be enclosed within doule quotes:
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match#directives
|
||||||
|
if clientCachedHash := req.Header.Get("If-None-Match"); quotedHash == clientCachedHash {
|
||||||
|
w.WriteHeader(http.StatusNotModified)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responsewriters.WriteObjectNegotiated(
|
||||||
|
serializer,
|
||||||
|
DiscoveryEndpointRestrictions,
|
||||||
|
AggregatedDiscoveryGV,
|
||||||
|
w,
|
||||||
|
req,
|
||||||
|
http.StatusOK,
|
||||||
|
object,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateETag(resources interface{}) (string, error) {
|
||||||
|
serialized, err := json.Marshal(resources)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%X", sha512.Sum512(serialized)), nil
|
||||||
|
}
|
@ -0,0 +1,170 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aggregated
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful/v3"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FakeResourceManager interface {
|
||||||
|
ResourceManager
|
||||||
|
Expect() ResourceManager
|
||||||
|
|
||||||
|
HasExpectedNumberActions() bool
|
||||||
|
Validate() error
|
||||||
|
WaitForActions(ctx context.Context, timeout time.Duration) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFakeResourceManager() FakeResourceManager {
|
||||||
|
return &fakeResourceManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a resource manager with helper functions for checking the actions
|
||||||
|
// match expected. For Use in tests
|
||||||
|
type fakeResourceManager struct {
|
||||||
|
recorderResourceManager
|
||||||
|
expect recorderResourceManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// a resource manager which instead of managing a discovery document,
|
||||||
|
// simply records the calls to its interface functoins for testing
|
||||||
|
type recorderResourceManager struct {
|
||||||
|
lock sync.RWMutex
|
||||||
|
Actions []recorderResourceManagerAction
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ResourceManager = &fakeResourceManager{}
|
||||||
|
var _ ResourceManager = &recorderResourceManager{}
|
||||||
|
|
||||||
|
// Storage type for a call to the resource manager
|
||||||
|
type recorderResourceManagerAction struct {
|
||||||
|
Type string
|
||||||
|
Group string
|
||||||
|
Version string
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceManager) Expect() ResourceManager {
|
||||||
|
return &f.expect
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceManager) HasExpectedNumberActions() bool {
|
||||||
|
f.lock.RLock()
|
||||||
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
|
f.expect.lock.RLock()
|
||||||
|
defer f.expect.lock.RUnlock()
|
||||||
|
|
||||||
|
return len(f.Actions) >= len(f.expect.Actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceManager) Validate() error {
|
||||||
|
f.lock.RLock()
|
||||||
|
defer f.lock.RUnlock()
|
||||||
|
|
||||||
|
f.expect.lock.RLock()
|
||||||
|
defer f.expect.lock.RUnlock()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(f.expect.Actions, f.Actions) {
|
||||||
|
return errors.New(cmp.Diff(f.expect.Actions, f.Actions))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeResourceManager) WaitForActions(ctx context.Context, timeout time.Duration) error {
|
||||||
|
err := wait.PollImmediateWithContext(
|
||||||
|
ctx,
|
||||||
|
100*time.Millisecond, // try every 100ms
|
||||||
|
timeout, // timeout after timeout
|
||||||
|
func(ctx context.Context) (done bool, err error) {
|
||||||
|
if f.HasExpectedNumberActions() {
|
||||||
|
return true, f.Validate()
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *recorderResourceManager) SetGroupPriority(groupName string, priority int) {
|
||||||
|
f.lock.Lock()
|
||||||
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
|
f.Actions = append(f.Actions, recorderResourceManagerAction{
|
||||||
|
Type: "SetGroupPriority",
|
||||||
|
Group: groupName,
|
||||||
|
Value: priority,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *recorderResourceManager) AddGroupVersion(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) {
|
||||||
|
f.lock.Lock()
|
||||||
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
|
f.Actions = append(f.Actions, recorderResourceManagerAction{
|
||||||
|
Type: "AddGroupVersion",
|
||||||
|
Group: groupName,
|
||||||
|
Value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func (f *recorderResourceManager) RemoveGroup(groupName string) {
|
||||||
|
f.lock.Lock()
|
||||||
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
|
f.Actions = append(f.Actions, recorderResourceManagerAction{
|
||||||
|
Type: "RemoveGroup",
|
||||||
|
Group: groupName,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
func (f *recorderResourceManager) RemoveGroupVersion(gv metav1.GroupVersion) {
|
||||||
|
f.lock.Lock()
|
||||||
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
|
f.Actions = append(f.Actions, recorderResourceManagerAction{
|
||||||
|
Type: "RemoveGroupVersion",
|
||||||
|
Group: gv.Group,
|
||||||
|
Version: gv.Version,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
func (f *recorderResourceManager) SetGroups(values []apidiscoveryv2beta1.APIGroupDiscovery) {
|
||||||
|
f.lock.Lock()
|
||||||
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
|
f.Actions = append(f.Actions, recorderResourceManagerAction{
|
||||||
|
Type: "SetGroups",
|
||||||
|
Value: values,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
func (f *recorderResourceManager) WebService() *restful.WebService {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *recorderResourceManager) ServeHTTP(http.ResponseWriter, *http.Request) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
@ -0,0 +1,302 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aggregated
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||||
|
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This handler serves the /apis endpoint for an aggregated list of
|
||||||
|
// api resources indexed by their group version.
|
||||||
|
type ResourceManager interface {
|
||||||
|
// Adds knowledge of the given groupversion to the discovery document
|
||||||
|
// If it was already being tracked, updates the stored APIVersionDiscovery
|
||||||
|
// Thread-safe
|
||||||
|
AddGroupVersion(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery)
|
||||||
|
|
||||||
|
// Sets priority for a group for sorting discovery.
|
||||||
|
// If a priority is set before the group is known, the priority will be ignored
|
||||||
|
// Once a group is removed, the priority is forgotten.
|
||||||
|
SetGroupPriority(groupName string, priority int)
|
||||||
|
|
||||||
|
// Removes all group versions for a given group
|
||||||
|
// Thread-safe
|
||||||
|
RemoveGroup(groupName string)
|
||||||
|
|
||||||
|
// Removes a specific groupversion. If all versions of a group have been
|
||||||
|
// removed, then the entire group is unlisted.
|
||||||
|
// Thread-safe
|
||||||
|
RemoveGroupVersion(gv metav1.GroupVersion)
|
||||||
|
|
||||||
|
// Resets the manager's known list of group-versions and replaces them
|
||||||
|
// with the given groups
|
||||||
|
// Thread-Safe
|
||||||
|
SetGroups([]apidiscoveryv2beta1.APIGroupDiscovery)
|
||||||
|
|
||||||
|
http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceDiscoveryManager struct {
|
||||||
|
serializer runtime.NegotiatedSerializer
|
||||||
|
// cache is an atomic pointer to avoid the use of locks
|
||||||
|
cache atomic.Pointer[cachedGroupList]
|
||||||
|
|
||||||
|
// Writes protected by the lock.
|
||||||
|
// List of all apigroups & resources indexed by the resource manager
|
||||||
|
lock sync.RWMutex
|
||||||
|
apiGroups map[string]*apidiscoveryv2beta1.APIGroupDiscovery
|
||||||
|
apiGroupNames map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResourceManager() ResourceManager {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme))
|
||||||
|
return &resourceDiscoveryManager{serializer: codecs, apiGroupNames: make(map[string]int)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rdm *resourceDiscoveryManager) SetGroupPriority(group string, priority int) {
|
||||||
|
rdm.lock.Lock()
|
||||||
|
defer rdm.lock.Unlock()
|
||||||
|
|
||||||
|
if _, exists := rdm.apiGroupNames[group]; exists {
|
||||||
|
rdm.apiGroupNames[group] = priority
|
||||||
|
rdm.cache.Store(nil)
|
||||||
|
} else {
|
||||||
|
klog.Warningf("DiscoveryManager: Attempted to set priority for group %s but does not exist", group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rdm *resourceDiscoveryManager) SetGroups(groups []apidiscoveryv2beta1.APIGroupDiscovery) {
|
||||||
|
rdm.lock.Lock()
|
||||||
|
defer rdm.lock.Unlock()
|
||||||
|
|
||||||
|
rdm.apiGroups = nil
|
||||||
|
rdm.cache.Store(nil)
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
rdm.addGroupVersionLocked(group.Name, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter unused out apiGroupNames
|
||||||
|
for name := range rdm.apiGroupNames {
|
||||||
|
if _, exists := rdm.apiGroups[name]; !exists {
|
||||||
|
delete(rdm.apiGroupNames, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rdm *resourceDiscoveryManager) AddGroupVersion(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) {
|
||||||
|
rdm.lock.Lock()
|
||||||
|
defer rdm.lock.Unlock()
|
||||||
|
|
||||||
|
rdm.addGroupVersionLocked(groupName, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rdm *resourceDiscoveryManager) addGroupVersionLocked(groupName string, value apidiscoveryv2beta1.APIVersionDiscovery) {
|
||||||
|
klog.Infof("Adding GroupVersion %s %s to ResourceManager", groupName, value.Version)
|
||||||
|
|
||||||
|
if rdm.apiGroups == nil {
|
||||||
|
rdm.apiGroups = make(map[string]*apidiscoveryv2beta1.APIGroupDiscovery)
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing, groupExists := rdm.apiGroups[groupName]; groupExists {
|
||||||
|
// If this version already exists, replace it
|
||||||
|
versionExists := false
|
||||||
|
|
||||||
|
// Not very efficient, but in practice there are generally not many versions
|
||||||
|
for i := range existing.Versions {
|
||||||
|
if existing.Versions[i].Version == value.Version {
|
||||||
|
// The new gv is the exact same as what is already in
|
||||||
|
// the map. This is a noop and cache should not be
|
||||||
|
// invalidated.
|
||||||
|
if reflect.DeepEqual(existing.Versions[i], value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
existing.Versions[i] = value
|
||||||
|
versionExists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !versionExists {
|
||||||
|
existing.Versions = append(existing.Versions, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
group := &apidiscoveryv2beta1.APIGroupDiscovery{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: groupName,
|
||||||
|
},
|
||||||
|
Versions: []apidiscoveryv2beta1.APIVersionDiscovery{value},
|
||||||
|
}
|
||||||
|
rdm.apiGroups[groupName] = group
|
||||||
|
rdm.apiGroupNames[groupName] = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset response document so it is recreated lazily
|
||||||
|
rdm.cache.Store(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rdm *resourceDiscoveryManager) RemoveGroupVersion(apiGroup metav1.GroupVersion) {
|
||||||
|
rdm.lock.Lock()
|
||||||
|
defer rdm.lock.Unlock()
|
||||||
|
group, exists := rdm.apiGroups[apiGroup.Group]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
modified := false
|
||||||
|
for i := range group.Versions {
|
||||||
|
if group.Versions[i].Version == apiGroup.Version {
|
||||||
|
group.Versions = append(group.Versions[:i], group.Versions[i+1:]...)
|
||||||
|
modified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no modification was done, cache does not need to be cleared
|
||||||
|
if !modified {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(group.Versions) == 0 {
|
||||||
|
delete(rdm.apiGroups, group.Name)
|
||||||
|
delete(rdm.apiGroupNames, group.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset response document so it is recreated lazily
|
||||||
|
rdm.cache.Store(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rdm *resourceDiscoveryManager) RemoveGroup(groupName string) {
|
||||||
|
rdm.lock.Lock()
|
||||||
|
defer rdm.lock.Unlock()
|
||||||
|
|
||||||
|
delete(rdm.apiGroups, groupName)
|
||||||
|
delete(rdm.apiGroupNames, groupName)
|
||||||
|
|
||||||
|
// Reset response document so it is recreated lazily
|
||||||
|
rdm.cache.Store(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepares the api group list for serving by converting them from map into
|
||||||
|
// list and sorting them according to insertion order
|
||||||
|
func (rdm *resourceDiscoveryManager) calculateAPIGroupsLocked() []apidiscoveryv2beta1.APIGroupDiscovery {
|
||||||
|
// Re-order the apiGroups by their priority.
|
||||||
|
groups := []apidiscoveryv2beta1.APIGroupDiscovery{}
|
||||||
|
for _, group := range rdm.apiGroups {
|
||||||
|
groups = append(groups, *group.DeepCopy())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(groups, func(i, j int) bool {
|
||||||
|
iName := groups[i].Name
|
||||||
|
jName := groups[j].Name
|
||||||
|
|
||||||
|
// Default to 0 priority by default
|
||||||
|
iPriority := rdm.apiGroupNames[iName]
|
||||||
|
jPriority := rdm.apiGroupNames[jName]
|
||||||
|
|
||||||
|
// Sort discovery based on apiservice priority.
|
||||||
|
// Duplicated from staging/src/k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helpers.go
|
||||||
|
if iPriority == jPriority {
|
||||||
|
// Equal priority uses name to break ties
|
||||||
|
return iName < jName
|
||||||
|
}
|
||||||
|
|
||||||
|
// i sorts before j if it has a lower priority
|
||||||
|
return iPriority > jPriority
|
||||||
|
})
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches from cache if it exists. If cache is empty, create it.
|
||||||
|
func (rdm *resourceDiscoveryManager) fetchFromCache() *cachedGroupList {
|
||||||
|
rdm.lock.RLock()
|
||||||
|
defer rdm.lock.RUnlock()
|
||||||
|
|
||||||
|
cacheLoad := rdm.cache.Load()
|
||||||
|
if cacheLoad != nil {
|
||||||
|
return cacheLoad
|
||||||
|
}
|
||||||
|
response := apidiscoveryv2beta1.APIGroupDiscoveryList{
|
||||||
|
Items: rdm.calculateAPIGroupsLocked(),
|
||||||
|
}
|
||||||
|
etag, err := calculateETag(response)
|
||||||
|
if err != nil {
|
||||||
|
klog.Errorf("failed to calculate etag for discovery document: %s", etag)
|
||||||
|
etag = ""
|
||||||
|
}
|
||||||
|
cached := &cachedGroupList{
|
||||||
|
cachedResponse: response,
|
||||||
|
cachedResponseETag: etag,
|
||||||
|
}
|
||||||
|
rdm.cache.Store(cached)
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
type cachedGroupList struct {
|
||||||
|
cachedResponse apidiscoveryv2beta1.APIGroupDiscoveryList
|
||||||
|
cachedResponseETag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rdm *resourceDiscoveryManager) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
cache := rdm.fetchFromCache()
|
||||||
|
response := cache.cachedResponse
|
||||||
|
etag := cache.cachedResponseETag
|
||||||
|
|
||||||
|
if len(etag) > 0 {
|
||||||
|
// Use proper e-tag headers if one is available
|
||||||
|
ServeHTTPWithETag(
|
||||||
|
&response,
|
||||||
|
etag,
|
||||||
|
rdm.serializer,
|
||||||
|
resp,
|
||||||
|
req,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Default to normal response in rare case etag is
|
||||||
|
// not cached with the object for some reason.
|
||||||
|
responsewriters.WriteObjectNegotiated(
|
||||||
|
rdm.serializer,
|
||||||
|
DiscoveryEndpointRestrictions,
|
||||||
|
AggregatedDiscoveryGV,
|
||||||
|
resp,
|
||||||
|
req,
|
||||||
|
http.StatusOK,
|
||||||
|
&response,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,501 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aggregated_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
fuzz "github.com/google/gofuzz"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||||
|
)
|
||||||
|
|
||||||
|
var scheme = runtime.NewScheme()
|
||||||
|
var codecs = runtimeserializer.NewCodecFactory(scheme)
|
||||||
|
|
||||||
|
const discoveryPath = "/apis"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Add all builtin types to scheme
|
||||||
|
apidiscoveryv2beta1.AddToScheme(scheme)
|
||||||
|
codecs = runtimeserializer.NewCodecFactory(scheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fuzzAPIGroups(atLeastNumGroups, maxNumGroups int, seed int64) apidiscoveryv2beta1.APIGroupDiscoveryList {
|
||||||
|
fuzzer := fuzz.NewWithSeed(seed)
|
||||||
|
fuzzer.NumElements(atLeastNumGroups, maxNumGroups)
|
||||||
|
fuzzer.NilChance(0)
|
||||||
|
fuzzer.Funcs(func(o *apidiscoveryv2beta1.APIGroupDiscovery, c fuzz.Continue) {
|
||||||
|
c.FuzzNoCustom(o)
|
||||||
|
|
||||||
|
// The ResourceManager will just not serve the group if its versions
|
||||||
|
// list is empty
|
||||||
|
atLeastOne := apidiscoveryv2beta1.APIVersionDiscovery{}
|
||||||
|
c.Fuzz(&atLeastOne)
|
||||||
|
o.Versions = append(o.Versions, atLeastOne)
|
||||||
|
|
||||||
|
o.TypeMeta = metav1.TypeMeta{}
|
||||||
|
var name string
|
||||||
|
c.Fuzz(&name)
|
||||||
|
o.ObjectMeta = metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var apis []apidiscoveryv2beta1.APIGroupDiscovery
|
||||||
|
fuzzer.Fuzz(&apis)
|
||||||
|
sort.Slice(apis[:], func(i, j int) bool {
|
||||||
|
return apis[i].Name < apis[j].Name
|
||||||
|
})
|
||||||
|
|
||||||
|
return apidiscoveryv2beta1.APIGroupDiscoveryList{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "APIGroupDiscoveryList",
|
||||||
|
APIVersion: "apidiscovery.k8s.io/v2beta1",
|
||||||
|
},
|
||||||
|
Items: apis,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchPath(handler http.Handler, acceptPrefix string, path string, etag string) (*http.Response, []byte, *apidiscoveryv2beta1.APIGroupDiscoveryList) {
|
||||||
|
// Expect json-formatted apis group list
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", discoveryPath, nil)
|
||||||
|
|
||||||
|
// Ask for JSON response
|
||||||
|
req.Header.Set("Accept", acceptPrefix+";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList")
|
||||||
|
|
||||||
|
if etag != "" {
|
||||||
|
// Quote provided etag if unquoted
|
||||||
|
quoted := etag
|
||||||
|
if !strings.HasPrefix(etag, "\"") {
|
||||||
|
quoted = strconv.Quote(etag)
|
||||||
|
}
|
||||||
|
req.Header.Set("If-None-Match", quoted)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
bytes := w.Body.Bytes()
|
||||||
|
var decoded *apidiscoveryv2beta1.APIGroupDiscoveryList
|
||||||
|
if len(bytes) > 0 {
|
||||||
|
decoded = &apidiscoveryv2beta1.APIGroupDiscoveryList{}
|
||||||
|
runtime.DecodeInto(codecs.UniversalDecoder(), bytes, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Result(), bytes, decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all builtin APIServices to the manager and check the output
|
||||||
|
func TestBasicResponse(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
|
||||||
|
apis := fuzzAPIGroups(1, 3, 10)
|
||||||
|
manager.SetGroups(apis.Items)
|
||||||
|
|
||||||
|
response, body, decoded := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
|
||||||
|
jsonFormatted, err := json.Marshal(&apis)
|
||||||
|
require.NoError(t, err, "json marshal should always succeed")
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
|
||||||
|
assert.Equal(t, "application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported")
|
||||||
|
assert.NotEmpty(t, response.Header.Get("ETag"), "E-Tag should be set")
|
||||||
|
|
||||||
|
assert.NoError(t, err, "decode should always succeed")
|
||||||
|
assert.EqualValues(t, &apis, decoded, "decoded value should equal input")
|
||||||
|
assert.Equal(t, string(jsonFormatted)+"\n", string(body), "response should be the api group list")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that protobuf is outputted correctly
|
||||||
|
func TestBasicResponseProtobuf(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
|
||||||
|
apis := fuzzAPIGroups(1, 3, 10)
|
||||||
|
manager.SetGroups(apis.Items)
|
||||||
|
|
||||||
|
response, _, decoded := fetchPath(manager, "application/vnd.kubernetes.protobuf", discoveryPath, "")
|
||||||
|
assert.Equal(t, http.StatusOK, response.StatusCode, "response should be 200 OK")
|
||||||
|
assert.Equal(t, "application/vnd.kubernetes.protobuf;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", response.Header.Get("Content-Type"), "Content-Type response header should be as requested in Accept header if supported")
|
||||||
|
assert.NotEmpty(t, response.Header.Get("ETag"), "E-Tag should be set")
|
||||||
|
assert.EqualValues(t, &apis, decoded, "decoded value should equal input")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that an etag associated with the service only depends on the apiresources
|
||||||
|
// e.g.: Multiple services with the same contents should have the same etag.
|
||||||
|
func TestEtagConsistent(t *testing.T) {
|
||||||
|
// Create 2 managers, add a bunch of services to each
|
||||||
|
manager1 := discoveryendpoint.NewResourceManager()
|
||||||
|
manager2 := discoveryendpoint.NewResourceManager()
|
||||||
|
|
||||||
|
apis := fuzzAPIGroups(1, 3, 11)
|
||||||
|
manager1.SetGroups(apis.Items)
|
||||||
|
manager2.SetGroups(apis.Items)
|
||||||
|
|
||||||
|
// Make sure etag of each is the same
|
||||||
|
res1_initial, _, _ := fetchPath(manager1, "application/json", discoveryPath, "")
|
||||||
|
res2_initial, _, _ := fetchPath(manager2, "application/json", discoveryPath, "")
|
||||||
|
|
||||||
|
assert.NotEmpty(t, res1_initial.Header.Get("ETag"), "Etag should be populated")
|
||||||
|
assert.NotEmpty(t, res2_initial.Header.Get("ETag"), "Etag should be populated")
|
||||||
|
assert.Equal(t, res1_initial.Header.Get("ETag"), res2_initial.Header.Get("ETag"), "etag should be deterministic")
|
||||||
|
|
||||||
|
// Then add one service to only one.
|
||||||
|
// Make sure etag is changed, but other is the same
|
||||||
|
apis = fuzzAPIGroups(1, 1, 11)
|
||||||
|
for _, group := range apis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager1.AddGroupVersion(group.Name, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res1_addedToOne, _, _ := fetchPath(manager1, "application/json", discoveryPath, "")
|
||||||
|
res2_addedToOne, _, _ := fetchPath(manager2, "application/json", discoveryPath, "")
|
||||||
|
|
||||||
|
assert.NotEmpty(t, res1_addedToOne.Header.Get("ETag"), "Etag should be populated")
|
||||||
|
assert.NotEmpty(t, res2_addedToOne.Header.Get("ETag"), "Etag should be populated")
|
||||||
|
assert.NotEqual(t, res1_initial.Header.Get("ETag"), res1_addedToOne.Header.Get("ETag"), "ETag should be changed since version was added")
|
||||||
|
assert.Equal(t, res2_initial.Header.Get("ETag"), res2_addedToOne.Header.Get("ETag"), "ETag should be unchanged since data was unchanged")
|
||||||
|
|
||||||
|
// Then add service to other one
|
||||||
|
// Make sure etag is the same
|
||||||
|
for _, group := range apis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager2.AddGroupVersion(group.Name, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res1_addedToBoth, _, _ := fetchPath(manager1, "application/json", discoveryPath, "")
|
||||||
|
res2_addedToBoth, _, _ := fetchPath(manager2, "application/json", discoveryPath, "")
|
||||||
|
|
||||||
|
assert.NotEmpty(t, res1_addedToOne.Header.Get("ETag"), "Etag should be populated")
|
||||||
|
assert.NotEmpty(t, res2_addedToOne.Header.Get("ETag"), "Etag should be populated")
|
||||||
|
assert.Equal(t, res1_addedToBoth.Header.Get("ETag"), res2_addedToBoth.Header.Get("ETag"), "ETags should be equal since content is equal")
|
||||||
|
assert.NotEqual(t, res2_initial.Header.Get("ETag"), res2_addedToBoth.Header.Get("ETag"), "ETag should be changed since data was changed")
|
||||||
|
|
||||||
|
// Remove the group version from both. Initial E-Tag should be restored
|
||||||
|
for _, group := range apis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager1.RemoveGroupVersion(metav1.GroupVersion{
|
||||||
|
Group: group.Name,
|
||||||
|
Version: version.Version,
|
||||||
|
})
|
||||||
|
manager2.RemoveGroupVersion(metav1.GroupVersion{
|
||||||
|
Group: group.Name,
|
||||||
|
Version: version.Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res1_removeFromBoth, _, _ := fetchPath(manager1, "application/json", discoveryPath, "")
|
||||||
|
res2_removeFromBoth, _, _ := fetchPath(manager2, "application/json", discoveryPath, "")
|
||||||
|
|
||||||
|
assert.NotEmpty(t, res1_addedToOne.Header.Get("ETag"), "Etag should be populated")
|
||||||
|
assert.NotEmpty(t, res2_addedToOne.Header.Get("ETag"), "Etag should be populated")
|
||||||
|
assert.Equal(t, res1_removeFromBoth.Header.Get("ETag"), res2_removeFromBoth.Header.Get("ETag"), "ETags should be equal since content is equal")
|
||||||
|
assert.Equal(t, res1_initial.Header.Get("ETag"), res1_removeFromBoth.Header.Get("ETag"), "ETag should be equal to initial value since added content was removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that if a request comes in with an If-None-Match header with an incorrect
|
||||||
|
// E-Tag, that fresh content is returned.
|
||||||
|
func TestEtagNonMatching(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
apis := fuzzAPIGroups(1, 3, 12)
|
||||||
|
manager.SetGroups(apis.Items)
|
||||||
|
|
||||||
|
// fetch the document once
|
||||||
|
initial, _, _ := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
assert.NotEmpty(t, initial.Header.Get("ETag"), "ETag should be populated")
|
||||||
|
|
||||||
|
// Send another request with a wrong e-tag. The same response should
|
||||||
|
// get sent again
|
||||||
|
second, _, _ := fetchPath(manager, "application/json", discoveryPath, "wrongetag")
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, initial.StatusCode, "response should be 200 OK")
|
||||||
|
assert.Equal(t, http.StatusOK, second.StatusCode, "response should be 200 OK")
|
||||||
|
assert.Equal(t, initial.Header.Get("ETag"), second.Header.Get("ETag"), "ETag of both requests should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that if a request comes in with an If-None-Match header with a correct
|
||||||
|
// E-Tag, that 304 Not Modified is returned
|
||||||
|
func TestEtagMatching(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
apis := fuzzAPIGroups(1, 3, 12)
|
||||||
|
manager.SetGroups(apis.Items)
|
||||||
|
|
||||||
|
// fetch the document once
|
||||||
|
initial, initialBody, _ := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
assert.NotEmpty(t, initial.Header.Get("ETag"), "ETag should be populated")
|
||||||
|
assert.NotEmpty(t, initialBody, "body should not be empty")
|
||||||
|
|
||||||
|
// Send another request with a wrong e-tag. The same response should
|
||||||
|
// get sent again
|
||||||
|
second, secondBody, _ := fetchPath(manager, "application/json", discoveryPath, initial.Header.Get("ETag"))
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, initial.StatusCode, "initial response should be 200 OK")
|
||||||
|
assert.Equal(t, http.StatusNotModified, second.StatusCode, "second response should be 304 Not Modified")
|
||||||
|
assert.Equal(t, initial.Header.Get("ETag"), second.Header.Get("ETag"), "ETag of both requests should be equal")
|
||||||
|
assert.Empty(t, secondBody, "body should be empty when returning 304 Not Modified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that if a request comes in with an If-None-Match header with an old
|
||||||
|
// E-Tag, that fresh content is returned
|
||||||
|
func TestEtagOutdated(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
apis := fuzzAPIGroups(1, 3, 15)
|
||||||
|
manager.SetGroups(apis.Items)
|
||||||
|
|
||||||
|
// fetch the document once
|
||||||
|
initial, initialBody, _ := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
assert.NotEmpty(t, initial.Header.Get("ETag"), "ETag should be populated")
|
||||||
|
assert.NotEmpty(t, initialBody, "body should not be empty")
|
||||||
|
|
||||||
|
// Then add some services so the etag changes
|
||||||
|
apis = fuzzAPIGroups(1, 3, 14)
|
||||||
|
for _, group := range apis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager.AddGroupVersion(group.Name, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send another request with the old e-tag. Response should not be 304 Not Modified
|
||||||
|
second, secondBody, _ := fetchPath(manager, "application/json", discoveryPath, initial.Header.Get("ETag"))
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, initial.StatusCode, "initial response should be 200 OK")
|
||||||
|
assert.Equal(t, http.StatusOK, second.StatusCode, "second response should be 304 Not Modified")
|
||||||
|
assert.NotEqual(t, initial.Header.Get("ETag"), second.Header.Get("ETag"), "ETag of both requests should be unequal since contents differ")
|
||||||
|
assert.NotEmpty(t, secondBody, "body should be not empty when returning 304 Not Modified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that an api service can be added or removed
|
||||||
|
func TestAddRemove(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
apis := fuzzAPIGroups(1, 3, 15)
|
||||||
|
for _, group := range apis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager.AddGroupVersion(group.Name, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, initialDocument := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
|
||||||
|
for _, group := range apis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager.RemoveGroupVersion(metav1.GroupVersion{
|
||||||
|
Group: group.Name,
|
||||||
|
Version: version.Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, secondDocument := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
|
||||||
|
require.NotNil(t, initialDocument, "initial document should parse")
|
||||||
|
require.NotNil(t, secondDocument, "second document should parse")
|
||||||
|
assert.Len(t, initialDocument.Items, len(apis.Items), "initial document should have set number of groups")
|
||||||
|
assert.Len(t, secondDocument.Items, 0, "second document should have no groups")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show that updating an existing service replaces and does not add the entry
|
||||||
|
// and instead replaces it
|
||||||
|
func TestUpdateService(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
apis := fuzzAPIGroups(1, 3, 15)
|
||||||
|
for _, group := range apis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager.AddGroupVersion(group.Name, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, initialDocument := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
|
||||||
|
assert.Equal(t, initialDocument, &apis, "should have returned expected document")
|
||||||
|
|
||||||
|
b, err := json.Marshal(apis)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
var newapis apidiscoveryv2beta1.APIGroupDiscoveryList
|
||||||
|
err = json.Unmarshal(b, &newapis)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newapis.Items[0].Versions[0].Resources[0].Resource = "changed a resource name!"
|
||||||
|
for _, group := range newapis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager.AddGroupVersion(group.Name, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, secondDocument := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
assert.Equal(t, secondDocument, &newapis, "should have returned expected document")
|
||||||
|
assert.NotEqual(t, secondDocument, initialDocument, "should have returned expected document")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the discovery manager is capable of serving requests to multiple users
|
||||||
|
// with unchanging data
|
||||||
|
func TestConcurrentRequests(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
apis := fuzzAPIGroups(1, 3, 15)
|
||||||
|
manager.SetGroups(apis.Items)
|
||||||
|
|
||||||
|
waitGroup := sync.WaitGroup{}
|
||||||
|
|
||||||
|
numReaders := 100
|
||||||
|
numRequestsPerReader := 100
|
||||||
|
|
||||||
|
// Spawn a bunch of readers that will keep sending requests to the server
|
||||||
|
for i := 0; i < numReaders; i++ {
|
||||||
|
waitGroup.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer waitGroup.Done()
|
||||||
|
etag := ""
|
||||||
|
for j := 0; j < numRequestsPerReader; j++ {
|
||||||
|
usedEtag := etag
|
||||||
|
if j%2 == 0 {
|
||||||
|
// Disable use of etag for every second request
|
||||||
|
usedEtag = ""
|
||||||
|
}
|
||||||
|
response, body, document := fetchPath(manager, "application/json", discoveryPath, usedEtag)
|
||||||
|
|
||||||
|
if usedEtag != "" {
|
||||||
|
assert.Equal(t, http.StatusNotModified, response.StatusCode, "response should be Not Modified if etag was used")
|
||||||
|
assert.Empty(t, body, "body should be empty if etag used")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, http.StatusOK, response.StatusCode, "response should be OK if etag was unused")
|
||||||
|
assert.Equal(t, &apis, document, "document should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
etag = response.Header.Get("ETag")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
waitGroup.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the handler is capable of serving many concurrent readers and many
|
||||||
|
// concurrent writers without tripping up. Good to run with go '-race' detector
|
||||||
|
// since there are not many "correctness" checks
|
||||||
|
func TestAbuse(t *testing.T) {
|
||||||
|
manager := discoveryendpoint.NewResourceManager()
|
||||||
|
|
||||||
|
numReaders := 100
|
||||||
|
numRequestsPerReader := 1000
|
||||||
|
|
||||||
|
numWriters := 10
|
||||||
|
numWritesPerWriter := 1000
|
||||||
|
|
||||||
|
waitGroup := sync.WaitGroup{}
|
||||||
|
|
||||||
|
// Spawn a bunch of writers that randomly add groups, remove groups, and
|
||||||
|
// reset the list of groups
|
||||||
|
for i := 0; i < numWriters; i++ {
|
||||||
|
source := rand.NewSource(int64(i))
|
||||||
|
|
||||||
|
waitGroup.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer waitGroup.Done()
|
||||||
|
|
||||||
|
// track list of groups we've added so that we can remove them
|
||||||
|
// randomly
|
||||||
|
var addedGroups []metav1.GroupVersion
|
||||||
|
|
||||||
|
for j := 0; j < numWritesPerWriter; j++ {
|
||||||
|
switch source.Int63() % 3 {
|
||||||
|
case 0:
|
||||||
|
// Add a fuzzed group
|
||||||
|
apis := fuzzAPIGroups(1, 2, 15)
|
||||||
|
for _, group := range apis.Items {
|
||||||
|
for _, version := range group.Versions {
|
||||||
|
manager.AddGroupVersion(group.Name, version)
|
||||||
|
addedGroups = append(addedGroups, metav1.GroupVersion{
|
||||||
|
Group: group.Name,
|
||||||
|
Version: version.Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
// Remove a group that we have added
|
||||||
|
if len(addedGroups) > 0 {
|
||||||
|
manager.RemoveGroupVersion(addedGroups[0])
|
||||||
|
addedGroups = addedGroups[1:]
|
||||||
|
} else {
|
||||||
|
// Send a request and try to remove a group someone else
|
||||||
|
// might have added
|
||||||
|
_, _, document := fetchPath(manager, "application/json", discoveryPath, "")
|
||||||
|
assert.NotNil(t, document, "manager should always succeed in returning a document")
|
||||||
|
|
||||||
|
if len(document.Items) > 0 {
|
||||||
|
manager.RemoveGroupVersion(metav1.GroupVersion{
|
||||||
|
Group: document.Items[0].Name,
|
||||||
|
Version: document.Items[0].Versions[0].Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
manager.SetGroups(nil)
|
||||||
|
addedGroups = nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn a bunch of readers that will keep sending requests to the server
|
||||||
|
// and making sure the response makes sense
|
||||||
|
for i := 0; i < numReaders; i++ {
|
||||||
|
waitGroup.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer waitGroup.Done()
|
||||||
|
|
||||||
|
etag := ""
|
||||||
|
for j := 0; j < numRequestsPerReader; j++ {
|
||||||
|
response, body, document := fetchPath(manager, "application/json", discoveryPath, etag)
|
||||||
|
|
||||||
|
if response.StatusCode == http.StatusNotModified {
|
||||||
|
assert.Equal(t, etag, response.Header.Get("ETag"))
|
||||||
|
assert.Empty(t, body, "body should be empty if etag used")
|
||||||
|
assert.Nil(t, document)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, http.StatusOK, response.StatusCode, "response should be OK if etag was unused")
|
||||||
|
assert.NotNil(t, document)
|
||||||
|
}
|
||||||
|
|
||||||
|
etag = response.Header.Get("ETag")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitGroup.Wait()
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aggregated
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AggregatedDiscoveryGV = schema.GroupVersion{Group: "apidiscovery.k8s.io", Version: "v2beta1"}
|
||||||
|
|
||||||
|
// Interface is from "k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||||
|
|
||||||
|
// DiscoveryEndpointRestrictions allows requests to /apis to provide a Content Negotiation GVK for aggregated discovery.
|
||||||
|
var DiscoveryEndpointRestrictions = discoveryEndpointRestrictions{}
|
||||||
|
|
||||||
|
type discoveryEndpointRestrictions struct{}
|
||||||
|
|
||||||
|
func (discoveryEndpointRestrictions) AllowsMediaTypeTransform(mimeType string, mimeSubType string, gvk *schema.GroupVersionKind) bool {
|
||||||
|
return IsAggregatedDiscoveryGVK(gvk)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (discoveryEndpointRestrictions) AllowsServerVersion(string) bool { return false }
|
||||||
|
func (discoveryEndpointRestrictions) AllowsStreamSchema(s string) bool { return s == "watch" }
|
||||||
|
|
||||||
|
// IsAggregatedDiscoveryGVK checks if a provided GVK is the GVK for serving aggregated discovery.
|
||||||
|
func IsAggregatedDiscoveryGVK(gvk *schema.GroupVersionKind) bool {
|
||||||
|
if gvk != nil {
|
||||||
|
return gvk.Group == "apidiscovery.k8s.io" && gvk.Version == "v2beta1" && gvk.Kind == "APIGroupDiscoveryList"
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aggregated
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
|
|
||||||
|
"github.com/emicklei/go-restful/v3"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||||
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WrappedHandler struct {
|
||||||
|
s runtime.NegotiatedSerializer
|
||||||
|
handler http.Handler
|
||||||
|
aggHandler http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wrapped *WrappedHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryEndpoint) {
|
||||||
|
mediaType, _ := negotiation.NegotiateMediaTypeOptions(req.Header.Get("Accept"), wrapped.s.SupportedMediaTypes(), DiscoveryEndpointRestrictions)
|
||||||
|
// mediaType.Convert looks at the request accept headers and is used to control whether the discovery document will be aggregated.
|
||||||
|
if IsAggregatedDiscoveryGVK(mediaType.Convert) {
|
||||||
|
wrapped.aggHandler.ServeHTTP(resp, req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wrapped.handler.ServeHTTP(resp, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wrapped *WrappedHandler) restfulHandle(req *restful.Request, resp *restful.Response) {
|
||||||
|
wrapped.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (wrapped *WrappedHandler) GenerateWebService(prefix string, returnType interface{}) *restful.WebService {
|
||||||
|
mediaTypes, _ := negotiation.MediaTypesForSerializer(wrapped.s)
|
||||||
|
ws := new(restful.WebService)
|
||||||
|
ws.Path(prefix)
|
||||||
|
ws.Doc("get available API versions")
|
||||||
|
ws.Route(ws.GET("/").To(wrapped.restfulHandle).
|
||||||
|
Doc("get available API versions").
|
||||||
|
Operation("getAPIVersions").
|
||||||
|
Produces(mediaTypes...).
|
||||||
|
Consumes(mediaTypes...).
|
||||||
|
Writes(returnType))
|
||||||
|
return ws
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapAggregatedDiscoveryToHandler wraps a handler with an option to
|
||||||
|
// emit the aggregated discovery by passing in the aggregated
|
||||||
|
// discovery type in content negotiation headers: eg: (Accept:
|
||||||
|
// application/json;v=v2beta1;g=apidiscovery.k8s.io;as=APIGroupDiscoveryList)
|
||||||
|
func WrapAggregatedDiscoveryToHandler(handler http.Handler, aggHandler http.Handler) *WrappedHandler {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
apidiscoveryv2beta1.AddToScheme(scheme)
|
||||||
|
codecs := serializer.NewCodecFactory(scheme)
|
||||||
|
return &WrappedHandler{codecs, handler, aggHandler}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2022 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package aggregated
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const discoveryPath = "/apis"
|
||||||
|
const jsonAccept = "application/json"
|
||||||
|
const protobufAccept = "application/vnd.kubernetes.protobuf"
|
||||||
|
const aggregatedAcceptSuffix = ";g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList"
|
||||||
|
|
||||||
|
const aggregatedJSONAccept = jsonAccept + aggregatedAcceptSuffix
|
||||||
|
const aggregatedProtoAccept = protobufAccept + aggregatedAcceptSuffix
|
||||||
|
|
||||||
|
func fetchPath(handler http.Handler, path, accept string) string {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest("GET", discoveryPath, nil)
|
||||||
|
|
||||||
|
// Ask for JSON response
|
||||||
|
req.Header.Set("Accept", accept)
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, req)
|
||||||
|
return string(w.Body.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeHTTPHandler struct {
|
||||||
|
data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeHTTPHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
io.WriteString(resp, f.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregationEnabled(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryEndpoint, true)()
|
||||||
|
|
||||||
|
unaggregated := fakeHTTPHandler{data: "unaggregated"}
|
||||||
|
aggregated := fakeHTTPHandler{data: "aggregated"}
|
||||||
|
wrapped := WrapAggregatedDiscoveryToHandler(unaggregated, aggregated)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
accept string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// Misconstructed/incorrect accept headers should be passed to the unaggregated handler to return an error
|
||||||
|
accept: "application/json;foo=bar",
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
// Empty accept headers are valid and should be handled by the unaggregated handler
|
||||||
|
accept: "",
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
accept: aggregatedJSONAccept,
|
||||||
|
expected: "aggregated",
|
||||||
|
}, {
|
||||||
|
accept: aggregatedProtoAccept,
|
||||||
|
expected: "aggregated",
|
||||||
|
}, {
|
||||||
|
accept: jsonAccept,
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
accept: protobufAccept,
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
// Server should return the first accepted type
|
||||||
|
accept: aggregatedJSONAccept + "," + jsonAccept,
|
||||||
|
expected: "aggregated",
|
||||||
|
}, {
|
||||||
|
// Server should return the first accepted type
|
||||||
|
accept: aggregatedProtoAccept + "," + protobufAccept,
|
||||||
|
expected: "aggregated",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
body := fetchPath(wrapped, discoveryPath, tc.accept)
|
||||||
|
assert.Equal(t, tc.expected, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAggregationDisabled(t *testing.T) {
|
||||||
|
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AggregatedDiscoveryEndpoint, false)()
|
||||||
|
|
||||||
|
unaggregated := fakeHTTPHandler{data: "unaggregated"}
|
||||||
|
aggregated := fakeHTTPHandler{data: "aggregated"}
|
||||||
|
wrapped := WrapAggregatedDiscoveryToHandler(unaggregated, aggregated)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
accept string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// Misconstructed/incorrect accept headers should be passed to the unaggregated handler to return an error
|
||||||
|
accept: "application/json;foo=bar",
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
// Empty accept headers are valid and should be handled by the unaggregated handler
|
||||||
|
accept: "",
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
|
||||||
|
accept: aggregatedJSONAccept,
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
accept: aggregatedProtoAccept,
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
accept: jsonAccept,
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
accept: protobufAccept,
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
// Server should return the first accepted type.
|
||||||
|
// If aggregation is disabled, the unaggregated type should be returned.
|
||||||
|
accept: aggregatedJSONAccept + "," + jsonAccept,
|
||||||
|
expected: "unaggregated",
|
||||||
|
}, {
|
||||||
|
// Server should return the first accepted type.
|
||||||
|
// If aggregation is disabled, the unaggregated type should be returned.
|
||||||
|
accept: aggregatedProtoAccept + "," + protobufAccept,
|
||||||
|
expected: "unaggregated",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
body := fetchPath(wrapped, discoveryPath, tc.accept)
|
||||||
|
assert.Equal(t, tc.expected, body)
|
||||||
|
}
|
||||||
|
}
|
@ -56,7 +56,7 @@ func (s *legacyRootAPIHandler) WebService() *restful.WebService {
|
|||||||
ws := new(restful.WebService)
|
ws := new(restful.WebService)
|
||||||
ws.Path(s.apiPrefix)
|
ws.Path(s.apiPrefix)
|
||||||
ws.Doc("get available API versions")
|
ws.Doc("get available API versions")
|
||||||
ws.Route(ws.GET("/").To(s.handle).
|
ws.Route(ws.GET("/").To(s.restfulHandle).
|
||||||
Doc("get available API versions").
|
Doc("get available API versions").
|
||||||
Operation("getAPIVersions").
|
Operation("getAPIVersions").
|
||||||
Produces(mediaTypes...).
|
Produces(mediaTypes...).
|
||||||
@ -65,12 +65,16 @@ func (s *legacyRootAPIHandler) WebService() *restful.WebService {
|
|||||||
return ws
|
return ws
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *legacyRootAPIHandler) handle(req *restful.Request, resp *restful.Response) {
|
func (s *legacyRootAPIHandler) restfulHandle(req *restful.Request, resp *restful.Response) {
|
||||||
clientIP := utilnet.GetClientIP(req.Request)
|
s.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *legacyRootAPIHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
clientIP := utilnet.GetClientIP(req)
|
||||||
apiVersions := &metav1.APIVersions{
|
apiVersions := &metav1.APIVersions{
|
||||||
ServerAddressByClientCIDRs: s.addresses.ServerAddressByClientCIDRs(clientIP),
|
ServerAddressByClientCIDRs: s.addresses.ServerAddressByClientCIDRs(clientIP),
|
||||||
Versions: []string{"v1"},
|
Versions: []string{"v1"},
|
||||||
}
|
}
|
||||||
|
|
||||||
responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, apiVersions, false)
|
responsewriters.WriteObjectNegotiated(s.serializer, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, resp, req, http.StatusOK, apiVersions, false)
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ import (
|
|||||||
type GroupManager interface {
|
type GroupManager interface {
|
||||||
AddGroup(apiGroup metav1.APIGroup)
|
AddGroup(apiGroup metav1.APIGroup)
|
||||||
RemoveGroup(groupName string)
|
RemoveGroup(groupName string)
|
||||||
|
ServeHTTP(resp http.ResponseWriter, req *http.Request)
|
||||||
WebService() *restful.WebService
|
WebService() *restful.WebService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
|
|
||||||
restful "github.com/emicklei/go-restful/v3"
|
restful "github.com/emicklei/go-restful/v3"
|
||||||
|
|
||||||
|
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
@ -105,7 +106,7 @@ type APIGroupVersion struct {
|
|||||||
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
|
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
|
||||||
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
|
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
|
||||||
// in a slash.
|
// in a slash.
|
||||||
func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]*storageversion.ResourceInfo, error) {
|
func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]apidiscoveryv2beta1.APIResourceDiscovery, []*storageversion.ResourceInfo, error) {
|
||||||
prefix := path.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version)
|
prefix := path.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version)
|
||||||
installer := &APIInstaller{
|
installer := &APIInstaller{
|
||||||
group: g,
|
group: g,
|
||||||
@ -117,7 +118,11 @@ func (g *APIGroupVersion) InstallREST(container *restful.Container) ([]*storagev
|
|||||||
versionDiscoveryHandler := discovery.NewAPIVersionHandler(g.Serializer, g.GroupVersion, staticLister{apiResources})
|
versionDiscoveryHandler := discovery.NewAPIVersionHandler(g.Serializer, g.GroupVersion, staticLister{apiResources})
|
||||||
versionDiscoveryHandler.AddToWebService(ws)
|
versionDiscoveryHandler.AddToWebService(ws)
|
||||||
container.Add(ws)
|
container.Add(ws)
|
||||||
return removeNonPersistedResources(resourceInfos), utilerrors.NewAggregate(registrationErrors)
|
aggregatedDiscoveryResources, err := ConvertGroupVersionIntoToDiscovery(apiResources)
|
||||||
|
if err != nil {
|
||||||
|
registrationErrors = append(registrationErrors, err)
|
||||||
|
}
|
||||||
|
return aggregatedDiscoveryResources, removeNonPersistedResources(resourceInfos), utilerrors.NewAggregate(registrationErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeNonPersistedResources(infos []*storageversion.ResourceInfo) []*storageversion.ResourceInfo {
|
func removeNonPersistedResources(infos []*storageversion.ResourceInfo) []*storageversion.ResourceInfo {
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
restful "github.com/emicklei/go-restful/v3"
|
restful "github.com/emicklei/go-restful/v3"
|
||||||
|
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/conversion"
|
"k8s.io/apimachinery/pkg/conversion"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -68,6 +69,94 @@ type action struct {
|
|||||||
AllNamespaces bool // true iff the action is namespaced but works on aggregate result for all namespaces
|
AllNamespaces bool // true iff the action is namespaced but works on aggregate result for all namespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ConvertGroupVersionIntoToDiscovery(list []metav1.APIResource) ([]apidiscoveryv2beta1.APIResourceDiscovery, error) {
|
||||||
|
var apiResourceList []apidiscoveryv2beta1.APIResourceDiscovery
|
||||||
|
parentResources := map[string]*apidiscoveryv2beta1.APIResourceDiscovery{}
|
||||||
|
|
||||||
|
// Loop through all top-level resources
|
||||||
|
for _, r := range list {
|
||||||
|
if strings.Contains(r.Name, "/") {
|
||||||
|
// Skip subresources for now so we can get the list of resources
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var scope apidiscoveryv2beta1.ResourceScope
|
||||||
|
if r.Namespaced {
|
||||||
|
scope = apidiscoveryv2beta1.ScopeNamespace
|
||||||
|
} else {
|
||||||
|
scope = apidiscoveryv2beta1.ScopeCluster
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResourceList = append(apiResourceList, apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
Resource: r.Name,
|
||||||
|
Scope: scope,
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Group: r.Group,
|
||||||
|
Version: r.Version,
|
||||||
|
Kind: r.Kind,
|
||||||
|
},
|
||||||
|
Verbs: r.Verbs,
|
||||||
|
ShortNames: r.ShortNames,
|
||||||
|
Categories: r.Categories,
|
||||||
|
SingularResource: r.SingularName,
|
||||||
|
})
|
||||||
|
parentResources[r.Name] = &apiResourceList[len(apiResourceList)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through all subresources
|
||||||
|
for _, r := range list {
|
||||||
|
// Split resource name and subresource name
|
||||||
|
split := strings.SplitN(r.Name, "/", 2)
|
||||||
|
|
||||||
|
if len(split) != 2 {
|
||||||
|
// Skip parent resources
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var scope apidiscoveryv2beta1.ResourceScope
|
||||||
|
if r.Namespaced {
|
||||||
|
scope = apidiscoveryv2beta1.ScopeNamespace
|
||||||
|
} else {
|
||||||
|
scope = apidiscoveryv2beta1.ScopeCluster
|
||||||
|
}
|
||||||
|
|
||||||
|
var parent *apidiscoveryv2beta1.APIResourceDiscovery
|
||||||
|
var exists bool
|
||||||
|
|
||||||
|
parent, exists = parentResources[split[0]]
|
||||||
|
if !exists {
|
||||||
|
// If a subresource exists without a parent, create a parent
|
||||||
|
apiResourceList = append(apiResourceList, apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
Resource: split[0],
|
||||||
|
Scope: scope,
|
||||||
|
})
|
||||||
|
parentResources[split[0]] = &apiResourceList[len(apiResourceList)-1]
|
||||||
|
parent = &apiResourceList[len(apiResourceList)-1]
|
||||||
|
parentResources[split[0]] = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent.Scope != scope {
|
||||||
|
return nil, fmt.Errorf("Error: Parent %s (scope: %s) and subresource %s (scope: %s) scope do not match", split[0], parent.Scope, split[1], scope)
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
subresource := apidiscoveryv2beta1.APISubresourceDiscovery{
|
||||||
|
Subresource: split[1],
|
||||||
|
Verbs: r.Verbs,
|
||||||
|
}
|
||||||
|
if r.Kind != "" {
|
||||||
|
subresource.ResponseKind = &metav1.GroupVersionKind{
|
||||||
|
Group: r.Group,
|
||||||
|
Version: r.Version,
|
||||||
|
Kind: r.Kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent.Subresources = append(parent.Subresources, subresource)
|
||||||
|
|
||||||
|
}
|
||||||
|
return apiResourceList, nil
|
||||||
|
}
|
||||||
|
|
||||||
// An interface to see if one storage supports override its default verb for monitoring
|
// An interface to see if one storage supports override its default verb for monitoring
|
||||||
type StorageMetricsOverride interface {
|
type StorageMetricsOverride interface {
|
||||||
// OverrideMetricsVerb gives a storage object an opportunity to override the verb reported to the metrics endpoint
|
// OverrideMetricsVerb gives a storage object an opportunity to override the verb reported to the metrics endpoint
|
||||||
|
@ -18,6 +18,10 @@ package endpoints
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsVowel(t *testing.T) {
|
func TestIsVowel(t *testing.T) {
|
||||||
@ -97,3 +101,243 @@ func TestGetArticleForNoun(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertAPIResourceToDiscovery(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
resources []metav1.APIResource
|
||||||
|
wantAPIResourceDiscovery []apidiscoveryv2beta1.APIResourceDiscovery
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Basic Test",
|
||||||
|
resources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
|
||||||
|
Name: "pods",
|
||||||
|
Namespaced: true,
|
||||||
|
Kind: "Pod",
|
||||||
|
ShortNames: []string{"po"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
{
|
||||||
|
Resource: "pods",
|
||||||
|
Scope: apidiscoveryv2beta1.ScopeNamespace,
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Kind: "Pod",
|
||||||
|
},
|
||||||
|
ShortNames: []string{"po"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Basic Group Version Test",
|
||||||
|
resources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
Name: "cronjobs",
|
||||||
|
Namespaced: true,
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "CronJob",
|
||||||
|
ShortNames: []string{"cj"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
{
|
||||||
|
Resource: "cronjobs",
|
||||||
|
Scope: apidiscoveryv2beta1.ScopeNamespace,
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "CronJob",
|
||||||
|
},
|
||||||
|
ShortNames: []string{"cj"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test with subresource",
|
||||||
|
resources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
Name: "cronjobs",
|
||||||
|
Namespaced: true,
|
||||||
|
Kind: "CronJob",
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
ShortNames: []string{"cj"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "cronjobs/status",
|
||||||
|
Namespaced: true,
|
||||||
|
Kind: "CronJob",
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
ShortNames: []string{"cj"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
{
|
||||||
|
Resource: "cronjobs",
|
||||||
|
Scope: apidiscoveryv2beta1.ScopeNamespace,
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "CronJob",
|
||||||
|
},
|
||||||
|
ShortNames: []string{"cj"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{{
|
||||||
|
Subresource: "status",
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "CronJob",
|
||||||
|
},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test with subresource with no parent",
|
||||||
|
resources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
Name: "cronjobs/status",
|
||||||
|
Namespaced: true,
|
||||||
|
Kind: "CronJob",
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
{
|
||||||
|
Resource: "cronjobs",
|
||||||
|
Scope: apidiscoveryv2beta1.ScopeNamespace,
|
||||||
|
Subresources: []apidiscoveryv2beta1.APISubresourceDiscovery{{
|
||||||
|
Subresource: "status",
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
Kind: "CronJob",
|
||||||
|
},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Test with mismatch parent and subresource scope",
|
||||||
|
resources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
Name: "cronjobs",
|
||||||
|
Namespaced: true,
|
||||||
|
Kind: "CronJob",
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
ShortNames: []string{"cj"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "cronjobs/status",
|
||||||
|
Namespaced: false,
|
||||||
|
Kind: "CronJob",
|
||||||
|
Group: "batch",
|
||||||
|
Version: "v1",
|
||||||
|
ShortNames: []string{"cj"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cluster Scope Test",
|
||||||
|
resources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
Name: "nodes",
|
||||||
|
Namespaced: false,
|
||||||
|
Kind: "Node",
|
||||||
|
ShortNames: []string{"no"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
{
|
||||||
|
Resource: "nodes",
|
||||||
|
Scope: apidiscoveryv2beta1.ScopeCluster,
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Kind: "Node",
|
||||||
|
},
|
||||||
|
ShortNames: []string{"no"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Namespace Scope Test",
|
||||||
|
resources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
Name: "nodes",
|
||||||
|
Namespaced: true,
|
||||||
|
Kind: "Node",
|
||||||
|
ShortNames: []string{"no"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
{
|
||||||
|
Resource: "nodes",
|
||||||
|
Scope: apidiscoveryv2beta1.ScopeNamespace,
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Kind: "Node",
|
||||||
|
},
|
||||||
|
ShortNames: []string{"no"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Singular Resource Name",
|
||||||
|
resources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
Name: "nodes",
|
||||||
|
SingularName: "node",
|
||||||
|
Kind: "Node",
|
||||||
|
ShortNames: []string{"no"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAPIResourceDiscovery: []apidiscoveryv2beta1.APIResourceDiscovery{
|
||||||
|
{
|
||||||
|
Resource: "nodes",
|
||||||
|
SingularResource: "node",
|
||||||
|
Scope: apidiscoveryv2beta1.ScopeCluster,
|
||||||
|
ResponseKind: &metav1.GroupVersionKind{
|
||||||
|
Kind: "Node",
|
||||||
|
},
|
||||||
|
ShortNames: []string{"no"},
|
||||||
|
Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
discoveryAPIResources, err := ConvertGroupVersionIntoToDiscovery(tt.resources)
|
||||||
|
if err != nil {
|
||||||
|
if tt.wantErr == false {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
require.Equal(t, tt.wantAPIResourceDiscovery, discoveryAPIResources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -47,6 +47,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
|
discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||||
"k8s.io/apiserver/pkg/endpoints/filterlatency"
|
"k8s.io/apiserver/pkg/endpoints/filterlatency"
|
||||||
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
|
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
|
||||||
apiopenapi "k8s.io/apiserver/pkg/endpoints/openapi"
|
apiopenapi "k8s.io/apiserver/pkg/endpoints/openapi"
|
||||||
@ -122,6 +123,7 @@ type Config struct {
|
|||||||
EnableIndex bool
|
EnableIndex bool
|
||||||
EnableProfiling bool
|
EnableProfiling bool
|
||||||
EnableDiscovery bool
|
EnableDiscovery bool
|
||||||
|
|
||||||
// Requires generic profiling enabled
|
// Requires generic profiling enabled
|
||||||
EnableContentionProfiling bool
|
EnableContentionProfiling bool
|
||||||
EnableMetrics bool
|
EnableMetrics bool
|
||||||
@ -259,6 +261,9 @@ type Config struct {
|
|||||||
|
|
||||||
// StorageVersionManager holds the storage versions of the API resources installed by this server.
|
// StorageVersionManager holds the storage versions of the API resources installed by this server.
|
||||||
StorageVersionManager storageversion.Manager
|
StorageVersionManager storageversion.Manager
|
||||||
|
|
||||||
|
// AggregatedDiscoveryGroupManager serves /apis in an aggregated form.
|
||||||
|
AggregatedDiscoveryGroupManager discoveryendpoint.ResourceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecommendedConfig struct {
|
type RecommendedConfig struct {
|
||||||
@ -668,6 +673,14 @@ func (c completedConfig) New(name string, delegationTarget DelegationTarget) (*G
|
|||||||
muxAndDiscoveryCompleteSignals: map[string]<-chan struct{}{},
|
muxAndDiscoveryCompleteSignals: map[string]<-chan struct{}{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AggregatedDiscoveryEndpoint) {
|
||||||
|
manager := c.AggregatedDiscoveryGroupManager
|
||||||
|
if manager == nil {
|
||||||
|
manager = discoveryendpoint.NewResourceManager()
|
||||||
|
}
|
||||||
|
s.AggregatedDiscoveryGroupManager = manager
|
||||||
|
s.AggregatedLegacyDiscoveryGroupManager = discoveryendpoint.NewResourceManager()
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
if c.JSONPatchMaxCopyBytes <= 0 {
|
if c.JSONPatchMaxCopyBytes <= 0 {
|
||||||
break
|
break
|
||||||
|
@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
systemd "github.com/coreos/go-systemd/v22/daemon"
|
systemd "github.com/coreos/go-systemd/v22/daemon"
|
||||||
|
|
||||||
|
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -39,6 +40,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
genericapi "k8s.io/apiserver/pkg/endpoints"
|
genericapi "k8s.io/apiserver/pkg/endpoints"
|
||||||
"k8s.io/apiserver/pkg/endpoints/discovery"
|
"k8s.io/apiserver/pkg/endpoints/discovery"
|
||||||
|
discoveryendpoint "k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||||
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
@ -137,9 +139,15 @@ type GenericAPIServer struct {
|
|||||||
// listedPathProvider is a lister which provides the set of paths to show at /
|
// listedPathProvider is a lister which provides the set of paths to show at /
|
||||||
listedPathProvider routes.ListedPathProvider
|
listedPathProvider routes.ListedPathProvider
|
||||||
|
|
||||||
// DiscoveryGroupManager serves /apis
|
// DiscoveryGroupManager serves /apis in an unaggregated form.
|
||||||
DiscoveryGroupManager discovery.GroupManager
|
DiscoveryGroupManager discovery.GroupManager
|
||||||
|
|
||||||
|
// AggregatedDiscoveryGroupManager serves /apis in an aggregated form.
|
||||||
|
AggregatedDiscoveryGroupManager discoveryendpoint.ResourceManager
|
||||||
|
|
||||||
|
// AggregatedLegacyDiscoveryGroupManager serves /api in an aggregated form.
|
||||||
|
AggregatedLegacyDiscoveryGroupManager discoveryendpoint.ResourceManager
|
||||||
|
|
||||||
// Enable swagger and/or OpenAPI if these configs are non-nil.
|
// Enable swagger and/or OpenAPI if these configs are non-nil.
|
||||||
openAPIConfig *openapicommon.Config
|
openAPIConfig *openapicommon.Config
|
||||||
|
|
||||||
@ -676,11 +684,35 @@ func (s *GenericAPIServer) installAPIResources(apiPrefix string, apiGroupInfo *A
|
|||||||
|
|
||||||
apiGroupVersion.MaxRequestBodyBytes = s.maxRequestBodyBytes
|
apiGroupVersion.MaxRequestBodyBytes = s.maxRequestBodyBytes
|
||||||
|
|
||||||
r, err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer)
|
discoveryAPIResources, r, err := apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err)
|
return fmt.Errorf("unable to setup API %v: %v", apiGroupInfo, err)
|
||||||
}
|
}
|
||||||
resourceInfos = append(resourceInfos, r...)
|
resourceInfos = append(resourceInfos, r...)
|
||||||
|
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.AggregatedDiscoveryEndpoint) {
|
||||||
|
// Aggregated discovery only aggregates resources under /apis
|
||||||
|
if apiPrefix == APIGroupPrefix {
|
||||||
|
s.AggregatedDiscoveryGroupManager.AddGroupVersion(
|
||||||
|
groupVersion.Group,
|
||||||
|
apidiscoveryv2beta1.APIVersionDiscovery{
|
||||||
|
Version: groupVersion.Version,
|
||||||
|
Resources: discoveryAPIResources,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// There is only one group version for legacy resources, priority can be defaulted to 0.
|
||||||
|
s.AggregatedLegacyDiscoveryGroupManager.AddGroupVersion(
|
||||||
|
groupVersion.Group,
|
||||||
|
apidiscoveryv2beta1.APIVersionDiscovery{
|
||||||
|
Version: groupVersion.Version,
|
||||||
|
Resources: discoveryAPIResources,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.RegisterDestroyFunc(apiGroupInfo.destroyStorage)
|
s.RegisterDestroyFunc(apiGroupInfo.destroyStorage)
|
||||||
@ -715,7 +747,13 @@ func (s *GenericAPIServer) InstallLegacyAPIGroup(apiPrefix string, apiGroupInfo
|
|||||||
|
|
||||||
// Install the version handler.
|
// Install the version handler.
|
||||||
// Add a handler at /<apiPrefix> to enumerate the supported api versions.
|
// Add a handler at /<apiPrefix> to enumerate the supported api versions.
|
||||||
s.Handler.GoRestfulContainer.Add(discovery.NewLegacyRootAPIHandler(s.discoveryAddresses, s.Serializer, apiPrefix).WebService())
|
legacyRootAPIHandler := discovery.NewLegacyRootAPIHandler(s.discoveryAddresses, s.Serializer, apiPrefix)
|
||||||
|
if utilfeature.DefaultFeatureGate.Enabled(features.AggregatedDiscoveryEndpoint) {
|
||||||
|
wrapped := discoveryendpoint.WrapAggregatedDiscoveryToHandler(legacyRootAPIHandler, s.AggregatedLegacyDiscoveryGroupManager)
|
||||||
|
s.Handler.GoRestfulContainer.Add(wrapped.GenerateWebService("/api", metav1.APIVersions{}))
|
||||||
|
} else {
|
||||||
|
s.Handler.GoRestfulContainer.Add(legacyRootAPIHandler.WebService())
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -1497,6 +1497,7 @@ k8s.io/apiserver/pkg/cel/metrics
|
|||||||
k8s.io/apiserver/pkg/endpoints
|
k8s.io/apiserver/pkg/endpoints
|
||||||
k8s.io/apiserver/pkg/endpoints/deprecation
|
k8s.io/apiserver/pkg/endpoints/deprecation
|
||||||
k8s.io/apiserver/pkg/endpoints/discovery
|
k8s.io/apiserver/pkg/endpoints/discovery
|
||||||
|
k8s.io/apiserver/pkg/endpoints/discovery/aggregated
|
||||||
k8s.io/apiserver/pkg/endpoints/filterlatency
|
k8s.io/apiserver/pkg/endpoints/filterlatency
|
||||||
k8s.io/apiserver/pkg/endpoints/filters
|
k8s.io/apiserver/pkg/endpoints/filters
|
||||||
k8s.io/apiserver/pkg/endpoints/handlers
|
k8s.io/apiserver/pkg/endpoints/handlers
|
||||||
|
Loading…
Reference in New Issue
Block a user