Skip to content

Commit 8d5944d

Browse files
Dipta Dastamalsaha
authored andcommitted
Add Stash CLI (#734)
fixes #715
1 parent 550ab37 commit 8d5944d

File tree

8 files changed

+733
-0
lines changed

8 files changed

+733
-0
lines changed

cli/backup-pv.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/appscode/go/flags"
8+
"github.com/appscode/go/log"
9+
"github.com/appscode/stash/apis/stash/v1beta1"
10+
"github.com/spf13/cobra"
11+
core "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
)
14+
15+
func NewBackupPVCmd() *cobra.Command {
16+
var (
17+
kubeConfig string
18+
namespace string
19+
template string
20+
volume string
21+
22+
targetDirs []string
23+
mountPath string
24+
)
25+
26+
var cmd = &cobra.Command{
27+
Use: "backup-pv",
28+
Short: `Backup persistent volume`,
29+
Long: `Backup persistent volume using BackupConfiguration Template`,
30+
DisableAutoGenTag: true,
31+
RunE: func(cmd *cobra.Command, args []string) error {
32+
flags.EnsureRequiredFlags(cmd, "volume", "template", "directories", "mountpath")
33+
34+
c, err := newStashCLIController(kubeConfig)
35+
if err != nil {
36+
return err
37+
}
38+
39+
// check backupConfigurationTemplate exists
40+
_, err = c.stashClient.StashV1beta1().BackupConfigurationTemplates().Get(template, metav1.GetOptions{})
41+
if err != nil {
42+
return fmt.Errorf("can't get BackupConfigurationTemplate %s, reason: %s", template, err)
43+
}
44+
45+
// get PV
46+
pv, err := c.kubeClient.CoreV1().PersistentVolumes().Get(volume, metav1.GetOptions{})
47+
if err != nil {
48+
return fmt.Errorf("can't get PersistentVolumes %s, reason: %s", volume, err)
49+
}
50+
51+
// create PVC and add default backup annotations
52+
pvc := &core.PersistentVolumeClaim{
53+
ObjectMeta: metav1.ObjectMeta{
54+
Name: volume + "-pvc", // use generateName ?
55+
Namespace: namespace,
56+
Annotations: map[string]string{
57+
v1beta1.KeyBackupConfigurationTemplate: template,
58+
v1beta1.KeyMountPath: mountPath,
59+
v1beta1.KeyTargetDirectories: strings.Join(targetDirs, ","),
60+
},
61+
},
62+
Spec: core.PersistentVolumeClaimSpec{
63+
// set other optional fields ?
64+
VolumeName: volume,
65+
AccessModes: pv.Spec.AccessModes,
66+
Resources: core.ResourceRequirements{
67+
Limits: pv.Spec.Capacity,
68+
Requests: pv.Spec.Capacity,
69+
},
70+
},
71+
}
72+
pvc, err = c.kubeClient.CoreV1().PersistentVolumeClaims(namespace).Create(pvc)
73+
if err != nil {
74+
return err
75+
}
76+
log.Infof("PVC %s/%s created and annotated", namespace, pvc.Name)
77+
return nil
78+
},
79+
}
80+
81+
cmd.Flags().StringVar(&kubeConfig, "kubeconfig", kubeConfig, "Path of the Kube config file.")
82+
cmd.Flags().StringVar(&volume, "volume", volume, "Name of the Persistent volume.")
83+
cmd.Flags().StringVar(&namespace, "namespace", "default", "Namespace for Persistent Volume Claim.")
84+
cmd.Flags().StringVar(&template, "template", template, "Name of the BackupConfigurationTemplate.")
85+
86+
cmd.Flags().StringSliceVar(&targetDirs, "directories", targetDirs, "List of target directories.")
87+
cmd.Flags().StringVar(&mountPath, "mountpath", mountPath, "Mount path for PVC.")
88+
89+
return cmd
90+
}

