mirror of https://github.com/k3s-io/k3s
Merge pull request #55259 from ironcladlou/gc-partial-discovery
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Tolerate partial discovery in garbage collector Allow the garbage collector to tolerate partial discovery failures. On a partial failure, use whatever was discovered, log the failures, and allow the resync logic to try again later. Fixes #55022. ```release-note API discovery failures no longer crash the kube controller manager via the garbage collector. ``` /cc @caesarxuchaopull/6/head
commit
42d5dc709e
|
@ -348,10 +348,7 @@ func startGarbageCollectorController(ctx ControllerContext) (bool, error) {
|
||||||
clientPool := dynamic.NewClientPool(config, restMapper, dynamic.LegacyAPIPathResolverFunc)
|
clientPool := dynamic.NewClientPool(config, restMapper, dynamic.LegacyAPIPathResolverFunc)
|
||||||
|
|
||||||
// Get an initial set of deletable resources to prime the garbage collector.
|
// Get an initial set of deletable resources to prime the garbage collector.
|
||||||
deletableResources, err := garbagecollector.GetDeletableResources(discoveryClient)
|
deletableResources := garbagecollector.GetDeletableResources(discoveryClient)
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
ignoredResources := make(map[schema.GroupResource]struct{})
|
ignoredResources := make(map[schema.GroupResource]struct{})
|
||||||
for _, r := range ctx.Options.GCIgnoredResources {
|
for _, r := range ctx.Options.GCIgnoredResources {
|
||||||
ignoredResources[schema.GroupResource{Group: r.Group, Resource: r.Resource}] = struct{}{}
|
ignoredResources[schema.GroupResource{Group: r.Group, Resource: r.Resource}] = struct{}{}
|
||||||
|
|
|
@ -66,6 +66,7 @@ go_test(
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
|
||||||
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library",
|
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library",
|
||||||
|
"//vendor/k8s.io/client-go/discovery:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/dynamic:go_default_library",
|
"//vendor/k8s.io/client-go/dynamic:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/informers:go_default_library",
|
"//vendor/k8s.io/client-go/informers:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
|
"//vendor/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||||
|
|
|
@ -171,11 +171,7 @@ func (gc *GarbageCollector) Sync(discoveryClient discovery.DiscoveryInterface, p
|
||||||
oldResources := make(map[schema.GroupVersionResource]struct{})
|
oldResources := make(map[schema.GroupVersionResource]struct{})
|
||||||
wait.Until(func() {
|
wait.Until(func() {
|
||||||
// Get the current resource list from discovery.
|
// Get the current resource list from discovery.
|
||||||
newResources, err := GetDeletableResources(discoveryClient)
|
newResources := GetDeletableResources(discoveryClient)
|
||||||
if err != nil {
|
|
||||||
utilruntime.HandleError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect first or abnormal sync and try again later.
|
// Detect first or abnormal sync and try again later.
|
||||||
if oldResources == nil || len(oldResources) == 0 {
|
if oldResources == nil || len(oldResources) == 0 {
|
||||||
|
@ -592,15 +588,37 @@ func (gc *GarbageCollector) GraphHasUID(UIDs []types.UID) bool {
|
||||||
// GetDeletableResources returns all resources from discoveryClient that the
|
// GetDeletableResources returns all resources from discoveryClient that the
|
||||||
// garbage collector should recognize and work with. More specifically, all
|
// garbage collector should recognize and work with. More specifically, all
|
||||||
// preferred resources which support the 'delete' verb.
|
// preferred resources which support the 'delete' verb.
|
||||||
func GetDeletableResources(discoveryClient discovery.DiscoveryInterface) (map[schema.GroupVersionResource]struct{}, error) {
|
//
|
||||||
|
// All discovery errors are considered temporary. Upon encountering any error,
|
||||||
|
// GetDeletableResources will log and return any discovered resources it was
|
||||||
|
// able to process (which may be none).
|
||||||
|
func GetDeletableResources(discoveryClient discovery.ServerResourcesInterface) map[schema.GroupVersionResource]struct{} {
|
||||||
preferredResources, err := discoveryClient.ServerPreferredResources()
|
preferredResources, err := discoveryClient.ServerPreferredResources()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get supported resources from server: %v", err)
|
if discovery.IsGroupDiscoveryFailedError(err) {
|
||||||
|
glog.Warning("failed to discover some groups: %v", err.(*discovery.ErrGroupDiscoveryFailed).Groups)
|
||||||
|
} else {
|
||||||
|
glog.Warning("failed to discover preferred resources: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if preferredResources == nil {
|
||||||
|
return map[schema.GroupVersionResource]struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is extracted from discovery.GroupVersionResources to allow tolerating
|
||||||
|
// failures on a per-resource basis.
|
||||||
deletableResources := discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{"delete"}}, preferredResources)
|
deletableResources := discovery.FilteredBy(discovery.SupportsAllVerbs{Verbs: []string{"delete"}}, preferredResources)
|
||||||
deletableGroupVersionResources, err := discovery.GroupVersionResources(deletableResources)
|
deletableGroupVersionResources := map[schema.GroupVersionResource]struct{}{}
|
||||||
if err != nil {
|
for _, rl := range deletableResources {
|
||||||
return nil, fmt.Errorf("Failed to parse resources from server: %v", err)
|
gv, err := schema.ParseGroupVersion(rl.GroupVersion)
|
||||||
|
if err != nil {
|
||||||
|
glog.Warning("ignoring invalid discovered resource %q: %v", rl.GroupVersion, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range rl.APIResources {
|
||||||
|
deletableGroupVersionResources[schema.GroupVersionResource{Group: gv.Group, Version: gv.Version, Resource: rl.APIResources[i].Name}] = struct{}{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return deletableGroupVersionResources, nil
|
|
||||||
|
return deletableGroupVersionResources
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package garbagecollector
|
package garbagecollector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
@ -37,6 +38,7 @@ import (
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||||
|
"k8s.io/client-go/discovery"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
@ -671,3 +673,109 @@ func TestOrphanDependentsFailure(t *testing.T) {
|
||||||
t.Errorf("expected error contains text %s, got %v", expected, err)
|
t.Errorf("expected error contains text %s, got %v", expected, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestGetDeletableResources ensures GetDeletableResources always returns
|
||||||
|
// something usable regardless of discovery output.
|
||||||
|
func TestGetDeletableResources(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
serverResources []*metav1.APIResourceList
|
||||||
|
err error
|
||||||
|
deletableResources map[schema.GroupVersionResource]struct{}
|
||||||
|
}{
|
||||||
|
"no error": {
|
||||||
|
serverResources: []*metav1.APIResourceList{
|
||||||
|
{
|
||||||
|
// Valid GroupVersion
|
||||||
|
GroupVersion: "apps/v1",
|
||||||
|
APIResources: []metav1.APIResource{
|
||||||
|
{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"delete"}},
|
||||||
|
{Name: "services", Namespaced: true, Kind: "Service"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Invalid GroupVersion, should be ignored
|
||||||
|
GroupVersion: "foo//whatever",
|
||||||
|
APIResources: []metav1.APIResource{
|
||||||
|
{Name: "bars", Namespaced: true, Kind: "Bar", Verbs: metav1.Verbs{"delete"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: nil,
|
||||||
|
deletableResources: map[schema.GroupVersionResource]struct{}{
|
||||||
|
{Group: "apps", Version: "v1", Resource: "pods"}: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nonspecific failure, includes usable results": {
|
||||||
|
serverResources: []*metav1.APIResourceList{
|
||||||
|
{
|
||||||
|
GroupVersion: "apps/v1",
|
||||||
|
APIResources: []metav1.APIResource{
|
||||||
|
{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"delete"}},
|
||||||
|
{Name: "services", Namespaced: true, Kind: "Service"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: fmt.Errorf("internal error"),
|
||||||
|
deletableResources: map[schema.GroupVersionResource]struct{}{
|
||||||
|
{Group: "apps", Version: "v1", Resource: "pods"}: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"partial discovery failure, includes usable results": {
|
||||||
|
serverResources: []*metav1.APIResourceList{
|
||||||
|
{
|
||||||
|
GroupVersion: "apps/v1",
|
||||||
|
APIResources: []metav1.APIResource{
|
||||||
|
{Name: "pods", Namespaced: true, Kind: "Pod", Verbs: metav1.Verbs{"delete"}},
|
||||||
|
{Name: "services", Namespaced: true, Kind: "Service"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
err: &discovery.ErrGroupDiscoveryFailed{
|
||||||
|
Groups: map[schema.GroupVersion]error{
|
||||||
|
{Group: "foo", Version: "v1"}: fmt.Errorf("discovery failure"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
deletableResources: map[schema.GroupVersionResource]struct{}{
|
||||||
|
{Group: "apps", Version: "v1", Resource: "pods"}: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"discovery failure, no results": {
|
||||||
|
serverResources: nil,
|
||||||
|
err: fmt.Errorf("internal error"),
|
||||||
|
deletableResources: map[schema.GroupVersionResource]struct{}{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, test := range tests {
|
||||||
|
t.Logf("testing %q", name)
|
||||||
|
client := &fakeServerResources{
|
||||||
|
PreferredResources: test.serverResources,
|
||||||
|
Error: test.err,
|
||||||
|
}
|
||||||
|
actual := GetDeletableResources(client)
|
||||||
|
if !reflect.DeepEqual(test.deletableResources, actual) {
|
||||||
|
t.Errorf("expected resources:\n%v\ngot:\n%v", test.deletableResources, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeServerResources struct {
|
||||||
|
PreferredResources []*metav1.APIResourceList
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ *fakeServerResources) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ *fakeServerResources) ServerResources() ([]*metav1.APIResourceList, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeServerResources) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
|
||||||
|
return f.PreferredResources, f.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_ *fakeServerResources) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
@ -233,10 +233,7 @@ func setup(t *testing.T, workerCount int) *testContext {
|
||||||
discoveryClient := cacheddiscovery.NewMemCacheClient(clientSet.Discovery())
|
discoveryClient := cacheddiscovery.NewMemCacheClient(clientSet.Discovery())
|
||||||
restMapper := discovery.NewDeferredDiscoveryRESTMapper(discoveryClient, meta.InterfacesForUnstructured)
|
restMapper := discovery.NewDeferredDiscoveryRESTMapper(discoveryClient, meta.InterfacesForUnstructured)
|
||||||
restMapper.Reset()
|
restMapper.Reset()
|
||||||
deletableResources, err := garbagecollector.GetDeletableResources(discoveryClient)
|
deletableResources := garbagecollector.GetDeletableResources(discoveryClient)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get deletable resources: %v", err)
|
|
||||||
}
|
|
||||||
config := *masterConfig
|
config := *masterConfig
|
||||||
config.ContentConfig = dynamic.ContentConfig()
|
config.ContentConfig = dynamic.ContentConfig()
|
||||||
metaOnlyClientPool := dynamic.NewClientPool(&config, restMapper, dynamic.LegacyAPIPathResolverFunc)
|
metaOnlyClientPool := dynamic.NewClientPool(&config, restMapper, dynamic.LegacyAPIPathResolverFunc)
|
||||||
|
|
Loading…
Reference in New Issue