From dd1e150511a9ae98455bf60d20edb1d380da8f44 Mon Sep 17 00:00:00 2001 From: Nolan Brubaker Date: Fri, 20 Jul 2018 17:03:11 -0400 Subject: [PATCH] Add RBAC support for 1.7 clusters Signed-off-by: Nolan Brubaker --- pkg/backup/rbac.go | 140 ++++++++ pkg/backup/service_account_action.go | 37 +- pkg/backup/service_account_action_test.go | 415 ++++++++++++++++++++-- pkg/cmd/server/plugin/plugin.go | 9 +- pkg/discovery/helper.go | 17 + pkg/util/test/fake_discovery_helper.go | 13 + 6 files changed, 584 insertions(+), 47 deletions(-) create mode 100644 pkg/backup/rbac.go diff --git a/pkg/backup/rbac.go b/pkg/backup/rbac.go new file mode 100644 index 000000000..d8d82c34e --- /dev/null +++ b/pkg/backup/rbac.go @@ -0,0 +1,140 @@ +/* +Copyright 2018 the Heptio Ark contributors. + +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 backup + +import ( + "github.com/pkg/errors" + rbac "k8s.io/api/rbac/v1" + rbacbeta "k8s.io/api/rbac/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + rbacclient "k8s.io/client-go/kubernetes/typed/rbac/v1" + rbacbetaclient "k8s.io/client-go/kubernetes/typed/rbac/v1beta1" +) + +// ClusterRoleBindingLister allows for listing ClusterRoleBindings in a version-independent way. +type ClusterRoleBindingLister interface { + // List returns a slice of ClusterRoleBindings which can represent either v1 or v1beta1 ClusterRoleBindings. + List() ([]ClusterRoleBinding, error) +} + +// noopClusterRoleBindingLister exists to handle clusters where RBAC is disabled. +type noopClusterRoleBindingLister struct { +} + +func (noop noopClusterRoleBindingLister) List() ([]ClusterRoleBinding, error) { + return []ClusterRoleBinding{}, nil +} + +type v1ClusterRoleBindingLister struct { + client rbacclient.ClusterRoleBindingInterface +} + +func (v1 v1ClusterRoleBindingLister) List() ([]ClusterRoleBinding, error) { + crbList, err := v1.client.List(metav1.ListOptions{}) + if err != nil { + return nil, errors.WithStack(err) + } + var crbs []ClusterRoleBinding + for _, crb := range crbList.Items { + crbs = append(crbs, v1ClusterRoleBinding{crb: crb}) + } + + return crbs, nil +} + +type v1beta1ClusterRoleBindingLister struct { + client rbacbetaclient.ClusterRoleBindingInterface +} + +func (v1beta1 v1beta1ClusterRoleBindingLister) List() ([]ClusterRoleBinding, error) { + crbList, err := v1beta1.client.List(metav1.ListOptions{}) + if err != nil { + return nil, errors.WithStack(err) + } + var crbs []ClusterRoleBinding + for _, crb := range crbList.Items { + crbs = append(crbs, v1beta1ClusterRoleBinding{crb: crb}) + } + + return crbs, nil +} + +// NewClusterRoleBindingListerMap creates a map of RBAC version strings to their associated +// ClusterRoleBindingLister structs. +// Necessary so that callers to the ClusterRoleBindingLister interfaces don't need the kubernetes.Interface. +func NewClusterRoleBindingListerMap(clientset kubernetes.Interface) map[string]ClusterRoleBindingLister { + return map[string]ClusterRoleBindingLister{ + rbac.SchemeGroupVersion.Version: v1ClusterRoleBindingLister{client: clientset.RbacV1().ClusterRoleBindings()}, + rbacbeta.SchemeGroupVersion.Version: v1beta1ClusterRoleBindingLister{client: clientset.RbacV1beta1().ClusterRoleBindings()}, + "": noopClusterRoleBindingLister{}, + } +} + +// ClusterRoleBinding abstracts access to ClusterRoleBindings whether they're v1 or v1beta1. +type ClusterRoleBinding interface { + // Name returns the name of a ClusterRoleBinding. + Name() string + // ServiceAccountSubjects returns the names of subjects that are service accounts in the given namespace. + ServiceAccountSubjects(namespace string) []string + // RoleRefName returns the name of a ClusterRoleBinding's RoleRef. + RoleRefName() string +} + +type v1ClusterRoleBinding struct { + crb rbac.ClusterRoleBinding +} + +func (c v1ClusterRoleBinding) Name() string { + return c.crb.Name +} + +func (c v1ClusterRoleBinding) RoleRefName() string { + return c.crb.RoleRef.Name +} + +func (c v1ClusterRoleBinding) ServiceAccountSubjects(namespace string) []string { + var saSubjects []string + for _, s := range c.crb.Subjects { + if s.Kind == rbac.ServiceAccountKind && s.Namespace == namespace { + saSubjects = append(saSubjects, s.Name) + } + } + return saSubjects +} + +type v1beta1ClusterRoleBinding struct { + crb rbacbeta.ClusterRoleBinding +} + +func (c v1beta1ClusterRoleBinding) Name() string { + return c.crb.Name +} + +func (c v1beta1ClusterRoleBinding) RoleRefName() string { + return c.crb.RoleRef.Name +} + +func (c v1beta1ClusterRoleBinding) ServiceAccountSubjects(namespace string) []string { + var saSubjects []string + for _, s := range c.crb.Subjects { + if s.Kind == rbac.ServiceAccountKind && s.Namespace == namespace { + saSubjects = append(saSubjects, s.Name) + } + } + return saSubjects +} diff --git a/pkg/backup/service_account_action.go b/pkg/backup/service_account_action.go index 7a330723f..65a4270e2 100644 --- a/pkg/backup/service_account_action.go +++ b/pkg/backup/service_account_action.go @@ -25,28 +25,41 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" - rbacclient "k8s.io/client-go/kubernetes/typed/rbac/v1" "github.com/heptio/ark/pkg/apis/ark/v1" + arkdiscovery "github.com/heptio/ark/pkg/discovery" "github.com/heptio/ark/pkg/kuberesource" ) // serviceAccountAction implements ItemAction. type serviceAccountAction struct { log logrus.FieldLogger - clusterRoleBindings []rbac.ClusterRoleBinding + clusterRoleBindings []ClusterRoleBinding } // NewServiceAccountAction creates a new ItemAction for service accounts. -func NewServiceAccountAction(log logrus.FieldLogger, client rbacclient.ClusterRoleBindingInterface) (ItemAction, error) { - clusterRoleBindings, err := client.List(metav1.ListOptions{}) +func NewServiceAccountAction(log logrus.FieldLogger, clusterRoleBindingListers map[string]ClusterRoleBindingLister, discoveryHelper arkdiscovery.Helper) (ItemAction, error) { + // Look up the supported RBAC version + var supportedAPI metav1.GroupVersionForDiscovery + for _, ag := range discoveryHelper.APIGroups() { + if ag.Name == rbac.GroupName { + supportedAPI = ag.PreferredVersion + break + } + } + + crbLister := clusterRoleBindingListers[supportedAPI.Version] + + // This should be safe because the List call will return a 0-item slice + // if there's no matching API version. + crbs, err := crbLister.List() if err != nil { - return nil, errors.WithStack(err) + return nil, err } return &serviceAccountAction{ log: log, - clusterRoleBindings: clusterRoleBindings.Items, + clusterRoleBindings: crbs, }, nil } @@ -76,14 +89,14 @@ func (a *serviceAccountAction) Execute(item runtime.Unstructured, backup *v1.Bac roles = sets.NewString() ) - for _, clusterRoleBinding := range a.clusterRoleBindings { - for _, subj := range clusterRoleBinding.Subjects { - if subj.Kind == rbac.ServiceAccountKind && subj.Namespace == namespace && subj.Name == name { + for _, crb := range a.clusterRoleBindings { + for _, s := range crb.ServiceAccountSubjects(namespace) { + if s == name { a.log.Infof("Adding clusterrole %s and clusterrolebinding %s to additionalItems since serviceaccount %s/%s is a subject", - clusterRoleBinding.RoleRef.Name, clusterRoleBinding.Name, namespace, name) + crb.RoleRefName(), crb, namespace, name) - bindings.Insert(clusterRoleBinding.Name) - roles.Insert(clusterRoleBinding.RoleRef.Name) + bindings.Insert(crb.Name()) + roles.Insert(crb.RoleRefName()) break } } diff --git a/pkg/backup/service_account_action_test.go b/pkg/backup/service_account_action_test.go index 2ef88b49f..ae5af9af2 100644 --- a/pkg/backup/service_account_action_test.go +++ b/pkg/backup/service_account_action_test.go @@ -24,29 +24,61 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "k8s.io/api/rbac/v1" + rbac "k8s.io/api/rbac/v1" + rbacbeta "k8s.io/api/rbac/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - rbacclient "k8s.io/client-go/kubernetes/typed/rbac/v1" "github.com/heptio/ark/pkg/kuberesource" arktest "github.com/heptio/ark/pkg/util/test" ) -type fakeClusterRoleBindingClient struct { - clusterRoleBindings []v1.ClusterRoleBinding +func newV1ClusterRoleBindingList(rbacCRBList []rbac.ClusterRoleBinding) []ClusterRoleBinding { + var crbs []ClusterRoleBinding + for _, c := range rbacCRBList { + crbs = append(crbs, v1ClusterRoleBinding{crb: c}) + } - rbacclient.ClusterRoleBindingInterface + return crbs } -func (c *fakeClusterRoleBindingClient) List(opts metav1.ListOptions) (*v1.ClusterRoleBindingList, error) { - return &v1.ClusterRoleBindingList{ - Items: c.clusterRoleBindings, - }, nil +func newV1beta1ClusterRoleBindingList(rbacCRBList []rbacbeta.ClusterRoleBinding) []ClusterRoleBinding { + var crbs []ClusterRoleBinding + for _, c := range rbacCRBList { + crbs = append(crbs, v1beta1ClusterRoleBinding{crb: c}) + } + + return crbs +} + +type FakeV1ClusterRoleBindingLister struct { + v1crbs []rbac.ClusterRoleBinding +} + +func (f FakeV1ClusterRoleBindingLister) List() ([]ClusterRoleBinding, error) { + var crbs []ClusterRoleBinding + for _, c := range f.v1crbs { + crbs = append(crbs, v1ClusterRoleBinding{crb: c}) + } + return crbs, nil +} + +type FakeV1beta1ClusterRoleBindingLister struct { + v1beta1crbs []rbacbeta.ClusterRoleBinding +} + +func (f FakeV1beta1ClusterRoleBindingLister) List() ([]ClusterRoleBinding, error) { + var crbs []ClusterRoleBinding + for _, c := range f.v1beta1crbs { + crbs = append(crbs, v1beta1ClusterRoleBinding{crb: c}) + } + return crbs, nil } func TestServiceAccountActionAppliesTo(t *testing.T) { - a, _ := NewServiceAccountAction(arktest.NewLogger(), &fakeClusterRoleBindingClient{}) + // Instantiating the struct directly since using + // NewServiceAccountAction requires a full kubernetes clientset + a := &serviceAccountAction{} actual, err := a.AppliesTo() require.NoError(t, err) @@ -57,11 +89,119 @@ func TestServiceAccountActionAppliesTo(t *testing.T) { assert.Equal(t, expected, actual) } +func TestNewServiceAccountAction(t *testing.T) { + tests := []struct { + name string + version string + expectedCRBs []ClusterRoleBinding + }{ + { + name: "rbac v1 API instantiates an saAction", + version: rbac.SchemeGroupVersion.Version, + expectedCRBs: []ClusterRoleBinding{ + v1ClusterRoleBinding{ + crb: rbac.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "v1crb-1", + }, + }, + }, + v1ClusterRoleBinding{ + crb: rbac.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "v1crb-2", + }, + }, + }, + }, + }, + { + name: "rbac v1beta1 API instantiates an saAction", + version: rbacbeta.SchemeGroupVersion.Version, + expectedCRBs: []ClusterRoleBinding{ + v1beta1ClusterRoleBinding{ + crb: rbacbeta.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "v1beta1crb-1", + }, + }, + }, + v1beta1ClusterRoleBinding{ + crb: rbacbeta.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "v1beta1crb-2", + }, + }, + }, + }, + }, + { + name: "no RBAC API instantiates an saAction with empty slice", + version: "", + expectedCRBs: []ClusterRoleBinding{}, + }, + } + // Set up all of our fakes outside the test loop + discoveryHelper := arktest.FakeDiscoveryHelper{} + logger := arktest.NewLogger() + + v1crbs := []rbac.ClusterRoleBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "v1crb-1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "v1crb-2", + }, + }, + } + + v1beta1crbs := []rbacbeta.ClusterRoleBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "v1beta1crb-1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "v1beta1crb-2", + }, + }, + } + + clusterRoleBindingListers := map[string]ClusterRoleBindingLister{ + rbac.SchemeGroupVersion.Version: FakeV1ClusterRoleBindingLister{v1crbs: v1crbs}, + rbacbeta.SchemeGroupVersion.Version: FakeV1beta1ClusterRoleBindingLister{v1beta1crbs: v1beta1crbs}, + "": noopClusterRoleBindingLister{}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // We only care about the preferred version, nothing else in the list + discoveryHelper.APIGroupsList = []metav1.APIGroup{ + { + Name: rbac.GroupName, + PreferredVersion: metav1.GroupVersionForDiscovery{ + Version: test.version, + }, + }, + } + action, err := NewServiceAccountAction(logger, clusterRoleBindingListers, &discoveryHelper) + require.NoError(t, err) + saAction, ok := action.(*serviceAccountAction) + require.True(t, ok) + assert.Equal(t, test.expectedCRBs, saAction.clusterRoleBindings) + }) + } +} + func TestServiceAccountActionExecute(t *testing.T) { tests := []struct { name string serviceAccount runtime.Unstructured - crbs []v1.ClusterRoleBinding + crbs []rbac.ClusterRoleBinding expectedAdditionalItems []ResourceIdentifier }{ { @@ -91,9 +231,9 @@ func TestServiceAccountActionExecute(t *testing.T) { } } `), - crbs: []v1.ClusterRoleBinding{ + crbs: []rbac.ClusterRoleBinding{ { - Subjects: []v1.Subject{ + Subjects: []rbac.Subject{ { Kind: "non-matching-kind", Namespace: "non-matching-ns", @@ -105,17 +245,17 @@ func TestServiceAccountActionExecute(t *testing.T) { Name: "ark", }, { - Kind: v1.ServiceAccountKind, + Kind: rbac.ServiceAccountKind, Namespace: "non-matching-ns", Name: "ark", }, { - Kind: v1.ServiceAccountKind, + Kind: rbac.ServiceAccountKind, Namespace: "heptio-ark", Name: "non-matching-name", }, }, - RoleRef: v1.RoleRef{ + RoleRef: rbac.RoleRef{ Name: "role", }, }, @@ -134,19 +274,19 @@ func TestServiceAccountActionExecute(t *testing.T) { } } `), - crbs: []v1.ClusterRoleBinding{ + crbs: []rbac.ClusterRoleBinding{ { ObjectMeta: metav1.ObjectMeta{ Name: "crb-1", }, - Subjects: []v1.Subject{ + Subjects: []rbac.Subject{ { Kind: "non-matching-kind", Namespace: "non-matching-ns", Name: "non-matching-name", }, }, - RoleRef: v1.RoleRef{ + RoleRef: rbac.RoleRef{ Name: "role-1", }, }, @@ -154,19 +294,19 @@ func TestServiceAccountActionExecute(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "crb-2", }, - Subjects: []v1.Subject{ + Subjects: []rbac.Subject{ { Kind: "non-matching-kind", Namespace: "non-matching-ns", Name: "non-matching-name", }, { - Kind: v1.ServiceAccountKind, + Kind: rbac.ServiceAccountKind, Namespace: "heptio-ark", Name: "ark", }, }, - RoleRef: v1.RoleRef{ + RoleRef: rbac.RoleRef{ Name: "role-2", }, }, @@ -174,14 +314,14 @@ func TestServiceAccountActionExecute(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "crb-3", }, - Subjects: []v1.Subject{ + Subjects: []rbac.Subject{ { - Kind: v1.ServiceAccountKind, + Kind: rbac.ServiceAccountKind, Namespace: "heptio-ark", Name: "ark", }, }, - RoleRef: v1.RoleRef{ + RoleRef: rbac.RoleRef{ Name: "role-3", }, }, @@ -189,9 +329,9 @@ func TestServiceAccountActionExecute(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "crb-4", }, - Subjects: []v1.Subject{ + Subjects: []rbac.Subject{ { - Kind: v1.ServiceAccountKind, + Kind: rbac.ServiceAccountKind, Namespace: "heptio-ark", Name: "ark", }, @@ -201,7 +341,7 @@ func TestServiceAccountActionExecute(t *testing.T) { Name: "non-matching-name", }, }, - RoleRef: v1.RoleRef{ + RoleRef: rbac.RoleRef{ Name: "role-4", }, }, @@ -235,14 +375,221 @@ func TestServiceAccountActionExecute(t *testing.T) { }, } - crbClient := &fakeClusterRoleBindingClient{} - for _, test := range tests { t.Run(test.name, func(t *testing.T) { - crbClient.clusterRoleBindings = test.crbs - - action, err := NewServiceAccountAction(arktest.NewLogger(), crbClient) - require.Nil(t, err) + // Create the action struct directly so we don't need to mock a clientset + action := &serviceAccountAction{ + log: arktest.NewLogger(), + clusterRoleBindings: newV1ClusterRoleBindingList(test.crbs), + } + + res, additional, err := action.Execute(test.serviceAccount, nil) + + assert.Equal(t, test.serviceAccount, res) + assert.Nil(t, err) + + // ensure slices are ordered for valid comparison + sort.Slice(test.expectedAdditionalItems, func(i, j int) bool { + return fmt.Sprintf("%s.%s", test.expectedAdditionalItems[i].GroupResource.String(), test.expectedAdditionalItems[i].Name) < + fmt.Sprintf("%s.%s", test.expectedAdditionalItems[j].GroupResource.String(), test.expectedAdditionalItems[j].Name) + }) + + sort.Slice(additional, func(i, j int) bool { + return fmt.Sprintf("%s.%s", additional[i].GroupResource.String(), additional[i].Name) < + fmt.Sprintf("%s.%s", additional[j].GroupResource.String(), additional[j].Name) + }) + + assert.Equal(t, test.expectedAdditionalItems, additional) + }) + } + +} + +func TestServiceAccountActionExecuteOnBeta1(t *testing.T) { + tests := []struct { + name string + serviceAccount runtime.Unstructured + crbs []rbacbeta.ClusterRoleBinding + expectedAdditionalItems []ResourceIdentifier + }{ + { + name: "no crbs", + serviceAccount: arktest.UnstructuredOrDie(` + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "namespace": "heptio-ark", + "name": "ark" + } + } + `), + crbs: nil, + expectedAdditionalItems: nil, + }, + { + name: "no matching crbs", + serviceAccount: arktest.UnstructuredOrDie(` + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "namespace": "heptio-ark", + "name": "ark" + } + } + `), + crbs: []rbacbeta.ClusterRoleBinding{ + { + Subjects: []rbacbeta.Subject{ + { + Kind: "non-matching-kind", + Namespace: "non-matching-ns", + Name: "non-matching-name", + }, + { + Kind: "non-matching-kind", + Namespace: "heptio-ark", + Name: "ark", + }, + { + Kind: rbacbeta.ServiceAccountKind, + Namespace: "non-matching-ns", + Name: "ark", + }, + { + Kind: rbacbeta.ServiceAccountKind, + Namespace: "heptio-ark", + Name: "non-matching-name", + }, + }, + RoleRef: rbacbeta.RoleRef{ + Name: "role", + }, + }, + }, + expectedAdditionalItems: nil, + }, + { + name: "some matching crbs", + serviceAccount: arktest.UnstructuredOrDie(` + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "namespace": "heptio-ark", + "name": "ark" + } + } + `), + crbs: []rbacbeta.ClusterRoleBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "crb-1", + }, + Subjects: []rbacbeta.Subject{ + { + Kind: "non-matching-kind", + Namespace: "non-matching-ns", + Name: "non-matching-name", + }, + }, + RoleRef: rbacbeta.RoleRef{ + Name: "role-1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "crb-2", + }, + Subjects: []rbacbeta.Subject{ + { + Kind: "non-matching-kind", + Namespace: "non-matching-ns", + Name: "non-matching-name", + }, + { + Kind: rbacbeta.ServiceAccountKind, + Namespace: "heptio-ark", + Name: "ark", + }, + }, + RoleRef: rbacbeta.RoleRef{ + Name: "role-2", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "crb-3", + }, + Subjects: []rbacbeta.Subject{ + { + Kind: rbacbeta.ServiceAccountKind, + Namespace: "heptio-ark", + Name: "ark", + }, + }, + RoleRef: rbacbeta.RoleRef{ + Name: "role-3", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "crb-4", + }, + Subjects: []rbacbeta.Subject{ + { + Kind: rbacbeta.ServiceAccountKind, + Namespace: "heptio-ark", + Name: "ark", + }, + { + Kind: "non-matching-kind", + Namespace: "non-matching-ns", + Name: "non-matching-name", + }, + }, + RoleRef: rbacbeta.RoleRef{ + Name: "role-4", + }, + }, + }, + expectedAdditionalItems: []ResourceIdentifier{ + { + GroupResource: kuberesource.ClusterRoleBindings, + Name: "crb-2", + }, + { + GroupResource: kuberesource.ClusterRoleBindings, + Name: "crb-3", + }, + { + GroupResource: kuberesource.ClusterRoleBindings, + Name: "crb-4", + }, + { + GroupResource: kuberesource.ClusterRoles, + Name: "role-2", + }, + { + GroupResource: kuberesource.ClusterRoles, + Name: "role-3", + }, + { + GroupResource: kuberesource.ClusterRoles, + Name: "role-4", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create the action struct directly so we don't need to mock a clientset + action := &serviceAccountAction{ + log: arktest.NewLogger(), + clusterRoleBindings: newV1beta1ClusterRoleBindingList(test.crbs), + } res, additional, err := action.Execute(test.serviceAccount, nil) diff --git a/pkg/cmd/server/plugin/plugin.go b/pkg/cmd/server/plugin/plugin.go index 3e3247e1b..48ee1da30 100644 --- a/pkg/cmd/server/plugin/plugin.go +++ b/pkg/cmd/server/plugin/plugin.go @@ -28,6 +28,7 @@ import ( "github.com/heptio/ark/pkg/cloudprovider/azure" "github.com/heptio/ark/pkg/cloudprovider/gcp" "github.com/heptio/ark/pkg/cmd" + arkdiscovery "github.com/heptio/ark/pkg/discovery" arkplugin "github.com/heptio/ark/pkg/plugin" "github.com/heptio/ark/pkg/restore" ) @@ -87,7 +88,13 @@ func NewCommand(f client.Factory) *cobra.Command { clientset, err := f.KubeClient() cmd.CheckError(err) - action, err = backup.NewServiceAccountAction(logger, clientset.RbacV1().ClusterRoleBindings()) + discoveryHelper, err := arkdiscovery.NewHelper(clientset.Discovery(), logger) + cmd.CheckError(err) + + action, err = backup.NewServiceAccountAction( + logger, + backup.NewClusterRoleBindingListerMap(clientset), + discoveryHelper) cmd.CheckError(err) default: logger.Fatal("Unrecognized plugin name") diff --git a/pkg/discovery/helper.go b/pkg/discovery/helper.go index 4f6cfb085..ba3d4592e 100644 --- a/pkg/discovery/helper.go +++ b/pkg/discovery/helper.go @@ -45,6 +45,10 @@ type Helper interface { // Refresh pulls an updated set of Ark-backuppable resources from the // discovery API. Refresh() error + + // APIGroups gets the current set of supported APIGroups + // in the cluster. + APIGroups() []metav1.APIGroup } type helper struct { @@ -56,6 +60,7 @@ type helper struct { mapper meta.RESTMapper resources []*metav1.APIResourceList resourcesMap map[schema.GroupVersionResource]metav1.APIResource + apiGroups []metav1.APIGroup } var _ Helper = &helper{} @@ -127,6 +132,12 @@ func (h *helper) Refresh() error { } } + apiGroupList, err := h.discoveryClient.ServerGroups() + if err != nil { + return errors.WithStack(err) + } + h.apiGroups = apiGroupList.Groups + return nil } @@ -165,3 +176,9 @@ func (h *helper) Resources() []*metav1.APIResourceList { defer h.lock.RUnlock() return h.resources } + +func (h *helper) APIGroups() []metav1.APIGroup { + h.lock.RLock() + defer h.lock.RUnlock() + return h.apiGroups +} diff --git a/pkg/util/test/fake_discovery_helper.go b/pkg/util/test/fake_discovery_helper.go index 874982d09..82f6b8609 100644 --- a/pkg/util/test/fake_discovery_helper.go +++ b/pkg/util/test/fake_discovery_helper.go @@ -29,6 +29,7 @@ type FakeDiscoveryHelper struct { ResourceList []*metav1.APIResourceList Mapper meta.RESTMapper AutoReturnResource bool + APIGroupsList []metav1.APIGroup } func NewFakeDiscoveryHelper(autoReturnResource bool, resources map[schema.GroupVersionResource]schema.GroupVersionResource) *FakeDiscoveryHelper { @@ -54,6 +55,14 @@ func NewFakeDiscoveryHelper(autoReturnResource bool, resources map[schema.GroupV } apiResourceMap[gvString] = append(apiResourceMap[gvString], metav1.APIResource{Name: gvr.Resource}) + helper.APIGroupsList = append(helper.APIGroupsList, + metav1.APIGroup{ + Name: gvr.Group, + PreferredVersion: metav1.GroupVersionForDiscovery{ + GroupVersion: gvString, + Version: gvr.Version, + }, + }) } for group, resources := range apiResourceMap { @@ -110,3 +119,7 @@ func (dh *FakeDiscoveryHelper) ResourceFor(input schema.GroupVersionResource) (s return schema.GroupVersionResource{}, metav1.APIResource{}, errors.New("APIResource not found") } + +func (dh *FakeDiscoveryHelper) APIGroups() []metav1.APIGroup { + return dh.APIGroupsList +}