cli/cli.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package cli
2+
3+
import (
4+
"path/filepath"
5+
6+
cs "github.com/appscode/stash/client/clientset/versioned"
7+
"github.com/spf13/cobra"
8+
"k8s.io/client-go/kubernetes"
9+
"k8s.io/client-go/rest"
10+
"k8s.io/client-go/util/homedir"
11+
meta_util "kmodules.xyz/client-go/meta"
12+
"kmodules.xyz/client-go/tools/clientcmd"
13+
)
14+
15+
const (
16+
cliScratchDir = "/tmp/stash-cli/scratch"
17+
cliSecretDir = "/tmp/stash-cli/secret"
18+
)
19+
20+
type stashCLIController struct {
21+
clientConfig *rest.Config
22+
kubeClient kubernetes.Interface
23+
stashClient cs.Interface
24+
}
25+
26+
func NewCLICmd() *cobra.Command {
27+
var cmd = &cobra.Command{
28+
Use: "cli",
29+
Short: `Stash CLI`,
30+
Long: `Kubectl plugin for Stash`,
31+
DisableAutoGenTag: true,
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
return nil
34+
},
35+
}
36+
37+
cmd.AddCommand(NewCopyRepositoryCmd())
38+
cmd.AddCommand(NewUnlockRepositoryCmd())
39+
cmd.AddCommand(NewUnlockLocalRepositoryCmd())
40+
cmd.AddCommand(NewTriggerBackupCmd())
41+
cmd.AddCommand(NewBackupPVCmd())
42+
cmd.AddCommand(NewDownloadCmd())
43+
44+
return cmd
45+
}
46+
47+
func newStashCLIController(kubeConfig string) (*stashCLIController, error) {
48+
var (
49+
controller = &stashCLIController{}
50+
err error
51+
)
52+
if kubeConfig == "" && !meta_util.PossiblyInCluster() {
53+
kubeConfig = filepath.Join(homedir.HomeDir(), "/.kube/config")
54+
}
55+
if controller.clientConfig, err = clientcmd.BuildConfigFromContext(kubeConfig, ""); err != nil {
56+
return nil, err
57+
}
58+
if controller.kubeClient, err = kubernetes.NewForConfig(controller.clientConfig); err != nil {
59+
return nil, err
60+
}
61+
if controller.stashClient, err = cs.NewForConfig(controller.clientConfig); err != nil {
62+
return nil, err
63+
}
64+
return controller, nil
65+
}

cli/copy_repository.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package cli
2+
3+
import (
4+
"github.com/appscode/go/flags"
5+
"github.com/appscode/go/log"
6+
"github.com/appscode/stash/apis/stash/v1alpha1"
7+
"github.com/appscode/stash/client/clientset/versioned/typed/stash/v1alpha1/util"
8+
"github.com/spf13/cobra"
9+
core "k8s.io/api/core/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
core_util "kmodules.xyz/client-go/core/v1"
12+
)
13+
14+
func NewCopyRepositoryCmd() *cobra.Command {
15+
var (
16+
kubeConfig string
17+
repositoryName string
18+
sourceNamespace string
19+
destinationNamespace string
20+
)
21+
22+
var cmd = &cobra.Command{
23+
Use: "copy-repository",
24+
Short: `Copy Repository and Secret`,
25+
Long: `Copy Repository and Secret from one namespace to another namespace`,
26+
DisableAutoGenTag: true,
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
flags.EnsureRequiredFlags(cmd, "repository", "source-namespace", "destination-namespace")
29+
30+
c, err := newStashCLIController(kubeConfig)
31+
if err != nil {
32+
return err
33+
}
34+
35+
// get source repository
36+
repository, err := c.stashClient.StashV1alpha1().Repositories(sourceNamespace).Get(repositoryName, metav1.GetOptions{})
37+
if err != nil {
38+
return err
39+
}
40+
// get source repository secret
41+
secret, err := c.kubeClient.CoreV1().Secrets(sourceNamespace).Get(repository.Spec.Backend.StorageSecretName, metav1.GetOptions{})
42+
if err != nil {
43+
return err
44+
}
45+
46+
// for local backend create/patch PVC
47+
if repository.Spec.Backend.Local != nil && repository.Spec.Backend.Local.PersistentVolumeClaim != nil {
48+
// get PVC
49+
pvc, err := c.kubeClient.CoreV1().PersistentVolumeClaims(sourceNamespace).Get(
50+
repository.Spec.Backend.Local.PersistentVolumeClaim.ClaimName,
51+
metav1.GetOptions{},
52+
)
53+
if err != nil {
54+
return err
55+
}
56+
_, _, err = core_util.CreateOrPatchPVC(
57+
c.kubeClient,
58+
metav1.ObjectMeta{
59+
Name: pvc.Name,
60+
Namespace: destinationNamespace,
61+
},
62+
func(obj *core.PersistentVolumeClaim) *core.PersistentVolumeClaim {
63+
obj.Spec = pvc.Spec
64+
return obj
65+
},
66+
)
67+
if err != nil {
68+
return err
69+
}
70+
log.Infof("PVC %s copied from namespace %s to %s", pvc.Name, sourceNamespace, destinationNamespace)
71+
}
72+
73+
// create/patch destination repository secret
74+
// only copy data
75+
_, _, err = core_util.CreateOrPatchSecret(
76+
c.kubeClient,
77+
metav1.ObjectMeta{
78+
Name: secret.Name,
79+
Namespace: destinationNamespace,
80+
},
81+
func(obj *core.Secret) *core.Secret {
82+
obj.Data = secret.Data
83+
return obj
84+
},
85+
)
86+
if err != nil {
87+
return err
88+
}
89+
log.Infof("Secret %s copied from namespace %s to %s", secret.Name, sourceNamespace, destinationNamespace)
90+
91+
// create/patch destination repository
92+
// only copy spec
93+
_, _, err = util.CreateOrPatchRepository(
94+
c.stashClient.StashV1alpha1(),
95+
metav1.ObjectMeta{
96+
Name: repository.Name,
97+
Namespace: destinationNamespace,
98+
},
99+
func(obj *v1alpha1.Repository) *v1alpha1.Repository {
100+
obj.Spec = repository.Spec
101+
return obj
102+
},
103+
)
104+
if err != nil {
105+
return err
106+
}
107+
log.Infof("Repository %s copied from namespace %s to %s", repositoryName, sourceNamespace, destinationNamespace)
108+
return nil
109+
},
110+
}
111+
112+
cmd.Flags().StringVar(&kubeConfig, "kubeconfig", kubeConfig, "Path of the Kube config file.")
113+
cmd.Flags().StringVar(&repositoryName, "repository", repositoryName, "Name of the Repository.")
114+
cmd.Flags().StringVar(&sourceNamespace, "source-namespace", sourceNamespace, "Source namespace.")
115+
cmd.Flags().StringVar(&destinationNamespace, "destination-namespace", destinationNamespace, "Destination namespace.")
116+
117+
return cmd
118+
}

