diff --git a/pkg/backup/service_account_action.go b/pkg/backup/service_account_action.go new file mode 100644 index 000000000..17e147ff4 --- /dev/null +++ b/pkg/backup/service_account_action.go @@ -0,0 +1,113 @@ +/* +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" + "github.com/sirupsen/logrus" + + rbac "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + rbacclient "k8s.io/client-go/kubernetes/typed/rbac/v1" + + "github.com/heptio/ark/pkg/apis/ark/v1" +) + +// serviceAccountAction implements ItemAction. +type serviceAccountAction struct { + log logrus.FieldLogger + clusterRoleBindings []rbac.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{}) + if err != nil { + return nil, errors.WithStack(err) + } + + return &serviceAccountAction{ + log: log, + clusterRoleBindings: clusterRoleBindings.Items, + }, nil +} + +// AppliesTo returns a ResourceSelector that applies only to service accounts. +func (a *serviceAccountAction) AppliesTo() (ResourceSelector, error) { + return ResourceSelector{ + IncludedResources: []string{"serviceaccounts"}, + }, nil +} + +var ( + crbGroupResource = schema.GroupResource{Group: "rbac.authorization.k8s.io", Resource: "clusterrolebindings"} + crGroupResource = schema.GroupResource{Group: "rbac.authorization.k8s.io", Resource: "clusterroles"} +) + +// Execute checks for any ClusterRoleBindings that have this service account as a subject, and +// adds the ClusterRoleBinding and associated ClusterRole to the list of additional items to +// be backed up. +func (a *serviceAccountAction) Execute(item runtime.Unstructured, backup *v1.Backup) (runtime.Unstructured, []ResourceIdentifier, error) { + a.log.Info("Running serviceAccountAction") + defer a.log.Info("Done running serviceAccountAction") + + objectMeta, err := meta.Accessor(item) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + var ( + namespace = objectMeta.GetNamespace() + name = objectMeta.GetName() + bindings = sets.NewString() + roles = sets.NewString() + ) + + for _, clusterRoleBinding := range a.clusterRoleBindings { + for _, subj := range clusterRoleBinding.Subjects { + if subj.Kind == rbac.ServiceAccountKind && subj.Namespace == namespace && subj.Name == 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) + + bindings.Insert(clusterRoleBinding.Name) + roles.Insert(clusterRoleBinding.RoleRef.Name) + break + } + } + } + + var additionalItems []ResourceIdentifier + for binding := range bindings { + additionalItems = append(additionalItems, ResourceIdentifier{ + GroupResource: crbGroupResource, + Name: binding, + }) + } + + for role := range roles { + additionalItems = append(additionalItems, ResourceIdentifier{ + GroupResource: crGroupResource, + Name: role, + }) + } + + return item, additionalItems, nil +} diff --git a/pkg/backup/service_account_action_test.go b/pkg/backup/service_account_action_test.go new file mode 100644 index 000000000..3319f5598 --- /dev/null +++ b/pkg/backup/service_account_action_test.go @@ -0,0 +1,266 @@ +/* +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 ( + "fmt" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + rbacclient "k8s.io/client-go/kubernetes/typed/rbac/v1" + + arktest "github.com/heptio/ark/pkg/util/test" +) + +type fakeClusterRoleBindingClient struct { + clusterRoleBindings []v1.ClusterRoleBinding + + rbacclient.ClusterRoleBindingInterface +} + +func (c *fakeClusterRoleBindingClient) List(opts metav1.ListOptions) (*v1.ClusterRoleBindingList, error) { + return &v1.ClusterRoleBindingList{ + Items: c.clusterRoleBindings, + }, nil +} + +func TestServiceAccountActionAppliesTo(t *testing.T) { + a, _ := NewServiceAccountAction(arktest.NewLogger(), &fakeClusterRoleBindingClient{}) + + actual, err := a.AppliesTo() + require.NoError(t, err) + + expected := ResourceSelector{ + IncludedResources: []string{"serviceaccounts"}, + } + assert.Equal(t, expected, actual) +} + +func TestServiceAccountActionExecute(t *testing.T) { + tests := []struct { + name string + serviceAccount runtime.Unstructured + crbs []v1.ClusterRoleBinding + expectedAdditionalItems []ResourceIdentifier + }{ + { + name: "no crbs", + serviceAccount: unstructuredOrDie(` + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "namespace": "heptio-ark", + "name": "ark" + } + } + `), + crbs: nil, + expectedAdditionalItems: nil, + }, + { + name: "no matching crbs", + serviceAccount: unstructuredOrDie(` + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "namespace": "heptio-ark", + "name": "ark" + } + } + `), + crbs: []v1.ClusterRoleBinding{ + { + Subjects: []v1.Subject{ + { + Kind: "non-matching-kind", + Namespace: "non-matching-ns", + Name: "non-matching-name", + }, + { + Kind: "non-matching-kind", + Namespace: "heptio-ark", + Name: "ark", + }, + { + Kind: v1.ServiceAccountKind, + Namespace: "non-matching-ns", + Name: "ark", + }, + { + Kind: v1.ServiceAccountKind, + Namespace: "heptio-ark", + Name: "non-matching-name", + }, + }, + RoleRef: v1.RoleRef{ + Name: "role", + }, + }, + }, + expectedAdditionalItems: nil, + }, + { + name: "some matching crbs", + serviceAccount: unstructuredOrDie(` + { + "apiVersion": "v1", + "kind": "ServiceAccount", + "metadata": { + "namespace": "heptio-ark", + "name": "ark" + } + } + `), + crbs: []v1.ClusterRoleBinding{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "crb-1", + }, + Subjects: []v1.Subject{ + { + Kind: "non-matching-kind", + Namespace: "non-matching-ns", + Name: "non-matching-name", + }, + }, + RoleRef: v1.RoleRef{ + Name: "role-1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "crb-2", + }, + Subjects: []v1.Subject{ + { + Kind: "non-matching-kind", + Namespace: "non-matching-ns", + Name: "non-matching-name", + }, + { + Kind: v1.ServiceAccountKind, + Namespace: "heptio-ark", + Name: "ark", + }, + }, + RoleRef: v1.RoleRef{ + Name: "role-2", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "crb-3", + }, + Subjects: []v1.Subject{ + { + Kind: v1.ServiceAccountKind, + Namespace: "heptio-ark", + Name: "ark", + }, + }, + RoleRef: v1.RoleRef{ + Name: "role-3", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "crb-4", + }, + Subjects: []v1.Subject{ + { + Kind: v1.ServiceAccountKind, + Namespace: "heptio-ark", + Name: "ark", + }, + { + Kind: "non-matching-kind", + Namespace: "non-matching-ns", + Name: "non-matching-name", + }, + }, + RoleRef: v1.RoleRef{ + Name: "role-4", + }, + }, + }, + expectedAdditionalItems: []ResourceIdentifier{ + { + GroupResource: crbGroupResource, + Name: "crb-2", + }, + { + GroupResource: crbGroupResource, + Name: "crb-3", + }, + { + GroupResource: crbGroupResource, + Name: "crb-4", + }, + { + GroupResource: crGroupResource, + Name: "role-2", + }, + { + GroupResource: crGroupResource, + Name: "role-3", + }, + { + GroupResource: crGroupResource, + Name: "role-4", + }, + }, + }, + } + + 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) + + 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) + }) + } + +} diff --git a/pkg/cmd/ark/ark.go b/pkg/cmd/ark/ark.go index 8db5a60ca..2a8147773 100644 --- a/pkg/cmd/ark/ark.go +++ b/pkg/cmd/ark/ark.go @@ -62,7 +62,7 @@ operations can also be performed as 'ark backup get' and 'ark schedule create'.` get.NewCommand(f), describe.NewCommand(f), create.NewCommand(f), - runplugin.NewCommand(), + runplugin.NewCommand(f), plugin.NewCommand(f), delete.NewCommand(f), cliclient.NewCommand(), diff --git a/pkg/cmd/server/plugin/plugin.go b/pkg/cmd/server/plugin/plugin.go index ec575bdd8..1c8feb469 100644 --- a/pkg/cmd/server/plugin/plugin.go +++ b/pkg/cmd/server/plugin/plugin.go @@ -22,40 +22,19 @@ import ( "github.com/spf13/cobra" "github.com/heptio/ark/pkg/backup" + "github.com/heptio/ark/pkg/client" "github.com/heptio/ark/pkg/cloudprovider" "github.com/heptio/ark/pkg/cloudprovider/aws" "github.com/heptio/ark/pkg/cloudprovider/azure" "github.com/heptio/ark/pkg/cloudprovider/gcp" + "github.com/heptio/ark/pkg/cmd" arkplugin "github.com/heptio/ark/pkg/plugin" "github.com/heptio/ark/pkg/restore" ) -func NewCommand() *cobra.Command { +func NewCommand(f client.Factory) *cobra.Command { logger := arkplugin.NewLogger() - objectStores := map[string]cloudprovider.ObjectStore{ - "aws": aws.NewObjectStore(), - "gcp": gcp.NewObjectStore(), - "azure": azure.NewObjectStore(), - } - - blockStores := map[string]cloudprovider.BlockStore{ - "aws": aws.NewBlockStore(), - "gcp": gcp.NewBlockStore(logger), - "azure": azure.NewBlockStore(), - } - - backupItemActions := map[string]backup.ItemAction{ - "pv": backup.NewBackupPVAction(logger), - "pod": backup.NewPodAction(logger), - } - - restoreItemActions := map[string]restore.ItemAction{ - "job": restore.NewJobAction(logger), - "pod": restore.NewPodAction(logger), - "svc": restore.NewServiceAction(logger), - } - c := &cobra.Command{ Use: "run-plugin [KIND] [NAME]", Hidden: true, @@ -72,18 +51,24 @@ func NewCommand() *cobra.Command { GRPCServer: plugin.DefaultGRPCServer, } - logger.Debugf("Executing run-plugin command") + logger.Debug("Executing run-plugin command") switch kind { case "cloudprovider": - objectStore, found := objectStores[name] - if !found { - logger.Fatalf("Unrecognized plugin name") - } + var ( + objectStore cloudprovider.ObjectStore + blockStore cloudprovider.BlockStore + ) - blockStore, found := blockStores[name] - if !found { - logger.Fatalf("Unrecognized plugin name") + switch name { + case "aws": + objectStore, blockStore = aws.NewObjectStore(), aws.NewBlockStore() + case "azure": + objectStore, blockStore = azure.NewObjectStore(), azure.NewBlockStore() + case "gcp": + objectStore, blockStore = gcp.NewObjectStore(), gcp.NewBlockStore(logger) + default: + logger.Fatal("Unrecognized plugin name") } serveConfig.Plugins = map[string]plugin.Plugin{ @@ -91,25 +76,45 @@ func NewCommand() *cobra.Command { string(arkplugin.PluginKindBlockStore): arkplugin.NewBlockStorePlugin(blockStore), } case arkplugin.PluginKindBackupItemAction.String(): - action, found := backupItemActions[name] - if !found { - logger.Fatalf("Unrecognized plugin name") + var action backup.ItemAction + + switch name { + case "pv": + action = backup.NewBackupPVAction(logger) + case "pod": + action = backup.NewPodAction(logger) + case "serviceaccount": + clientset, err := f.KubeClient() + cmd.CheckError(err) + + action, err = backup.NewServiceAccountAction(logger, clientset.RbacV1().ClusterRoleBindings()) + cmd.CheckError(err) + default: + logger.Fatal("Unrecognized plugin name") } serveConfig.Plugins = map[string]plugin.Plugin{ kind: arkplugin.NewBackupItemActionPlugin(action), } case arkplugin.PluginKindRestoreItemAction.String(): - action, found := restoreItemActions[name] - if !found { - logger.Fatalf("Unrecognized plugin name") + var action restore.ItemAction + + switch name { + case "job": + action = restore.NewJobAction(logger) + case "pod": + action = restore.NewPodAction(logger) + case "svc": + action = restore.NewServiceAction(logger) + default: + logger.Fatal("Unrecognized plugin name") } serveConfig.Plugins = map[string]plugin.Plugin{ kind: arkplugin.NewRestoreItemActionPlugin(action), } default: - logger.Fatalf("Unsupported plugin kind") + logger.Fatal("Unsupported plugin kind") } plugin.Serve(serveConfig) diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go index 7bd6a637a..3136bb9ed 100644 --- a/pkg/plugin/manager.go +++ b/pkg/plugin/manager.go @@ -184,6 +184,7 @@ func (m *manager) registerPlugins() error { } m.pluginRegistry.register("pv", arkCommand, []string{"run-plugin", string(PluginKindBackupItemAction), "pv"}, PluginKindBackupItemAction) m.pluginRegistry.register("backup-pod", arkCommand, []string{"run-plugin", string(PluginKindBackupItemAction), "pod"}, PluginKindBackupItemAction) + m.pluginRegistry.register("serviceaccount", arkCommand, []string{"run-plugin", string(PluginKindBackupItemAction), "serviceaccount"}, PluginKindBackupItemAction) m.pluginRegistry.register("job", arkCommand, []string{"run-plugin", string(PluginKindRestoreItemAction), "job"}, PluginKindRestoreItemAction) m.pluginRegistry.register("restore-pod", arkCommand, []string{"run-plugin", string(PluginKindRestoreItemAction), "pod"}, PluginKindRestoreItemAction) diff --git a/third_party/kubernetes/pkg/kubectl/cmd/completion.go b/third_party/kubernetes/pkg/kubectl/cmd/completion.go index 9d8ccd746..731e6542c 100644 --- a/third_party/kubernetes/pkg/kubectl/cmd/completion.go +++ b/third_party/kubernetes/pkg/kubectl/cmd/completion.go @@ -21,8 +21,9 @@ package cmd import ( "bytes" - "github.com/spf13/cobra" "io" + + "github.com/spf13/cobra" ) func GenZshCompletion(out io.Writer, ark *cobra.Command) {