From 2cf1758a0c082a8c5a234ed2eb26f24a90d2ef84 Mon Sep 17 00:00:00 2001 From: Shlok Chaudhari Date: Thu, 1 Aug 2024 21:06:34 -0500 Subject: [PATCH] Adding "./kubestr browse snapshot" command (#277) * Adding the kubestr browse pvc command. Handling kubestr browse support for backward compatibility. * Adding browse snapshot command. Updating browse command to browse pvc command. * chore(deps): bump github/codeql-action in the github-actions group (#272) Bumps the github-actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.25.12 to 3.25.13 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/4fa2a7953630fd2f3fb380f21be14ede0169dd4f...2d790406f505036ef40ecba973cc774a50395aac) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump docker/build-push-action in the docker group (#273) Bumps the docker group with 1 update: [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/build-push-action` from 6.3.0 to 6.4.1 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/1a162644f9a7e87d8f4b053101d1d9a712edc18c...1ca370b3a9802c92e886402e0dd88098a2533b12) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor dependency-group: docker ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Removing unused snapshot function parameter in cleanup * Adding mock tests for SnapshotBrowserStepper * Adding Deprecated msg to the 'browse' command * Adding fake tests for snapshot_inspector.go * Renamed testcase CSITestSuite.TestCreateInspectorApplication to TestCreateInspectorApplicationForPVC * Adding snapshot_inspector_steps_test.go * Updating Deprecated msg for 'browse' command * Making namespace, runAsUser & localport flags persistent * Removing namespace, runAsUser & localport flags for browse snapshot because we made those persistent * Removing storage class flag * Update cmd/rootCmd.go Co-authored-by: Sirish Bathina --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sirish Bathina --- cmd/rootCmd.go | 52 +- pkg/csi/csi_ops.go | 49 ++ pkg/csi/mocks/mock_argument_validator.go | 36 +- .../mocks/mock_snapshot_browser_stepper.go | 97 +++ pkg/csi/mocks/mock_snapshot_fetcher.go | 68 +++ pkg/csi/pvc_inspector_steps_test.go | 2 +- pkg/csi/snapshot_inspector.go | 239 ++++++++ pkg/csi/snapshot_inspector_steps_test.go | 566 ++++++++++++++++++ pkg/csi/snapshot_inspector_test.go | 192 ++++++ pkg/csi/types/csi_types.go | 26 + 10 files changed, 1314 insertions(+), 13 deletions(-) create mode 100644 pkg/csi/mocks/mock_snapshot_browser_stepper.go create mode 100644 pkg/csi/mocks/mock_snapshot_fetcher.go create mode 100644 pkg/csi/snapshot_inspector.go create mode 100644 pkg/csi/snapshot_inspector_steps_test.go create mode 100644 pkg/csi/snapshot_inspector_test.go diff --git a/cmd/rootCmd.go b/cmd/rootCmd.go index 4f36fd7..d53356e 100644 --- a/cmd/rootCmd.go +++ b/cmd/rootCmd.go @@ -110,6 +110,20 @@ var ( }, } + browseSnapshotCmd = &cobra.Command{ + Use: "snapshot [Snapshot name]", + Short: "Browse the contents of a CSI VolumeSnapshot via file browser", + Long: "Browse the contents of a CSI provisioned VolumeSnapshot by cloning the volume and mounting it with a file browser.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return CsiSnapshotBrowse(context.Background(), args[0], + namespace, + csiCheckRunAsUser, + browseLocalPort, + ) + }, + } + blockMountRunAsUser int64 blockMountCleanup bool blockMountCleanupOnly bool @@ -178,7 +192,7 @@ func init() { rootCmd.AddCommand(browseCmd) browseCmd.Flags().StringVarP(&csiCheckVolumeSnapshotClass, "volumesnapshotclass", "v", "", "The name of a VolumeSnapshotClass. (Required)") _ = browseCmd.MarkFlagRequired("volumesnapshotclass") - browseCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", fio.DefaultNS, "The namespace of the PersistentVolumeClaim.") + browseCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", fio.DefaultNS, "The namespace of the resource to browse.") browseCmd.PersistentFlags().Int64VarP(&csiCheckRunAsUser, "runAsUser", "u", 0, "Runs the inspector pod as a user (int)") browseCmd.PersistentFlags().IntVarP(&browseLocalPort, "localport", "l", 8080, "The local port to expose the inspector") @@ -186,8 +200,10 @@ func init() { browsePvcCmd.Flags().StringVarP(&csiCheckVolumeSnapshotClass, "volumesnapshotclass", "v", "", "The name of a VolumeSnapshotClass. (Required)") _ = browsePvcCmd.MarkFlagRequired("volumesnapshotclass") + browseCmd.AddCommand(browseSnapshotCmd) + rootCmd.AddCommand(blockMountCmd) - blockMountCmd.Flags().StringVarP(&storageClass, "storageclass", "s", "", "The name of a Storageclass. (Required)") + blockMountCmd.Flags().StringVarP(&storageClass, "storageclass", "s", "", "The name of a StorageClass. (Required)") _ = blockMountCmd.MarkFlagRequired("storageclass") blockMountCmd.Flags().StringVarP(&namespace, "namespace", "n", fio.DefaultNS, "The namespace used to run the check.") blockMountCmd.Flags().StringVarP(&containerImage, "image", "i", "", "The container image used to create a pod.") @@ -373,6 +389,38 @@ func CsiPvcBrowse(ctx context.Context, return err } +func CsiSnapshotBrowse(ctx context.Context, + snapshotName string, + namespace string, + runAsUser int64, + localPort int, +) error { + kubecli, err := kubestr.LoadKubeCli() + if err != nil { + fmt.Printf("Failed to load kubeCli (%s)", err.Error()) + return err + } + dyncli, err := kubestr.LoadDynCli() + if err != nil { + fmt.Printf("Failed to load dynCli (%s)", err.Error()) + return err + } + browseRunner := &csi.SnapshotBrowseRunner{ + KubeCli: kubecli, + DynCli: dyncli, + } + err = browseRunner.RunSnapshotBrowse(ctx, &csitypes.SnapshotBrowseArgs{ + SnapshotName: snapshotName, + Namespace: namespace, + RunAsUser: runAsUser, + LocalPort: localPort, + }) + if err != nil { + fmt.Printf("Failed to run Snapshot browser (%s)\n", err.Error()) + } + return err +} + func BlockMountCheck(ctx context.Context, output, outfile string, cleanupOnly bool, checkerArgs block.BlockMountCheckerArgs) error { kubecli, err := kubestr.LoadKubeCli() if err != nil { diff --git a/pkg/csi/csi_ops.go b/pkg/csi/csi_ops.go index 7316731..280a8d2 100644 --- a/pkg/csi/csi_ops.go +++ b/pkg/csi/csi_ops.go @@ -5,6 +5,8 @@ package csi import ( "context" "fmt" + "k8s.io/apimachinery/pkg/runtime" + "log" "net/http" "net/url" "strings" @@ -44,6 +46,7 @@ type ArgumentValidator interface { //Rename ValidatePVC(ctx context.Context, pvcName, namespace string) (*v1.PersistentVolumeClaim, error) FetchPV(ctx context.Context, pvName string) (*v1.PersistentVolume, error) + ValidateVolumeSnapshot(ctx context.Context, snapshotName, namespace string, groupVersion *metav1.GroupVersionForDiscovery) (*snapv1.VolumeSnapshot, error) ValidateNamespace(ctx context.Context, namespace string) error ValidateStorageClass(ctx context.Context, storageClass string) (*sv1.StorageClass, error) ValidateVolumeSnapshotClass(ctx context.Context, volumeSnapshotClass string, groupVersion *metav1.GroupVersionForDiscovery) (*unstructured.Unstructured, error) @@ -68,6 +71,17 @@ func (o *validateOperations) ValidatePVC(ctx context.Context, pvcName, namespace return o.kubeCli.CoreV1().PersistentVolumeClaims(namespace).Get(ctx, pvcName, metav1.GetOptions{}) } +func (o *validateOperations) ValidateVolumeSnapshot(ctx context.Context, snapshotName, namespace string, groupVersion *metav1.GroupVersionForDiscovery) (*snapv1.VolumeSnapshot, error) { + VolSnapGVR := schema.GroupVersionResource{Group: snapv1.GroupName, Version: groupVersion.Version, Resource: common.VolumeSnapshotResourcePlural} + uVS, err := o.dynCli.Resource(VolSnapGVR).Namespace(namespace).Get(ctx, snapshotName, metav1.GetOptions{}) + if err != nil { + log.Fatalf("Failed to get VolumeSnapshot: %v", err) + } + volumeSnapshot := &snapv1.VolumeSnapshot{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(uVS.UnstructuredContent(), volumeSnapshot) + return volumeSnapshot, err +} + func (o *validateOperations) FetchPV(ctx context.Context, pvName string) (*v1.PersistentVolume, error) { if o.kubeCli == nil { return nil, fmt.Errorf("kubeCli not initialized") @@ -317,6 +331,41 @@ func (c *applicationCreate) getErrorFromEvents(ctx context.Context, namespace, n return nil } +//go:generate go run github.com/golang/mock/mockgen -destination=mocks/mock_snapshot_fetcher.go -package=mocks . SnapshotFetcher +type SnapshotFetcher interface { + NewSnapshotter() (kansnapshot.Snapshotter, error) + GetVolumeSnapshot(ctx context.Context, snapshotter kansnapshot.Snapshotter, args *types.FetchSnapshotArgs) (*snapv1.VolumeSnapshot, error) +} + +type snapshotFetch struct { + kubeCli kubernetes.Interface + dynCli dynamic.Interface +} + +func (f *snapshotFetch) NewSnapshotter() (kansnapshot.Snapshotter, error) { + if f.kubeCli == nil { + return nil, fmt.Errorf("kubeCli not initialized") + } + if f.dynCli == nil { + return nil, fmt.Errorf("dynCli not initialized") + } + return kansnapshot.NewSnapshotter(f.kubeCli, f.dynCli) +} + +func (f *snapshotFetch) GetVolumeSnapshot(ctx context.Context, snapshotter kansnapshot.Snapshotter, args *types.FetchSnapshotArgs) (*snapv1.VolumeSnapshot, error) { + if snapshotter == nil || args == nil { + return nil, fmt.Errorf("snapshotter or args are empty") + } + if err := args.Validate(); err != nil { + return nil, err + } + snap, err := snapshotter.Get(ctx, args.SnapshotName, args.Namespace) + if err != nil { + return nil, errors.Wrapf(err, "Failed to get CSI snapshot (%s) in Namespace (%s)", args.SnapshotName, args.Namespace) + } + return snap, nil +} + //go:generate go run github.com/golang/mock/mockgen -destination=mocks/mock_snapshot_creator.go -package=mocks . SnapshotCreator type SnapshotCreator interface { NewSnapshotter() (kansnapshot.Snapshotter, error) diff --git a/pkg/csi/mocks/mock_argument_validator.go b/pkg/csi/mocks/mock_argument_validator.go index 22553a0..86af720 100644 --- a/pkg/csi/mocks/mock_argument_validator.go +++ b/pkg/csi/mocks/mock_argument_validator.go @@ -9,9 +9,10 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - v1 "k8s.io/api/core/v1" - v10 "k8s.io/api/storage/v1" - v11 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + v10 "k8s.io/api/core/v1" + v11 "k8s.io/api/storage/v1" + v12 "k8s.io/apimachinery/pkg/apis/meta/v1" unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -39,10 +40,10 @@ func (m *MockArgumentValidator) EXPECT() *MockArgumentValidatorMockRecorder { } // FetchPV mocks base method. -func (m *MockArgumentValidator) FetchPV(arg0 context.Context, arg1 string) (*v1.PersistentVolume, error) { +func (m *MockArgumentValidator) FetchPV(arg0 context.Context, arg1 string) (*v10.PersistentVolume, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FetchPV", arg0, arg1) - ret0, _ := ret[0].(*v1.PersistentVolume) + ret0, _ := ret[0].(*v10.PersistentVolume) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -68,10 +69,10 @@ func (mr *MockArgumentValidatorMockRecorder) ValidateNamespace(arg0, arg1 interf } // ValidatePVC mocks base method. -func (m *MockArgumentValidator) ValidatePVC(arg0 context.Context, arg1, arg2 string) (*v1.PersistentVolumeClaim, error) { +func (m *MockArgumentValidator) ValidatePVC(arg0 context.Context, arg1, arg2 string) (*v10.PersistentVolumeClaim, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidatePVC", arg0, arg1, arg2) - ret0, _ := ret[0].(*v1.PersistentVolumeClaim) + ret0, _ := ret[0].(*v10.PersistentVolumeClaim) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -83,10 +84,10 @@ func (mr *MockArgumentValidatorMockRecorder) ValidatePVC(arg0, arg1, arg2 interf } // ValidateStorageClass mocks base method. -func (m *MockArgumentValidator) ValidateStorageClass(arg0 context.Context, arg1 string) (*v10.StorageClass, error) { +func (m *MockArgumentValidator) ValidateStorageClass(arg0 context.Context, arg1 string) (*v11.StorageClass, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidateStorageClass", arg0, arg1) - ret0, _ := ret[0].(*v10.StorageClass) + ret0, _ := ret[0].(*v11.StorageClass) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -97,8 +98,23 @@ func (mr *MockArgumentValidatorMockRecorder) ValidateStorageClass(arg0, arg1 int return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateStorageClass", reflect.TypeOf((*MockArgumentValidator)(nil).ValidateStorageClass), arg0, arg1) } +// ValidateVolumeSnapshot mocks base method. +func (m *MockArgumentValidator) ValidateVolumeSnapshot(arg0 context.Context, arg1, arg2 string, arg3 *v12.GroupVersionForDiscovery) (*v1.VolumeSnapshot, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateVolumeSnapshot", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*v1.VolumeSnapshot) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateVolumeSnapshot indicates an expected call of ValidateVolumeSnapshot. +func (mr *MockArgumentValidatorMockRecorder) ValidateVolumeSnapshot(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateVolumeSnapshot", reflect.TypeOf((*MockArgumentValidator)(nil).ValidateVolumeSnapshot), arg0, arg1, arg2, arg3) +} + // ValidateVolumeSnapshotClass mocks base method. -func (m *MockArgumentValidator) ValidateVolumeSnapshotClass(arg0 context.Context, arg1 string, arg2 *v11.GroupVersionForDiscovery) (*unstructured.Unstructured, error) { +func (m *MockArgumentValidator) ValidateVolumeSnapshotClass(arg0 context.Context, arg1 string, arg2 *v12.GroupVersionForDiscovery) (*unstructured.Unstructured, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ValidateVolumeSnapshotClass", arg0, arg1, arg2) ret0, _ := ret[0].(*unstructured.Unstructured) diff --git a/pkg/csi/mocks/mock_snapshot_browser_stepper.go b/pkg/csi/mocks/mock_snapshot_browser_stepper.go new file mode 100644 index 0000000..4c5bb02 --- /dev/null +++ b/pkg/csi/mocks/mock_snapshot_browser_stepper.go @@ -0,0 +1,97 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/kastenhq/kubestr/pkg/csi (interfaces: SnapshotBrowserStepper) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + types "github.com/kastenhq/kubestr/pkg/csi/types" + v1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + v10 "k8s.io/api/core/v1" + v11 "k8s.io/api/storage/v1" +) + +// MockSnapshotBrowserStepper is a mock of SnapshotBrowserStepper interface. +type MockSnapshotBrowserStepper struct { + ctrl *gomock.Controller + recorder *MockSnapshotBrowserStepperMockRecorder +} + +// MockSnapshotBrowserStepperMockRecorder is the mock recorder for MockSnapshotBrowserStepper. +type MockSnapshotBrowserStepperMockRecorder struct { + mock *MockSnapshotBrowserStepper +} + +// NewMockSnapshotBrowserStepper creates a new mock instance. +func NewMockSnapshotBrowserStepper(ctrl *gomock.Controller) *MockSnapshotBrowserStepper { + mock := &MockSnapshotBrowserStepper{ctrl: ctrl} + mock.recorder = &MockSnapshotBrowserStepperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSnapshotBrowserStepper) EXPECT() *MockSnapshotBrowserStepperMockRecorder { + return m.recorder +} + +// Cleanup mocks base method. +func (m *MockSnapshotBrowserStepper) Cleanup(arg0 context.Context, arg1 *v10.PersistentVolumeClaim, arg2 *v10.Pod) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Cleanup", arg0, arg1, arg2) +} + +// Cleanup indicates an expected call of Cleanup. +func (mr *MockSnapshotBrowserStepperMockRecorder) Cleanup(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cleanup", reflect.TypeOf((*MockSnapshotBrowserStepper)(nil).Cleanup), arg0, arg1, arg2) +} + +// CreateInspectorApplication mocks base method. +func (m *MockSnapshotBrowserStepper) CreateInspectorApplication(arg0 context.Context, arg1 *types.SnapshotBrowseArgs, arg2 *v1.VolumeSnapshot, arg3 *v11.StorageClass) (*v10.Pod, *v10.PersistentVolumeClaim, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateInspectorApplication", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*v10.Pod) + ret1, _ := ret[1].(*v10.PersistentVolumeClaim) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// CreateInspectorApplication indicates an expected call of CreateInspectorApplication. +func (mr *MockSnapshotBrowserStepperMockRecorder) CreateInspectorApplication(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateInspectorApplication", reflect.TypeOf((*MockSnapshotBrowserStepper)(nil).CreateInspectorApplication), arg0, arg1, arg2, arg3) +} + +// PortForwardAPod mocks base method. +func (m *MockSnapshotBrowserStepper) PortForwardAPod(arg0 context.Context, arg1 *v10.Pod, arg2 int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PortForwardAPod", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// PortForwardAPod indicates an expected call of PortForwardAPod. +func (mr *MockSnapshotBrowserStepperMockRecorder) PortForwardAPod(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PortForwardAPod", reflect.TypeOf((*MockSnapshotBrowserStepper)(nil).PortForwardAPod), arg0, arg1, arg2) +} + +// ValidateArgs mocks base method. +func (m *MockSnapshotBrowserStepper) ValidateArgs(arg0 context.Context, arg1 *types.SnapshotBrowseArgs) (*v1.VolumeSnapshot, *v11.StorageClass, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateArgs", arg0, arg1) + ret0, _ := ret[0].(*v1.VolumeSnapshot) + ret1, _ := ret[1].(*v11.StorageClass) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// ValidateArgs indicates an expected call of ValidateArgs. +func (mr *MockSnapshotBrowserStepperMockRecorder) ValidateArgs(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateArgs", reflect.TypeOf((*MockSnapshotBrowserStepper)(nil).ValidateArgs), arg0, arg1) +} diff --git a/pkg/csi/mocks/mock_snapshot_fetcher.go b/pkg/csi/mocks/mock_snapshot_fetcher.go new file mode 100644 index 0000000..8bd55e8 --- /dev/null +++ b/pkg/csi/mocks/mock_snapshot_fetcher.go @@ -0,0 +1,68 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/kastenhq/kubestr/pkg/csi (interfaces: SnapshotFetcher) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + snapshot "github.com/kanisterio/kanister/pkg/kube/snapshot" + types "github.com/kastenhq/kubestr/pkg/csi/types" + v1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" +) + +// MockSnapshotFetcher is a mock of SnapshotFetcher interface. +type MockSnapshotFetcher struct { + ctrl *gomock.Controller + recorder *MockSnapshotFetcherMockRecorder +} + +// MockSnapshotFetcherMockRecorder is the mock recorder for MockSnapshotFetcher. +type MockSnapshotFetcherMockRecorder struct { + mock *MockSnapshotFetcher +} + +// NewMockSnapshotFetcher creates a new mock instance. +func NewMockSnapshotFetcher(ctrl *gomock.Controller) *MockSnapshotFetcher { + mock := &MockSnapshotFetcher{ctrl: ctrl} + mock.recorder = &MockSnapshotFetcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSnapshotFetcher) EXPECT() *MockSnapshotFetcherMockRecorder { + return m.recorder +} + +// GetVolumeSnapshot mocks base method. +func (m *MockSnapshotFetcher) GetVolumeSnapshot(arg0 context.Context, arg1 snapshot.Snapshotter, arg2 *types.FetchSnapshotArgs) (*v1.VolumeSnapshot, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVolumeSnapshot", arg0, arg1, arg2) + ret0, _ := ret[0].(*v1.VolumeSnapshot) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetVolumeSnapshot indicates an expected call of GetVolumeSnapshot. +func (mr *MockSnapshotFetcherMockRecorder) GetVolumeSnapshot(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolumeSnapshot", reflect.TypeOf((*MockSnapshotFetcher)(nil).GetVolumeSnapshot), arg0, arg1, arg2) +} + +// NewSnapshotter mocks base method. +func (m *MockSnapshotFetcher) NewSnapshotter() (snapshot.Snapshotter, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewSnapshotter") + ret0, _ := ret[0].(snapshot.Snapshotter) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewSnapshotter indicates an expected call of NewSnapshotter. +func (mr *MockSnapshotFetcherMockRecorder) NewSnapshotter() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewSnapshotter", reflect.TypeOf((*MockSnapshotFetcher)(nil).NewSnapshotter)) +} diff --git a/pkg/csi/pvc_inspector_steps_test.go b/pkg/csi/pvc_inspector_steps_test.go index 8b53069..a78649c 100644 --- a/pkg/csi/pvc_inspector_steps_test.go +++ b/pkg/csi/pvc_inspector_steps_test.go @@ -486,7 +486,7 @@ func (s *CSITestSuite) TestPvcBrowseSnapshotPVC(c *C) { } } -func (s *CSITestSuite) TestCreateInspectorApplication(c *C) { +func (s *CSITestSuite) TestCreateInspectorApplicationForPVC(c *C) { ctx := context.Background() resourceQuantity := resource.MustParse("1Gi") snapshotAPIGroup := "snapshot.storage.k8s.io" diff --git a/pkg/csi/snapshot_inspector.go b/pkg/csi/snapshot_inspector.go new file mode 100644 index 0000000..9e2645a --- /dev/null +++ b/pkg/csi/snapshot_inspector.go @@ -0,0 +1,239 @@ +package csi + +import ( + "bytes" + "context" + "fmt" + "github.com/kastenhq/kubestr/pkg/csi/types" + snapv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + sv1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "os" + "os/signal" + "sync" + "syscall" +) + +type SnapshotBrowseRunner struct { + KubeCli kubernetes.Interface + DynCli dynamic.Interface + browserSteps SnapshotBrowserStepper + pvc *v1.PersistentVolumeClaim + pod *v1.Pod + snapshot *snapv1.VolumeSnapshot +} + +func (r *SnapshotBrowseRunner) RunSnapshotBrowse(ctx context.Context, args *types.SnapshotBrowseArgs) error { + r.browserSteps = &snapshotBrowserSteps{ + validateOps: &validateOperations{ + kubeCli: r.KubeCli, + dynCli: r.DynCli, + }, + versionFetchOps: &apiVersionFetch{ + kubeCli: r.KubeCli, + }, + createAppOps: &applicationCreate{ + kubeCli: r.KubeCli, + }, + snapshotFetchOps: &snapshotFetch{ + kubeCli: r.KubeCli, + dynCli: r.DynCli, + }, + portForwardOps: &portforward{}, + cleanerOps: &cleanse{ + kubeCli: r.KubeCli, + dynCli: r.DynCli, + }, + } + return r.RunSnapshotBrowseHelper(ctx, args) +} + +func (r *SnapshotBrowseRunner) RunSnapshotBrowseHelper(ctx context.Context, args *types.SnapshotBrowseArgs) error { + defer func() { + fmt.Println("Cleaning up resources.") + r.browserSteps.Cleanup(ctx, r.pvc, r.pod) + }() + + if r.KubeCli == nil || r.DynCli == nil { + return fmt.Errorf("cli uninitialized") + } + + fmt.Println("Fetching the snapshot.") + vs, sc, err := r.browserSteps.ValidateArgs(ctx, args) + if err != nil { + return errors.Wrap(err, "Failed to validate arguments.") + } + r.snapshot = vs + + fmt.Println("Creating the file browser application.") + r.pod, r.pvc, err = r.browserSteps.CreateInspectorApplication(ctx, args, r.snapshot, sc) + if err != nil { + return errors.Wrap(err, "Failed to create inspector application.") + } + + fmt.Println("Forwarding the port.") + err = r.browserSteps.PortForwardAPod(ctx, r.pod, args.LocalPort) + if err != nil { + return errors.Wrap(err, "Failed to port forward Pod.") + } + + return nil +} + +//go:generate go run github.com/golang/mock/mockgen -destination=mocks/mock_snapshot_browser_stepper.go -package=mocks . SnapshotBrowserStepper +type SnapshotBrowserStepper interface { + ValidateArgs(ctx context.Context, args *types.SnapshotBrowseArgs) (*snapv1.VolumeSnapshot, *sv1.StorageClass, error) + CreateInspectorApplication(ctx context.Context, args *types.SnapshotBrowseArgs, snapshot *snapv1.VolumeSnapshot, storageClass *sv1.StorageClass) (*v1.Pod, *v1.PersistentVolumeClaim, error) + PortForwardAPod(ctx context.Context, pod *v1.Pod, localPort int) error + Cleanup(ctx context.Context, pvc *v1.PersistentVolumeClaim, pod *v1.Pod) +} + +type snapshotBrowserSteps struct { + validateOps ArgumentValidator + versionFetchOps ApiVersionFetcher + snapshotFetchOps SnapshotFetcher + createAppOps ApplicationCreator + portForwardOps PortForwarder + cleanerOps Cleaner + SnapshotGroupVersion *metav1.GroupVersionForDiscovery +} + +func (s *snapshotBrowserSteps) ValidateArgs(ctx context.Context, args *types.SnapshotBrowseArgs) (*snapv1.VolumeSnapshot, *sv1.StorageClass, error) { + if err := args.Validate(); err != nil { + return nil, nil, errors.Wrap(err, "Failed to validate input arguments") + } + if err := s.validateOps.ValidateNamespace(ctx, args.Namespace); err != nil { + return nil, nil, errors.Wrap(err, "Failed to validate Namespace") + } + groupVersion, err := s.versionFetchOps.GetCSISnapshotGroupVersion() + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to fetch groupVersion") + } + s.SnapshotGroupVersion = groupVersion + snapshot, err := s.validateOps.ValidateVolumeSnapshot(ctx, args.SnapshotName, args.Namespace, groupVersion) + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to validate VolumeSnapshot") + } + pvc, err := s.validateOps.ValidatePVC(ctx, *snapshot.Spec.Source.PersistentVolumeClaimName, args.Namespace) + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to validate source PVC") + } + sc, err := s.validateOps.ValidateStorageClass(ctx, *pvc.Spec.StorageClassName) + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to validate SC") + } + uVSC, err := s.validateOps.ValidateVolumeSnapshotClass(ctx, *snapshot.Spec.VolumeSnapshotClassName, groupVersion) + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to validate VolumeSnapshotClass") + } + vscDriver := getDriverNameFromUVSC(*uVSC, groupVersion.GroupVersion) + if sc.Provisioner != vscDriver { + return nil, nil, fmt.Errorf("StorageClass provisioner (%s) and VolumeSnapshotClass driver (%s) are different.", sc.Provisioner, vscDriver) + } + return snapshot, sc, nil +} + +func (s *snapshotBrowserSteps) CreateInspectorApplication(ctx context.Context, args *types.SnapshotBrowseArgs, snapshot *snapv1.VolumeSnapshot, storageClass *sv1.StorageClass) (*v1.Pod, *v1.PersistentVolumeClaim, error) { + snapshotAPIGroup := "snapshot.storage.k8s.io" + snapshotKind := "VolumeSnapshot" + dataSource := &v1.TypedLocalObjectReference{ + APIGroup: &snapshotAPIGroup, + Kind: snapshotKind, + Name: snapshot.Name, + } + pvcArgs := &types.CreatePVCArgs{ + GenerateName: clonedPVCGenerateName, + StorageClass: storageClass.Name, + Namespace: args.Namespace, + DataSource: dataSource, + RestoreSize: snapshot.Status.RestoreSize, + } + pvc, err := s.createAppOps.CreatePVC(ctx, pvcArgs) + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to restore PVC") + } + podArgs := &types.CreatePodArgs{ + GenerateName: clonedPodGenerateName, + PVCName: pvc.Name, + Namespace: args.Namespace, + RunAsUser: args.RunAsUser, + ContainerImage: "filebrowser/filebrowser:v2", + ContainerArgs: []string{"--noauth", "-r", "/data"}, + MountPath: "/data", + } + pod, err := s.createAppOps.CreatePod(ctx, podArgs) + if err != nil { + return nil, pvc, errors.Wrap(err, "Failed to create restored Pod") + } + if err = s.createAppOps.WaitForPodReady(ctx, args.Namespace, pod.Name); err != nil { + return pod, pvc, errors.Wrap(err, "Pod failed to become ready") + } + return pod, pvc, nil +} + +func (s *snapshotBrowserSteps) PortForwardAPod(ctx context.Context, pod *v1.Pod, localPort int) error { + var wg sync.WaitGroup + wg.Add(1) + stopChan, readyChan, errChan := make(chan struct{}, 1), make(chan struct{}, 1), make(chan string) + out, errOut := new(bytes.Buffer), new(bytes.Buffer) + cfg, err := s.portForwardOps.FetchRestConfig() + if err != nil { + return errors.New("Failed to fetch rest config") + } + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigs + fmt.Println("Stopping port forward") + close(stopChan) + wg.Done() + }() + + go func() { + pfArgs := &types.PortForwardAPodRequest{ + RestConfig: cfg, + Pod: pod, + LocalPort: localPort, + PodPort: 80, + OutStream: bytes.Buffer(*out), + ErrOutStream: bytes.Buffer(*errOut), + StopCh: stopChan, + ReadyCh: readyChan, + } + err = s.portForwardOps.PortForwardAPod(pfArgs) + if err != nil { + errChan <- fmt.Sprintf("Failed to port forward (%s)", err.Error()) + } + }() + + select { + case <-readyChan: + url := fmt.Sprintf("http://localhost:%d/", localPort) + fmt.Printf("Port forwarding is ready to get traffic. visit %s\n", url) + openbrowser(url) + wg.Wait() + case msg := <-errChan: + return errors.New(msg) + } + + return nil +} + +func (s *snapshotBrowserSteps) Cleanup(ctx context.Context, pvc *v1.PersistentVolumeClaim, pod *v1.Pod) { + if pvc != nil { + err := s.cleanerOps.DeletePVC(ctx, pvc.Name, pvc.Namespace) + if err != nil { + fmt.Println("Failed to delete PVC", pvc) + } + } + if pod != nil { + err := s.cleanerOps.DeletePod(ctx, pod.Name, pod.Namespace) + if err != nil { + fmt.Println("Failed to delete Pod", pod) + } + } +} diff --git a/pkg/csi/snapshot_inspector_steps_test.go b/pkg/csi/snapshot_inspector_steps_test.go new file mode 100644 index 0000000..1f6e767 --- /dev/null +++ b/pkg/csi/snapshot_inspector_steps_test.go @@ -0,0 +1,566 @@ +package csi + +import ( + "context" + "fmt" + + "github.com/golang/mock/gomock" + "github.com/kastenhq/kubestr/pkg/common" + "github.com/kastenhq/kubestr/pkg/csi/mocks" + "github.com/kastenhq/kubestr/pkg/csi/types" + snapv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + . "gopkg.in/check.v1" + v1 "k8s.io/api/core/v1" + sv1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func (s *CSITestSuite) TestSnapshotBrowseValidateArgs(c *C) { + ctx := context.Background() + scName := "sc" + vscName := "vsc" + pvcName := "pvc" + type fields struct { + validateOps *mocks.MockArgumentValidator + versionOps *mocks.MockApiVersionFetcher + } + for _, tc := range []struct { + args *types.SnapshotBrowseArgs + prepare func(f *fields) + errChecker Checker + }{ + { // valid args + args: &types.SnapshotBrowseArgs{ + SnapshotName: "vs", + Namespace: "ns", + }, + prepare: func(f *fields) { + gomock.InOrder( + f.validateOps.EXPECT().ValidateNamespace(gomock.Any(), "ns").Return(nil), + f.versionOps.EXPECT().GetCSISnapshotGroupVersion().Return( + &metav1.GroupVersionForDiscovery{ + GroupVersion: common.SnapshotAlphaVersion, + }, nil), + f.validateOps.EXPECT().ValidateVolumeSnapshot(gomock.Any(), "vs", "ns", gomock.Any()).Return( + &snapv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + Namespace: "ns", + }, + Spec: snapv1.VolumeSnapshotSpec{ + Source: snapv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: &pvcName, + }, + VolumeSnapshotClassName: &vscName, + }, + }, nil, + ), + f.validateOps.EXPECT().ValidatePVC(gomock.Any(), "pvc", "ns").Return( + &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + Namespace: "ns", + }, + Spec: v1.PersistentVolumeClaimSpec{ + VolumeName: "vol", + StorageClassName: &scName, + }, + }, nil, + ), + f.validateOps.EXPECT().ValidateStorageClass(gomock.Any(), scName).Return( + &sv1.StorageClass{ + Provisioner: "p1", + }, nil), + f.validateOps.EXPECT().ValidateVolumeSnapshotClass(gomock.Any(), "vsc", &metav1.GroupVersionForDiscovery{ + GroupVersion: common.SnapshotAlphaVersion, + }).Return(&unstructured.Unstructured{ + Object: map[string]interface{}{ + common.VolSnapClassAlphaDriverKey: "p1", + }, + }, nil), + ) + }, + errChecker: IsNil, + }, + { // driver mismatch + args: &types.SnapshotBrowseArgs{ + SnapshotName: "vs", + Namespace: "ns", + }, + prepare: func(f *fields) { + gomock.InOrder( + f.validateOps.EXPECT().ValidateNamespace(gomock.Any(), "ns").Return(nil), + f.versionOps.EXPECT().GetCSISnapshotGroupVersion().Return( + &metav1.GroupVersionForDiscovery{ + GroupVersion: common.SnapshotAlphaVersion, + }, nil), + f.validateOps.EXPECT().ValidateVolumeSnapshot(gomock.Any(), "vs", "ns", gomock.Any()).Return( + &snapv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + Namespace: "ns", + }, + Spec: snapv1.VolumeSnapshotSpec{ + Source: snapv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: &pvcName, + }, + VolumeSnapshotClassName: &vscName, + }, + }, nil, + ), + f.validateOps.EXPECT().ValidatePVC(gomock.Any(), "pvc", "ns").Return( + &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + Namespace: "ns", + }, + Spec: v1.PersistentVolumeClaimSpec{ + VolumeName: "vol", + StorageClassName: &scName, + }, + }, nil, + ), + f.validateOps.EXPECT().ValidateStorageClass(gomock.Any(), gomock.Any()).Return( + &sv1.StorageClass{ + Provisioner: "p1", + }, nil), + f.validateOps.EXPECT().ValidateVolumeSnapshotClass(gomock.Any(), "vsc", &metav1.GroupVersionForDiscovery{ + GroupVersion: common.SnapshotAlphaVersion, + }).Return(&unstructured.Unstructured{ + Object: map[string]interface{}{ + common.VolSnapClassAlphaDriverKey: "p2", + }, + }, nil), + ) + }, + errChecker: NotNil, + }, + { // vsc error + args: &types.SnapshotBrowseArgs{ + SnapshotName: "vs", + Namespace: "ns", + }, + prepare: func(f *fields) { + gomock.InOrder( + f.validateOps.EXPECT().ValidateNamespace(gomock.Any(), "ns").Return(nil), + f.versionOps.EXPECT().GetCSISnapshotGroupVersion().Return(nil, nil), + f.validateOps.EXPECT().ValidateVolumeSnapshot(gomock.Any(), "vs", "ns", gomock.Any()).Return( + &snapv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + Namespace: "ns", + }, + Spec: snapv1.VolumeSnapshotSpec{ + Source: snapv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: &pvcName, + }, + VolumeSnapshotClassName: &vscName, + }, + }, nil, + ), + f.validateOps.EXPECT().ValidatePVC(gomock.Any(), "pvc", "ns").Return( + &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + Namespace: "ns", + }, + Spec: v1.PersistentVolumeClaimSpec{ + VolumeName: "vol", + StorageClassName: &scName, + }, + }, nil, + ), + f.validateOps.EXPECT().ValidateStorageClass(gomock.Any(), gomock.Any()).Return(nil, nil), + f.validateOps.EXPECT().ValidateVolumeSnapshotClass(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("vsc error")), + ) + }, + errChecker: NotNil, + }, + { // get driver versionn error + args: &types.SnapshotBrowseArgs{ + SnapshotName: "vs", + Namespace: "ns", + }, + prepare: func(f *fields) { + gomock.InOrder( + f.validateOps.EXPECT().ValidateNamespace(gomock.Any(), "ns").Return(nil), + f.versionOps.EXPECT().GetCSISnapshotGroupVersion().Return(nil, fmt.Errorf("driver version error")), + ) + }, + errChecker: NotNil, + }, + { // sc error + args: &types.SnapshotBrowseArgs{ + SnapshotName: "vs", + Namespace: "ns", + }, + prepare: func(f *fields) { + gomock.InOrder( + f.validateOps.EXPECT().ValidateNamespace(gomock.Any(), "ns").Return(nil), + f.versionOps.EXPECT().GetCSISnapshotGroupVersion().Return(nil, nil), + f.validateOps.EXPECT().ValidateVolumeSnapshot(gomock.Any(), "vs", "ns", gomock.Any()).Return( + &snapv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + Namespace: "ns", + }, + Spec: snapv1.VolumeSnapshotSpec{ + Source: snapv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: &pvcName, + }, + VolumeSnapshotClassName: &vscName, + }, + }, nil, + ), + f.validateOps.EXPECT().ValidatePVC(gomock.Any(), "pvc", "ns").Return( + &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + Namespace: "ns", + }, + Spec: v1.PersistentVolumeClaimSpec{ + VolumeName: "vol", + StorageClassName: &scName, + }, + }, nil, + ), + f.validateOps.EXPECT().ValidateStorageClass(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("sc error")), + ) + }, + errChecker: NotNil, + }, + { // validate vs error + args: &types.SnapshotBrowseArgs{ + SnapshotName: "vs", + Namespace: "ns", + }, + prepare: func(f *fields) { + gomock.InOrder( + f.validateOps.EXPECT().ValidateNamespace(gomock.Any(), "ns").Return(nil), + f.versionOps.EXPECT().GetCSISnapshotGroupVersion().Return(nil, nil), + f.validateOps.EXPECT().ValidateVolumeSnapshot(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("validate vs error")), + ) + }, + errChecker: NotNil, + }, + { // validate ns error + args: &types.SnapshotBrowseArgs{ + SnapshotName: "vs", + Namespace: "ns", + }, + prepare: func(f *fields) { + gomock.InOrder( + f.validateOps.EXPECT().ValidateNamespace(gomock.Any(), "ns").Return(fmt.Errorf("validate ns error")), + ) + }, + errChecker: NotNil, + }, + { // validate vs error + args: &types.SnapshotBrowseArgs{ + SnapshotName: "", + Namespace: "ns", + }, + errChecker: NotNil, + }, + { // validate ns error + args: &types.SnapshotBrowseArgs{ + SnapshotName: "dfd", + Namespace: "", + }, + errChecker: NotNil, + }, + } { + ctrl := gomock.NewController(c) + defer ctrl.Finish() + f := fields{ + validateOps: mocks.NewMockArgumentValidator(ctrl), + versionOps: mocks.NewMockApiVersionFetcher(ctrl), + } + if tc.prepare != nil { + tc.prepare(&f) + } + stepper := &snapshotBrowserSteps{ + validateOps: f.validateOps, + versionFetchOps: f.versionOps, + } + _, _, err := stepper.ValidateArgs(ctx, tc.args) + c.Check(err, tc.errChecker) + } +} + +func (s *CSITestSuite) TestCreateInspectorApplicationForSnapshot(c *C) { + ctx := context.Background() + resourceQuantity := resource.MustParse("1Gi") + snapshotAPIGroup := "snapshot.storage.k8s.io" + type fields struct { + createAppOps *mocks.MockApplicationCreator + } + for _, tc := range []struct { + args *types.SnapshotBrowseArgs + snapshot *snapv1.VolumeSnapshot + sc *sv1.StorageClass + prepare func(f *fields) + errChecker Checker + podChecker Checker + pvcChecker Checker + }{ + { + args: &types.SnapshotBrowseArgs{ + Namespace: "ns", + RunAsUser: 100, + }, + sc: &sv1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sc", + }, + }, + snapshot: &snapv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + }, + Status: &snapv1.VolumeSnapshotStatus{ + RestoreSize: &resourceQuantity, + }, + }, + prepare: func(f *fields) { + gomock.InOrder( + f.createAppOps.EXPECT().CreatePVC(gomock.Any(), &types.CreatePVCArgs{ + GenerateName: clonedPVCGenerateName, + StorageClass: "sc", + Namespace: "ns", + DataSource: &v1.TypedLocalObjectReference{ + APIGroup: &snapshotAPIGroup, + Kind: "VolumeSnapshot", + Name: "vs", + }, + RestoreSize: &resourceQuantity, + }).Return(&v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + }, + }, nil), + f.createAppOps.EXPECT().CreatePod(gomock.Any(), &types.CreatePodArgs{ + GenerateName: clonedPodGenerateName, + PVCName: "pvc", + Namespace: "ns", + ContainerArgs: []string{"--noauth", "-r", "/data"}, + MountPath: "/data", + RunAsUser: 100, + ContainerImage: "filebrowser/filebrowser:v2", + }).Return(&v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + }, + }, nil), + f.createAppOps.EXPECT().WaitForPodReady(gomock.Any(), "ns", "pod").Return(nil), + ) + }, + errChecker: IsNil, + podChecker: NotNil, + pvcChecker: NotNil, + }, + { + args: &types.SnapshotBrowseArgs{ + Namespace: "ns", + RunAsUser: 100, + }, + sc: &sv1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sc", + }, + }, + snapshot: &snapv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + }, + Status: &snapv1.VolumeSnapshotStatus{ + RestoreSize: &resourceQuantity, + }, + }, + prepare: func(f *fields) { + gomock.InOrder( + f.createAppOps.EXPECT().CreatePVC(gomock.Any(), &types.CreatePVCArgs{ + GenerateName: clonedPVCGenerateName, + StorageClass: "sc", + Namespace: "ns", + DataSource: &v1.TypedLocalObjectReference{ + APIGroup: &snapshotAPIGroup, + Kind: "VolumeSnapshot", + Name: "vs", + }, + RestoreSize: &resourceQuantity, + }).Return(&v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + }, + }, nil), + f.createAppOps.EXPECT().CreatePod(gomock.Any(), &types.CreatePodArgs{ + GenerateName: clonedPodGenerateName, + PVCName: "pvc", + Namespace: "ns", + ContainerArgs: []string{"--noauth", "-r", "/data"}, + MountPath: "/data", + RunAsUser: 100, + ContainerImage: "filebrowser/filebrowser:v2", + }).Return(&v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + }, + }, nil), + f.createAppOps.EXPECT().WaitForPodReady(gomock.Any(), "ns", "pod").Return(fmt.Errorf("pod ready error")), + ) + }, + errChecker: NotNil, + podChecker: NotNil, + pvcChecker: NotNil, + }, + { + args: &types.SnapshotBrowseArgs{ + Namespace: "ns", + RunAsUser: 100, + }, + sc: &sv1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sc", + }, + }, + snapshot: &snapv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + }, + Status: &snapv1.VolumeSnapshotStatus{ + RestoreSize: &resourceQuantity, + }, + }, + prepare: func(f *fields) { + gomock.InOrder( + f.createAppOps.EXPECT().CreatePVC(gomock.Any(), gomock.Any()).Return(&v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + }, + }, nil), + f.createAppOps.EXPECT().CreatePod(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("pod error")), + ) + }, + errChecker: NotNil, + podChecker: IsNil, + pvcChecker: NotNil, + }, + { + args: &types.SnapshotBrowseArgs{ + Namespace: "ns", + RunAsUser: 100, + }, + sc: &sv1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sc", + }, + }, + snapshot: &snapv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vs", + }, + Status: &snapv1.VolumeSnapshotStatus{ + RestoreSize: &resourceQuantity, + }, + }, + prepare: func(f *fields) { + gomock.InOrder( + f.createAppOps.EXPECT().CreatePVC(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("error")), + ) + }, + errChecker: NotNil, + podChecker: IsNil, + pvcChecker: IsNil, + }, + } { + ctrl := gomock.NewController(c) + defer ctrl.Finish() + f := fields{ + createAppOps: mocks.NewMockApplicationCreator(ctrl), + } + if tc.prepare != nil { + tc.prepare(&f) + } + stepper := &snapshotBrowserSteps{ + createAppOps: f.createAppOps, + } + pod, pvc, err := stepper.CreateInspectorApplication(ctx, tc.args, tc.snapshot, tc.sc) + c.Check(err, tc.errChecker) + c.Check(pod, tc.podChecker) + c.Check(pvc, tc.pvcChecker) + } +} + +func (s *CSITestSuite) TestSnapshotBrowseCleanup(c *C) { + ctx := context.Background() + groupversion := &metav1.GroupVersionForDiscovery{ + GroupVersion: "gv", + Version: "v", + } + type fields struct { + cleanerOps *mocks.MockCleaner + } + for _, tc := range []struct { + pvc *v1.PersistentVolumeClaim + pod *v1.Pod + prepare func(f *fields) + }{ + { + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + Namespace: "ns", + }, + }, + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "ns", + }, + }, + prepare: func(f *fields) { + gomock.InOrder( + f.cleanerOps.EXPECT().DeletePVC(ctx, "pvc", "ns").Return(nil), + f.cleanerOps.EXPECT().DeletePod(ctx, "pod", "ns").Return(nil), + ) + }, + }, + { + pvc: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc", + Namespace: "ns", + }, + }, + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "ns", + }, + }, + prepare: func(f *fields) { + gomock.InOrder( + f.cleanerOps.EXPECT().DeletePVC(ctx, "pvc", "ns").Return(fmt.Errorf("err")), + f.cleanerOps.EXPECT().DeletePod(ctx, "pod", "ns").Return(fmt.Errorf("err")), + ) + }, + }, + } { + ctrl := gomock.NewController(c) + defer ctrl.Finish() + f := fields{ + cleanerOps: mocks.NewMockCleaner(ctrl), + } + if tc.prepare != nil { + tc.prepare(&f) + } + stepper := &snapshotBrowserSteps{ + cleanerOps: f.cleanerOps, + SnapshotGroupVersion: groupversion, + } + stepper.Cleanup(ctx, tc.pvc, tc.pod) + } +} diff --git a/pkg/csi/snapshot_inspector_test.go b/pkg/csi/snapshot_inspector_test.go new file mode 100644 index 0000000..22a0232 --- /dev/null +++ b/pkg/csi/snapshot_inspector_test.go @@ -0,0 +1,192 @@ +package csi + +import ( + "context" + "fmt" + + "github.com/golang/mock/gomock" + "github.com/kastenhq/kubestr/pkg/csi/mocks" + "github.com/kastenhq/kubestr/pkg/csi/types" + snapv1 "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1" + . "gopkg.in/check.v1" + v1 "k8s.io/api/core/v1" + sv1 "k8s.io/api/storage/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + fakedynamic "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +func (s *CSITestSuite) TestRunSnapshotBrowseHelper(c *C) { + ctx := context.Background() + type fields struct { + stepperOps *mocks.MockSnapshotBrowserStepper + } + for _, tc := range []struct { + kubeCli kubernetes.Interface + dynCli dynamic.Interface + args *types.SnapshotBrowseArgs + prepare func(f *fields) + errChecker Checker + }{ + { + // success + kubeCli: fake.NewSimpleClientset(), + dynCli: fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()), + args: &types.SnapshotBrowseArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return( + &snapv1.VolumeSnapshot{}, &sv1.StorageClass{}, nil, + ), + f.stepperOps.EXPECT().CreateInspectorApplication(gomock.Any(), gomock.Any(), + &snapv1.VolumeSnapshot{}, &sv1.StorageClass{}, + ).Return( + &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "ns", + }, + }, + &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc1", + Namespace: "ns", + }, + }, + nil, + ), + f.stepperOps.EXPECT().PortForwardAPod(gomock.Any(), + &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "ns", + }, + }, gomock.Any(), + ).Return(nil), + f.stepperOps.EXPECT().Cleanup(gomock.Any(), + &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc1", + Namespace: "ns", + }, + }, + &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + Namespace: "ns", + }, + }, + ), + ) + }, + errChecker: IsNil, + }, + { + // portforward failure + kubeCli: fake.NewSimpleClientset(), + dynCli: fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()), + args: &types.SnapshotBrowseArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return(nil, nil, nil), + f.stepperOps.EXPECT().CreateInspectorApplication(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, nil), + f.stepperOps.EXPECT().PortForwardAPod(gomock.Any(), gomock.Any(), gomock.Any()).Return(fmt.Errorf("portforward error")), + f.stepperOps.EXPECT().Cleanup(gomock.Any(), gomock.Any(), gomock.Any()), + ) + }, + errChecker: NotNil, + }, + { + // createapp failure + kubeCli: fake.NewSimpleClientset(), + dynCli: fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()), + args: &types.SnapshotBrowseArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return(nil, nil, nil), + f.stepperOps.EXPECT().CreateInspectorApplication(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, fmt.Errorf("createapp error")), + f.stepperOps.EXPECT().Cleanup(gomock.Any(), gomock.Any(), gomock.Any()), + ) + }, + errChecker: NotNil, + }, + { + // fetch snapshot failure + kubeCli: fake.NewSimpleClientset(), + dynCli: fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()), + args: &types.SnapshotBrowseArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return(nil, nil, fmt.Errorf("snapshot error")), + f.stepperOps.EXPECT().Cleanup(gomock.Any(), gomock.Any(), gomock.Any()), + ) + }, + errChecker: NotNil, + }, + { + // validate failure + kubeCli: fake.NewSimpleClientset(), + dynCli: fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()), + args: &types.SnapshotBrowseArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return(nil, nil, fmt.Errorf("validate error")), + f.stepperOps.EXPECT().Cleanup(gomock.Any(), gomock.Any(), gomock.Any()), + ) + }, + errChecker: NotNil, + }, + { + // emptycli failure + kubeCli: nil, + dynCli: fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()), + args: &types.SnapshotBrowseArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().Cleanup(gomock.Any(), gomock.Any(), gomock.Any()), + ) + }, + errChecker: NotNil, + }, + { + // emptydyncli failure + kubeCli: fake.NewSimpleClientset(), + dynCli: nil, + args: &types.SnapshotBrowseArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().Cleanup(gomock.Any(), gomock.Any(), gomock.Any()), + ) + }, + errChecker: NotNil, + }, + } { + ctrl := gomock.NewController(c) + defer ctrl.Finish() + f := fields{ + stepperOps: mocks.NewMockSnapshotBrowserStepper(ctrl), + } + if tc.prepare != nil { + tc.prepare(&f) + } + runner := &SnapshotBrowseRunner{ + KubeCli: tc.kubeCli, + DynCli: tc.dynCli, + browserSteps: f.stepperOps, + } + err := runner.RunSnapshotBrowseHelper(ctx, tc.args) + c.Check(err, tc.errChecker) + } +} + +func (s *CSITestSuite) TestSnapshotBrowseRunner(c *C) { + ctx := context.Background() + r := &SnapshotBrowseRunner{ + browserSteps: &snapshotBrowserSteps{}, + } + err := r.RunSnapshotBrowseHelper(ctx, nil) + c.Check(err, NotNil) +} diff --git a/pkg/csi/types/csi_types.go b/pkg/csi/types/csi_types.go index d09a745..da29d65 100644 --- a/pkg/csi/types/csi_types.go +++ b/pkg/csi/types/csi_types.go @@ -94,6 +94,18 @@ func (c *CreateSnapshotArgs) Validate() error { return nil } +type FetchSnapshotArgs struct { + Namespace string + SnapshotName string +} + +func (c *FetchSnapshotArgs) Validate() error { + if c.Namespace == "" || c.SnapshotName == "" { + return fmt.Errorf("Invalid FetchSnapshotArgs (%v)", c) + } + return nil +} + type CreateFromSourceCheckArgs struct { VolumeSnapshotClass string SnapshotName string @@ -122,6 +134,20 @@ func (p *PVCBrowseArgs) Validate() error { return nil } +type SnapshotBrowseArgs struct { + SnapshotName string + Namespace string + RunAsUser int64 + LocalPort int +} + +func (p *SnapshotBrowseArgs) Validate() error { + if p.SnapshotName == "" || p.Namespace == "" { + return fmt.Errorf("Invalid SnapshotBrowseArgs (%v)", p) + } + return nil +} + type PortForwardAPodRequest struct { // RestConfig is the kubernetes config RestConfig *rest.Config