cli/download.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/appscode/go/flags"
10+
"github.com/appscode/go/log"
11+
"github.com/appscode/stash/pkg/restic"
12+
"github.com/appscode/stash/pkg/util"
13+
"github.com/spf13/cobra"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
)
16+
17+
func NewDownloadCmd() *cobra.Command {
18+
var (
19+
kubeConfig string
20+
repositoryName string
21+
namespace string
22+
restoreOpt = restic.RestoreOptions{
23+
SourceHost: restic.DefaultHost,
24+
}
25+
)
26+
27+
var cmd = &cobra.Command{
28+
Use: "download",
29+
Short: `Download snapshots`,
30+
Long: `Download contents of snapshots from Repository`,
31+
DisableAutoGenTag: true,
32+
RunE: func(cmd *cobra.Command, args []string) error {
33+
flags.EnsureRequiredFlags(cmd, "repository")
34+
35+
c, err := newStashCLIController(kubeConfig)
36+
if err != nil {
37+
return err
38+
}
39+
40+
// get source repository
41+
repository, err := c.stashClient.StashV1alpha1().Repositories(namespace).Get(repositoryName, metav1.GetOptions{})
42+
if err != nil {
43+
return err
44+
}
45+
46+
// unlock local backend
47+
if repository.Spec.Backend.Local != nil {
48+
return fmt.Errorf("can't restore from repository with local backend")
49+
}
50+
51+
// get source repository secret
52+
secret, err := c.kubeClient.CoreV1().Secrets(namespace).Get(repository.Spec.Backend.StorageSecretName, metav1.GetOptions{})
53+
if err != nil {
54+
return err
55+
}
56+
57+
// cleanup whole scratch/secret dir at the end
58+
defer os.RemoveAll(cliScratchDir)
59+
defer os.RemoveAll(cliSecretDir)
60+
61+
// write repository secrets in a temp dir
62+
if err := os.MkdirAll(cliSecretDir, 0755); err != nil {
63+
return err
64+
}
65+
for key, value := range secret.Data {
66+
if err := ioutil.WriteFile(filepath.Join(cliSecretDir, key), value, 0755); err != nil {
67+
return err
68+
}
69+
}
70+
71+
// configure restic wrapper
72+
extraOpt := util.ExtraOptions{
73+
SecretDir: cliSecretDir,
74+
EnableCache: false,
75+
ScratchDir: cliScratchDir,
76+
}
77+
setupOpt, err := util.SetupOptionsForRepository(*repository, extraOpt)
78+
if err != nil {
79+
return fmt.Errorf("setup option for repository fail")
80+
}
81+
resticWrapper, err := restic.NewResticWrapper(setupOpt)
82+
if err != nil {
83+
return err
84+
}
85+
// if destination flag not specified, restore in current directory
86+
if restoreOpt.Destination == "" {
87+
restoreOpt.Destination, err = os.Getwd()
88+
if err != nil {
89+
return err
90+
}
91+
}
92+
// run restore
93+
if _, err = resticWrapper.RunRestore(restoreOpt); err != nil {
94+
return err
95+
}
96+
log.Infof("Repository %s/%s restored in path %s", namespace, repositoryName, restoreOpt.Destination)
97+
return nil
98+
},
99+
}
100+
101+
cmd.Flags().StringVar(&kubeConfig, "kubeconfig", kubeConfig, "Path of the Kube config file.")
102+
cmd.Flags().StringVar(&repositoryName, "repository", repositoryName, "Name of the Repository.")
103+
cmd.Flags().StringVar(&namespace, "namespace", "default", "Namespace of the Repository.")
104+
105+
cmd.Flags().StringVar(&restoreOpt.Destination, "destination", restoreOpt.Destination, "Destination path where snapshot will be restored.")
106+
cmd.Flags().StringVar(&restoreOpt.SourceHost, "host", restoreOpt.SourceHost, "Name of the source host machine")
107+
cmd.Flags().StringSliceVar(&restoreOpt.RestoreDirs, "directories", restoreOpt.RestoreDirs, "List of directories to be restored")
108+
// TODO: only allow a single snapshot ?
109+
cmd.Flags().StringSliceVar(&restoreOpt.Snapshots, "snapshots", restoreOpt.Snapshots, "List of snapshots to be restored")
110+
111+
return cmd
112+
}

0 commit comments

Comments
 (0)