diff --git a/cmd/rootCmd.go b/cmd/rootCmd.go index 023b900..b9d6596 100644 --- a/cmd/rootCmd.go +++ b/cmd/rootCmd.go @@ -128,6 +128,25 @@ var ( }, } + fromSnapshot string + toPVC string + path string + restoreFileCmd = &cobra.Command{ + Use: "file-restore", + Short: "Restore file(s) from a VolumeSnapshot to it's source PVC", + Long: "Restore file(s) from a given CSI provisioned VolumeSnapshot to a PVC.", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return FileRestore(context.Background(), + fromSnapshot, + toPVC, + namespace, + csiCheckRunAsUser, + browseLocalPort, + path) + }, + } + blockMountRunAsUser int64 blockMountCleanup bool blockMountCleanupOnly bool @@ -207,6 +226,15 @@ func init() { browseCmd.AddCommand(browseSnapshotCmd) + rootCmd.AddCommand(restoreFileCmd) + restoreFileCmd.Flags().StringVarP(&fromSnapshot, "fromSnapshot", "f", "", "The name of a VolumeSnapshot. (Required)") + _ = restoreFileCmd.MarkFlagRequired("fromSnapshot") + restoreFileCmd.Flags().StringVarP(&toPVC, "toPVC", "t", "", "The name of a PersistentVolumeClaim.") + restoreFileCmd.Flags().StringVarP(&namespace, "namespace", "n", fio.DefaultNS, "The namespace of both the given PVC & VS.") + restoreFileCmd.Flags().Int64VarP(&csiCheckRunAsUser, "runAsUser", "u", 0, "Runs the inspector pod as a user (int)") + restoreFileCmd.Flags().IntVarP(&browseLocalPort, "localport", "l", 8080, "The local port to expose the inspector") + restoreFileCmd.Flags().StringVarP(&path, "path", "p", "", "Path of a file or directory that needs to be restored") + rootCmd.AddCommand(blockMountCmd) blockMountCmd.Flags().StringVarP(&storageClass, "storageclass", "s", "", "The name of a StorageClass. (Required)") _ = blockMountCmd.MarkFlagRequired("storageclass") @@ -430,6 +458,42 @@ func CsiSnapshotBrowse(ctx context.Context, return err } +func FileRestore(ctx context.Context, + snapshotName string, + pvcName string, + namespace string, + runAsUser int64, + localPort int, + path string, +) 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 + } + fileRestoreRunner := &csi.FileRestoreRunner{ + KubeCli: kubecli, + DynCli: dyncli, + } + err = fileRestoreRunner.RunFileRestore(ctx, &csitypes.FileRestoreArgs{ + SnapshotName: snapshotName, + PVCName: pvcName, + Namespace: namespace, + RunAsUser: runAsUser, + LocalPort: localPort, + Path: path, + }) + if err != nil { + fmt.Printf("Failed to run file-restore (%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 37f23b7..202db70 100644 --- a/pkg/csi/csi_ops.go +++ b/pkg/csi/csi_ops.go @@ -332,41 +332,6 @@ 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/file_restore_inspector.go b/pkg/csi/file_restore_inspector.go new file mode 100644 index 0000000..8b343bb --- /dev/null +++ b/pkg/csi/file_restore_inspector.go @@ -0,0 +1,300 @@ +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 FileRestoreRunner struct { + KubeCli kubernetes.Interface + DynCli dynamic.Interface + restoreSteps FileRestoreStepper + restorePVC *v1.PersistentVolumeClaim + pod *v1.Pod + snapshot *snapv1.VolumeSnapshot +} + +func (f *FileRestoreRunner) RunFileRestore(ctx context.Context, args *types.FileRestoreArgs) error { + f.restoreSteps = &fileRestoreSteps{ + validateOps: &validateOperations{ + kubeCli: f.KubeCli, + dynCli: f.DynCli, + }, + versionFetchOps: &apiVersionFetch{ + kubeCli: f.KubeCli, + }, + createAppOps: &applicationCreate{ + kubeCli: f.KubeCli, + }, + portForwardOps: &portforward{}, + kubeExecutor: &kubeExec{ + kubeCli: f.KubeCli, + }, + cleanerOps: &cleanse{ + kubeCli: f.KubeCli, + dynCli: f.DynCli, + }, + } + return f.RunFileRestoreHelper(ctx, args) +} + +func (f *FileRestoreRunner) RunFileRestoreHelper(ctx context.Context, args *types.FileRestoreArgs) error { + defer func() { + fmt.Println("Cleaning up browser pod & restored PVC.") + f.restoreSteps.Cleanup(ctx, f.restorePVC, f.pod) + }() + + if f.KubeCli == nil || f.DynCli == nil { + return fmt.Errorf("cli uninitialized") + } + + fmt.Println("Fetching the snapshot.") + vs, sourcePVC, sc, err := f.restoreSteps.ValidateArgs(ctx, args) + if err != nil { + return errors.Wrap(err, "Failed to validate arguments.") + } + f.snapshot = vs + + fmt.Println("Creating the restored PVC & browser Pod.") + f.pod, f.restorePVC, err = f.restoreSteps.CreateInspectorApplication(ctx, args, f.snapshot, sourcePVC, sc) + if err != nil { + return errors.Wrap(err, "Failed to create inspector application.") + } + + if args.Path != "" { + fmt.Printf("Restoring the file %s\n", args.Path) + _, err := f.restoreSteps.ExecuteCopyCommand(ctx, args, f.pod) + if err != nil { + return errors.Wrap(err, "Failed to execute cp command in pod.") + } + fmt.Printf("File restored from VolumeSnapshot %s to Source PVC %s.\n", f.snapshot.Name, sourcePVC.Name) + return nil + } + + fmt.Println("Forwarding the port.") + err = f.restoreSteps.PortForwardAPod(f.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_file_restore_stepper.go -package=mocks . FileRestoreStepper +type FileRestoreStepper interface { + ValidateArgs(ctx context.Context, args *types.FileRestoreArgs) (*snapv1.VolumeSnapshot, *v1.PersistentVolumeClaim, *sv1.StorageClass, error) + CreateInspectorApplication(ctx context.Context, args *types.FileRestoreArgs, snapshot *snapv1.VolumeSnapshot, sourcePVC *v1.PersistentVolumeClaim, storageClass *sv1.StorageClass) (*v1.Pod, *v1.PersistentVolumeClaim, error) + ExecuteCopyCommand(ctx context.Context, args *types.FileRestoreArgs, pod *v1.Pod) (string, error) + PortForwardAPod(pod *v1.Pod, localPort int) error + Cleanup(ctx context.Context, restorePVC *v1.PersistentVolumeClaim, pod *v1.Pod) +} + +type fileRestoreSteps struct { + validateOps ArgumentValidator + versionFetchOps ApiVersionFetcher + createAppOps ApplicationCreator + portForwardOps PortForwarder + cleanerOps Cleaner + kubeExecutor KubeExecutor + SnapshotGroupVersion *metav1.GroupVersionForDiscovery +} + +func (f *fileRestoreSteps) ValidateArgs(ctx context.Context, args *types.FileRestoreArgs) (*snapv1.VolumeSnapshot, *v1.PersistentVolumeClaim, *sv1.StorageClass, error) { + if err := args.Validate(); err != nil { + return nil, nil, nil, errors.Wrap(err, "Failed to validate input arguments") + } + if err := f.validateOps.ValidateNamespace(ctx, args.Namespace); err != nil { + return nil, nil, nil, errors.Wrap(err, "Failed to validate Namespace") + } + groupVersion, err := f.versionFetchOps.GetCSISnapshotGroupVersion() + if err != nil { + return nil, nil, nil, errors.Wrap(err, "Failed to fetch groupVersion") + } + f.SnapshotGroupVersion = groupVersion + snapshot, err := f.validateOps.ValidateVolumeSnapshot(ctx, args.SnapshotName, args.Namespace, groupVersion) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "Failed to validate VolumeSnapshot") + } + var sourcePVC *v1.PersistentVolumeClaim + if args.PVCName == "" { + fmt.Println("Fetching the source PVC from snapshot.") + if *snapshot.Spec.Source.PersistentVolumeClaimName == "" { + return nil, nil, nil, errors.Wrap(err, "Failed to fetch source PVC. VolumeSnapshot does not have a PVC as it's source") + } + sourcePVC, err = f.validateOps.ValidatePVC(ctx, *snapshot.Spec.Source.PersistentVolumeClaimName, args.Namespace) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "Failed to validate source PVC") + } + } else { + fmt.Println("Fetching the source PVC.") + sourcePVC, err = f.validateOps.ValidatePVC(ctx, args.PVCName, args.Namespace) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "Failed to validate source PVC") + } + } + for _, sourceAccessMode := range sourcePVC.Spec.AccessModes { + if sourceAccessMode == v1.ReadWriteOncePod { + return nil, nil, nil, fmt.Errorf("Unsupported %s AccessMode found in source PVC. Supported AccessModes are ReadOnlyMany & ReadWriteMany", sourceAccessMode) + } + } + sc, err := f.validateOps.ValidateStorageClass(ctx, *sourcePVC.Spec.StorageClassName) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "Failed to validate StorageClass") + } + uVSC, err := f.validateOps.ValidateVolumeSnapshotClass(ctx, *snapshot.Spec.VolumeSnapshotClassName, groupVersion) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "Failed to validate VolumeSnapshotClass") + } + vscDriver := getDriverNameFromUVSC(*uVSC, groupVersion.GroupVersion) + if sc.Provisioner != vscDriver { + return nil, nil, nil, fmt.Errorf("StorageClass provisioner (%s) and VolumeSnapshotClass driver (%s) are different.", sc.Provisioner, vscDriver) + } + return snapshot, sourcePVC, sc, nil +} + +func (f *fileRestoreSteps) CreateInspectorApplication(ctx context.Context, args *types.FileRestoreArgs, snapshot *snapv1.VolumeSnapshot, sourcePVC *v1.PersistentVolumeClaim, 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, + } + restorePVC, err := f.createAppOps.CreatePVC(ctx, pvcArgs) + if err != nil { + return nil, nil, errors.Wrap(err, "Failed to restore PVC") + } + podArgs := &types.CreatePodArgs{ + GenerateName: clonedPodGenerateName, + Namespace: args.Namespace, + RunAsUser: args.RunAsUser, + ContainerImage: "filebrowser/filebrowser:v2", + ContainerArgs: []string{"--noauth"}, + PVCMap: map[string]types.VolumePath{ + restorePVC.Name: { + MountPath: "/srv/snapshot-data", + }, + sourcePVC.Name: { + MountPath: "/srv/source-data", + }, + }, + } + if args.Path != "" { + podArgs = &types.CreatePodArgs{ + GenerateName: clonedPodGenerateName, + Namespace: args.Namespace, + RunAsUser: args.RunAsUser, + ContainerImage: "alpine:3.19", + Command: []string{"/bin/sh"}, + ContainerArgs: []string{"-c", "while true; do sleep 3600; done"}, + PVCMap: map[string]types.VolumePath{ + restorePVC.Name: { + MountPath: "/snapshot-data", + }, + sourcePVC.Name: { + MountPath: "/source-data", + }, + }, + } + } + pod, err := f.createAppOps.CreatePod(ctx, podArgs) + if err != nil { + return nil, restorePVC, errors.Wrap(err, "Failed to create browse Pod") + } + if err = f.createAppOps.WaitForPodReady(ctx, args.Namespace, pod.Name); err != nil { + return pod, restorePVC, errors.Wrap(err, "Pod failed to become ready") + } + return pod, restorePVC, nil +} + +func (f *fileRestoreSteps) ExecuteCopyCommand(ctx context.Context, args *types.FileRestoreArgs, pod *v1.Pod) (string, error) { + command := []string{"cp", "-rf", fmt.Sprintf("/snapshot-data%s", args.Path), fmt.Sprintf("/source-data%s", args.Path)} + stdout, err := f.kubeExecutor.Exec(ctx, args.Namespace, pod.Name, pod.Spec.Containers[0].Name, command) + if err != nil { + return "", errors.Wrapf(err, "Error running command:(%v)", command) + } + return stdout, nil +} + +func (f *fileRestoreSteps) PortForwardAPod(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 := f.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("\nStopping 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 = f.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 (f *fileRestoreSteps) Cleanup(ctx context.Context, restorePVC *v1.PersistentVolumeClaim, pod *v1.Pod) { + if restorePVC != nil { + err := f.cleanerOps.DeletePVC(ctx, restorePVC.Name, restorePVC.Namespace) + if err != nil { + fmt.Println("Failed to delete restore PVC", restorePVC) + } + } + if pod != nil { + err := f.cleanerOps.DeletePod(ctx, pod.Name, pod.Namespace) + if err != nil { + fmt.Println("Failed to delete Pod", pod) + } + } +} diff --git a/pkg/csi/file_restore_inspector_steps_test.go b/pkg/csi/file_restore_inspector_steps_test.go new file mode 100644 index 0000000..24974ea --- /dev/null +++ b/pkg/csi/file_restore_inspector_steps_test.go @@ -0,0 +1,594 @@ +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) TestFileRestoreValidateArgs(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.FileRestoreArgs + prepare func(f *fields) + errChecker Checker + }{ + { // valid args + args: &types.FileRestoreArgs{ + 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.FileRestoreArgs{ + 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.FileRestoreArgs{ + 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.FileRestoreArgs{ + 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.FileRestoreArgs{ + 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.FileRestoreArgs{ + 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.FileRestoreArgs{ + 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.FileRestoreArgs{ + SnapshotName: "", + Namespace: "ns", + }, + errChecker: NotNil, + }, + { // validate ns error + args: &types.FileRestoreArgs{ + 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 := &fileRestoreSteps{ + validateOps: f.validateOps, + versionFetchOps: f.versionOps, + } + _, _, _, err := stepper.ValidateArgs(ctx, tc.args) + c.Check(err, tc.errChecker) + } +} + +func (s *CSITestSuite) TestCreateInspectorApplicationForFileRestore(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.FileRestoreArgs + snapshot *snapv1.VolumeSnapshot + sc *sv1.StorageClass + prepare func(f *fields) + errChecker Checker + podChecker Checker + pvcChecker Checker + }{ + { + args: &types.FileRestoreArgs{ + 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: "restorePVC", + }, + }, nil), + f.createAppOps.EXPECT().CreatePod(gomock.Any(), &types.CreatePodArgs{ + GenerateName: clonedPodGenerateName, + Namespace: "ns", + ContainerArgs: []string{"--noauth"}, + RunAsUser: 100, + ContainerImage: "filebrowser/filebrowser:v2", + PVCMap: map[string]types.VolumePath{ + "restorePVC": { + MountPath: "/srv/snapshot-data", + }, + "sourcePVC": { + MountPath: "/srv/source-data", + }, + }, + }).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.FileRestoreArgs{ + 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: "restorePVC", + }, + }, nil), + f.createAppOps.EXPECT().CreatePod(gomock.Any(), &types.CreatePodArgs{ + GenerateName: clonedPodGenerateName, + Namespace: "ns", + ContainerArgs: []string{"--noauth"}, + RunAsUser: 100, + ContainerImage: "filebrowser/filebrowser:v2", + PVCMap: map[string]types.VolumePath{ + "restorePVC": { + MountPath: "/srv/snapshot-data", + }, + "sourcePVC": { + MountPath: "/srv/source-data", + }, + }, + }).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.FileRestoreArgs{ + 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: "restorePVC", + }, + }, nil), + f.createAppOps.EXPECT().CreatePod(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("pod error")), + ) + }, + errChecker: NotNil, + podChecker: IsNil, + pvcChecker: NotNil, + }, + { + args: &types.FileRestoreArgs{ + 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 := &fileRestoreSteps{ + createAppOps: f.createAppOps, + } + sourcePVC := v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sourcePVC", + Namespace: tc.args.Namespace, + }, + Spec: v1.PersistentVolumeClaimSpec{ + AccessModes: []v1.PersistentVolumeAccessMode{ + v1.ReadWriteOnce, + }, + Resources: v1.VolumeResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, + } + pod, pvc, err := stepper.CreateInspectorApplication(ctx, tc.args, tc.snapshot, &sourcePVC, tc.sc) + c.Check(err, tc.errChecker) + c.Check(pod, tc.podChecker) + c.Check(pvc, tc.pvcChecker) + } +} + +func (s *CSITestSuite) TestFileRestoreCleanup(c *C) { + ctx := context.Background() + groupversion := &metav1.GroupVersionForDiscovery{ + GroupVersion: "gv", + Version: "v", + } + type fields struct { + cleanerOps *mocks.MockCleaner + } + for _, tc := range []struct { + restorePVC *v1.PersistentVolumeClaim + pod *v1.Pod + prepare func(f *fields) + }{ + { + restorePVC: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "restorePVC", + Namespace: "ns", + }, + }, + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "ns", + }, + }, + prepare: func(f *fields) { + gomock.InOrder( + f.cleanerOps.EXPECT().DeletePVC(ctx, "restorePVC", "ns").Return(nil), + f.cleanerOps.EXPECT().DeletePod(ctx, "pod", "ns").Return(nil), + ) + }, + }, + { + restorePVC: &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "restorePVC", + Namespace: "ns", + }, + }, + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "ns", + }, + }, + prepare: func(f *fields) { + gomock.InOrder( + f.cleanerOps.EXPECT().DeletePVC(ctx, "restorePVC", "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 := &fileRestoreSteps{ + cleanerOps: f.cleanerOps, + SnapshotGroupVersion: groupversion, + } + stepper.Cleanup(ctx, tc.restorePVC, tc.pod) + } +} diff --git a/pkg/csi/file_restore_inspector_test.go b/pkg/csi/file_restore_inspector_test.go new file mode 100644 index 0000000..713586b --- /dev/null +++ b/pkg/csi/file_restore_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) TestRunFileRestoreHelper(c *C) { + ctx := context.Background() + type fields struct { + stepperOps *mocks.MockFileRestoreStepper + } + for _, tc := range []struct { + kubeCli kubernetes.Interface + dynCli dynamic.Interface + args *types.FileRestoreArgs + prepare func(f *fields) + errChecker Checker + }{ + { + // success + kubeCli: fake.NewSimpleClientset(), + dynCli: fakedynamic.NewSimpleDynamicClient(runtime.NewScheme()), + args: &types.FileRestoreArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return( + &snapv1.VolumeSnapshot{}, &v1.PersistentVolumeClaim{}, &sv1.StorageClass{}, nil, + ), + f.stepperOps.EXPECT().CreateInspectorApplication(gomock.Any(), gomock.Any(), + &snapv1.VolumeSnapshot{}, &v1.PersistentVolumeClaim{}, &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( + &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.FileRestoreArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return(nil, nil, nil, nil), + f.stepperOps.EXPECT().CreateInspectorApplication(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil, nil), + f.stepperOps.EXPECT().PortForwardAPod(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.FileRestoreArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return(nil, nil, nil, nil), + f.stepperOps.EXPECT().CreateInspectorApplication(gomock.Any(), 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.FileRestoreArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return(nil, 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.FileRestoreArgs{}, + prepare: func(f *fields) { + gomock.InOrder( + f.stepperOps.EXPECT().ValidateArgs(gomock.Any(), gomock.Any()).Return(nil, 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.FileRestoreArgs{}, + 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.FileRestoreArgs{}, + 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.NewMockFileRestoreStepper(ctrl), + } + if tc.prepare != nil { + tc.prepare(&f) + } + runner := &FileRestoreRunner{ + KubeCli: tc.kubeCli, + DynCli: tc.dynCli, + restoreSteps: f.stepperOps, + } + err := runner.RunFileRestoreHelper(ctx, tc.args) + c.Check(err, tc.errChecker) + } +} + +func (s *CSITestSuite) TestFileRestoreRunner(c *C) { + ctx := context.Background() + r := &FileRestoreRunner{ + restoreSteps: &fileRestoreSteps{}, + } + err := r.RunFileRestoreHelper(ctx, nil) + c.Check(err, NotNil) +} diff --git a/pkg/csi/mocks/mock_file_restore_stepper.go b/pkg/csi/mocks/mock_file_restore_stepper.go new file mode 100644 index 0000000..1e2c90c --- /dev/null +++ b/pkg/csi/mocks/mock_file_restore_stepper.go @@ -0,0 +1,113 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/kastenhq/kubestr/pkg/csi (interfaces: FileRestoreStepper) + +// 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" +) + +// MockFileRestoreStepper is a mock of FileRestoreStepper interface. +type MockFileRestoreStepper struct { + ctrl *gomock.Controller + recorder *MockFileRestoreStepperMockRecorder +} + +// MockFileRestoreStepperMockRecorder is the mock recorder for MockFileRestoreStepper. +type MockFileRestoreStepperMockRecorder struct { + mock *MockFileRestoreStepper +} + +// NewMockFileRestoreStepper creates a new mock instance. +func NewMockFileRestoreStepper(ctrl *gomock.Controller) *MockFileRestoreStepper { + mock := &MockFileRestoreStepper{ctrl: ctrl} + mock.recorder = &MockFileRestoreStepperMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFileRestoreStepper) EXPECT() *MockFileRestoreStepperMockRecorder { + return m.recorder +} + +// Cleanup mocks base method. +func (m *MockFileRestoreStepper) 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 *MockFileRestoreStepperMockRecorder) Cleanup(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Cleanup", reflect.TypeOf((*MockFileRestoreStepper)(nil).Cleanup), arg0, arg1, arg2) +} + +// CreateInspectorApplication mocks base method. +func (m *MockFileRestoreStepper) CreateInspectorApplication(arg0 context.Context, arg1 *types.FileRestoreArgs, arg2 *v1.VolumeSnapshot, arg3 *v10.PersistentVolumeClaim, arg4 *v11.StorageClass) (*v10.Pod, *v10.PersistentVolumeClaim, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateInspectorApplication", arg0, arg1, arg2, arg3, arg4) + 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 *MockFileRestoreStepperMockRecorder) CreateInspectorApplication(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateInspectorApplication", reflect.TypeOf((*MockFileRestoreStepper)(nil).CreateInspectorApplication), arg0, arg1, arg2, arg3, arg4) +} + +// ExecuteCopyCommand mocks base method. +func (m *MockFileRestoreStepper) ExecuteCopyCommand(arg0 context.Context, arg1 *types.FileRestoreArgs, arg2 *v10.Pod) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteCopyCommand", arg0, arg1, arg2) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteCopyCommand indicates an expected call of ExecuteCopyCommand. +func (mr *MockFileRestoreStepperMockRecorder) ExecuteCopyCommand(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteCopyCommand", reflect.TypeOf((*MockFileRestoreStepper)(nil).ExecuteCopyCommand), arg0, arg1, arg2) +} + +// PortForwardAPod mocks base method. +func (m *MockFileRestoreStepper) PortForwardAPod(arg0 *v10.Pod, arg1 int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PortForwardAPod", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// PortForwardAPod indicates an expected call of PortForwardAPod. +func (mr *MockFileRestoreStepperMockRecorder) PortForwardAPod(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PortForwardAPod", reflect.TypeOf((*MockFileRestoreStepper)(nil).PortForwardAPod), arg0, arg1) +} + +// ValidateArgs mocks base method. +func (m *MockFileRestoreStepper) ValidateArgs(arg0 context.Context, arg1 *types.FileRestoreArgs) (*v1.VolumeSnapshot, *v10.PersistentVolumeClaim, *v11.StorageClass, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ValidateArgs", arg0, arg1) + ret0, _ := ret[0].(*v1.VolumeSnapshot) + ret1, _ := ret[1].(*v10.PersistentVolumeClaim) + ret2, _ := ret[2].(*v11.StorageClass) + ret3, _ := ret[3].(error) + return ret0, ret1, ret2, ret3 +} + +// ValidateArgs indicates an expected call of ValidateArgs. +func (mr *MockFileRestoreStepperMockRecorder) ValidateArgs(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateArgs", reflect.TypeOf((*MockFileRestoreStepper)(nil).ValidateArgs), arg0, arg1) +} diff --git a/pkg/csi/mocks/mock_snapshot_fetcher.go b/pkg/csi/mocks/mock_snapshot_fetcher.go deleted file mode 100644 index 8bd55e8..0000000 --- a/pkg/csi/mocks/mock_snapshot_fetcher.go +++ /dev/null @@ -1,68 +0,0 @@ -// 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/snapshot_inspector.go b/pkg/csi/snapshot_inspector.go index f8fbd02..7a7824b 100644 --- a/pkg/csi/snapshot_inspector.go +++ b/pkg/csi/snapshot_inspector.go @@ -39,10 +39,6 @@ func (r *SnapshotBrowseRunner) RunSnapshotBrowse(ctx context.Context, args *type createAppOps: &applicationCreate{ kubeCli: r.KubeCli, }, - snapshotFetchOps: &snapshotFetch{ - kubeCli: r.KubeCli, - dynCli: r.DynCli, - }, portForwardOps: &portforward{}, kubeExecutor: &kubeExec{ kubeCli: r.KubeCli, @@ -52,10 +48,6 @@ func (r *SnapshotBrowseRunner) RunSnapshotBrowse(ctx context.Context, args *type dynCli: r.DynCli, }, } - if args.ShowTree { - fmt.Println("Show Tree works for VS!") - return nil - } return r.RunSnapshotBrowseHelper(ctx, args) } @@ -113,7 +105,6 @@ type SnapshotBrowserStepper interface { type snapshotBrowserSteps struct { validateOps ArgumentValidator versionFetchOps ApiVersionFetcher - snapshotFetchOps SnapshotFetcher createAppOps ApplicationCreator portForwardOps PortForwarder cleanerOps Cleaner diff --git a/pkg/csi/types/csi_types.go b/pkg/csi/types/csi_types.go index dd92b36..3cdc2da 100644 --- a/pkg/csi/types/csi_types.go +++ b/pkg/csi/types/csi_types.go @@ -162,6 +162,22 @@ func (p *SnapshotBrowseArgs) Validate() error { return nil } +type FileRestoreArgs struct { + SnapshotName string + PVCName string + Namespace string + RunAsUser int64 + LocalPort int + Path string +} + +func (f *FileRestoreArgs) Validate() error { + if f.SnapshotName == "" || f.Namespace == "" { + return fmt.Errorf("Invalid FileRestoreArgs (%v)", f) + } + return nil +} + type PortForwardAPodRequest struct { // RestConfig is the kubernetes config RestConfig *rest.Config