mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-02-14 18:29:53 +00:00
fix(discovery): handle partial results from ServerGroupsAndResources (#1944)
This commit is contained in:
@@ -6,11 +6,15 @@ import (
|
||||
|
||||
// HasResource takes an api version and a kind of a resource and checks if the resource
|
||||
// is supported by the k8s api server.
|
||||
// This function handles partial results from ServerGroupsAndResources(): "The returned group and resource lists might be non-nil with partial
|
||||
// results even in the case of non-nil error."
|
||||
func HasResource(dc discovery.DiscoveryInterface, apiVersion, kind string) (bool, error) {
|
||||
_, apiLists, err := dc.ServerGroupsAndResources()
|
||||
if err != nil {
|
||||
|
||||
if apiLists == nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Compare the resource api version and kind and find the resource.
|
||||
for _, apiList := range apiLists {
|
||||
if apiList.GroupVersion == apiVersion {
|
||||
@@ -21,5 +25,6 @@ func HasResource(dc discovery.DiscoveryInterface, apiVersion, kind string) (bool
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
fakediscovery "k8s.io/client-go/discovery/fake"
|
||||
fakeclientset "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
@@ -78,3 +81,232 @@ func TestHasResource(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasResourceWithPartialDiscoveryFailure verifies that HasResource correctly handles
|
||||
// partial discovery failures where ServerGroupsAndResources() returns both an error AND
|
||||
// partial results (non-nil apiLists). This simulates real Kubernetes behavior when some
|
||||
// API groups fail to load but others succeed.
|
||||
func TestHasResourceWithPartialDiscoveryFailure(t *testing.T) {
|
||||
testKind := "Foo"
|
||||
testKindGroupVersion := "v1"
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
apiResourceList []*metav1.APIResourceList
|
||||
discoveryError error
|
||||
wantResult bool
|
||||
wantError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "resource found in partial results with discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Foo",
|
||||
},
|
||||
{
|
||||
Kind: "Bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: true,
|
||||
wantError: false,
|
||||
description: "Should return (true, nil) when resource exists in partial results despite discovery error",
|
||||
},
|
||||
{
|
||||
name: "resource not found in partial results with discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Bar",
|
||||
},
|
||||
{
|
||||
Kind: "Baz",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when resource not in partial results and discovery error exists",
|
||||
},
|
||||
{
|
||||
name: "nil api resource list with discovery error",
|
||||
apiResourceList: nil,
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when apiLists is nil and discovery error exists",
|
||||
},
|
||||
{
|
||||
name: "empty api resource list with discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when apiLists is empty and discovery error exists",
|
||||
},
|
||||
{
|
||||
name: "multiple groups with partial results and discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Pod",
|
||||
},
|
||||
{
|
||||
Kind: "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
GroupVersion: "v2",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
{Group: "batch", Version: "v1beta1"}: errors.New("failed to retrieve batch/v1beta1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when resource not found across multiple partial groups with discovery error",
|
||||
},
|
||||
{
|
||||
name: "resource found with different version in partial results with discovery error",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v2",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: &discovery.ErrGroupDiscoveryFailed{
|
||||
Groups: map[schema.GroupVersion]error{
|
||||
{Group: "apps", Version: "v1"}: errors.New("failed to retrieve apps/v1"),
|
||||
},
|
||||
},
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when resource exists with different version in partial results",
|
||||
},
|
||||
{
|
||||
name: "generic error with partial results containing resource",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: errors.New("connection timeout"),
|
||||
wantResult: true,
|
||||
wantError: false,
|
||||
description: "Should return (true, nil) when resource exists in partial results even with generic error",
|
||||
},
|
||||
{
|
||||
name: "generic error without resource in partial results",
|
||||
apiResourceList: []*metav1.APIResourceList{
|
||||
{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Kind: "Bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
discoveryError: errors.New("connection timeout"),
|
||||
wantResult: false,
|
||||
wantError: true,
|
||||
description: "Should return (false, error) when resource not in partial results with generic error",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client := fakeclientset.NewSimpleClientset()
|
||||
fakeDiscovery, ok := client.Discovery().(*fakediscovery.FakeDiscovery)
|
||||
if !ok {
|
||||
t.Fatalf("could not convert Discovery() to *FakeDiscovery")
|
||||
}
|
||||
|
||||
// Configure the fake discovery to return both resources and error
|
||||
fakeDiscovery.Resources = tc.apiResourceList
|
||||
|
||||
// Create a mock discovery interface that returns both error and partial results
|
||||
mockDiscovery := &mockDiscoveryWithPartialFailure{
|
||||
FakeDiscovery: fakeDiscovery,
|
||||
errorToReturn: tc.discoveryError,
|
||||
}
|
||||
|
||||
exists, err := HasResource(mockDiscovery, testKindGroupVersion, testKind)
|
||||
|
||||
// Verify error expectation
|
||||
if tc.wantError && err == nil {
|
||||
t.Errorf("%s: expected error but got nil", tc.description)
|
||||
}
|
||||
if !tc.wantError && err != nil {
|
||||
t.Errorf("%s: expected no error but got: %v", tc.description, err)
|
||||
}
|
||||
|
||||
// Verify result expectation
|
||||
if exists != tc.wantResult {
|
||||
t.Errorf("%s: unexpected result for HasResource:\n\t(WANT) %t\n\t(GOT) %t", tc.description, tc.wantResult, exists)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// mockDiscoveryWithPartialFailure wraps FakeDiscovery to simulate partial discovery failures
|
||||
// where ServerGroupsAndResources() returns both an error AND partial results.
|
||||
type mockDiscoveryWithPartialFailure struct {
|
||||
*fakediscovery.FakeDiscovery
|
||||
errorToReturn error
|
||||
}
|
||||
|
||||
// ServerGroupsAndResources simulates the Kubernetes API behavior where partial results
|
||||
// can be returned even when an error occurs. This happens when some API groups fail to
|
||||
// load but others succeed.
|
||||
func (m *mockDiscoveryWithPartialFailure) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
|
||||
groups, resources, _ := m.FakeDiscovery.ServerGroupsAndResources()
|
||||
return groups, resources, m.errorToReturn
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user