Skip to content

Commit a570614

Browse files
authored
Add commands for generating restore rules and managing restic keys (#189)
Signed-off-by: hmsayem <hmsayem@appscode.com>
1 parent 460773d commit a570614

26 files changed

+1415
-177
lines changed

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/pkg/errors v0.9.1
1010
github.com/spf13/cobra v1.7.0
1111
golang.org/x/text v0.9.0
12+
gomodules.xyz/flags v0.1.3
1213
gomodules.xyz/go-sh v0.1.0
1314
gomodules.xyz/logs v0.0.7
1415
gomodules.xyz/pointer v0.1.0
@@ -25,7 +26,8 @@ require (
2526
kmodules.xyz/objectstore-api v0.25.1
2627
kmodules.xyz/offshoot-api v0.25.4
2728
kmodules.xyz/openshift v0.25.0
28-
stash.appscode.dev/apimachinery v0.31.1
29+
sigs.k8s.io/yaml v1.3.0
30+
stash.appscode.dev/apimachinery v0.31.2-0.20231004063459-741d01499f90
2931
stash.appscode.dev/stash v0.31.0
3032
)
3133

@@ -104,7 +106,6 @@ require (
104106
golang.org/x/term v0.8.0 // indirect
105107
golang.org/x/time v0.1.0 // indirect
106108
gomodules.xyz/clock v0.0.0-20200817085942-06523dba733f // indirect
107-
gomodules.xyz/flags v0.1.3 // indirect
108109
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
109110
gomodules.xyz/mergo v0.3.13 // indirect
110111
gomodules.xyz/sets v0.2.1 // indirect
@@ -127,5 +128,4 @@ require (
127128
sigs.k8s.io/kustomize/api v0.12.1 // indirect
128129
sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
129130
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
130-
sigs.k8s.io/yaml v1.3.0 // indirect
131131
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,7 +1102,7 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
11021102
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
11031103
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
11041104
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
1105-
stash.appscode.dev/apimachinery v0.31.1 h1:5a4+kSnsNvg+iT+xTyoD7SjzbSdmTrmY+VuQuY/qJpw=
1106-
stash.appscode.dev/apimachinery v0.31.1/go.mod h1:IDbssRbYLSnMwZAQOGX4Vam+hl43FofP8BuXMLXVaPQ=
1105+
stash.appscode.dev/apimachinery v0.31.2-0.20231004063459-741d01499f90 h1:PaLblgeoXMiuKFuK6lor9DIHWnC0xblT6/Rfs7wvIyY=
1106+
stash.appscode.dev/apimachinery v0.31.2-0.20231004063459-741d01499f90/go.mod h1:IDbssRbYLSnMwZAQOGX4Vam+hl43FofP8BuXMLXVaPQ=
11071107
stash.appscode.dev/stash v0.31.0 h1:q5dLevm2mZECXEpyba04JbqyUeVtYTKtfS1QcpRU+II=
11081108
stash.appscode.dev/stash v0.31.0/go.mod h1:TfYcLyr+YsTqoKHNbyOgOCPqygjRpf3USMztNc2EfjU=

pkg/add_key.go

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
/*
2+
Copyright AppsCode Inc. and Contributors
3+
4+
Licensed under the AppsCode Community License 1.0.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://github.com/appscode/licenses/raw/1.0.0/AppsCode-Community-1.0.0.md
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package pkg
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"os"
23+
"os/exec"
24+
"os/user"
25+
"path/filepath"
26+
27+
"stash.appscode.dev/apimachinery/apis"
28+
"stash.appscode.dev/apimachinery/apis/stash/v1alpha1"
29+
"stash.appscode.dev/apimachinery/pkg/restic"
30+
"stash.appscode.dev/stash/pkg/util"
31+
32+
"github.com/pkg/errors"
33+
"github.com/spf13/cobra"
34+
"gomodules.xyz/flags"
35+
core "k8s.io/api/core/v1"
36+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
"k8s.io/cli-runtime/pkg/genericclioptions"
38+
"k8s.io/client-go/rest"
39+
"k8s.io/klog/v2"
40+
)
41+
42+
type keyOptions struct {
43+
config *rest.Config
44+
repo *v1alpha1.Repository
45+
localDirs cliLocalDirectories
46+
restic.KeyOptions
47+
}
48+
49+
func NewCmdAddKey(clientGetter genericclioptions.RESTClientGetter) *cobra.Command {
50+
opt := keyOptions{}
51+
cmd := &cobra.Command{
52+
Use: "add",
53+
Short: `Add a new key (password) to a restic repository`,
54+
DisableAutoGenTag: true,
55+
RunE: func(cmd *cobra.Command, args []string) error {
56+
flags.EnsureRequiredFlags(cmd, "new-password-file")
57+
var err error
58+
opt.File, err = filepath.Abs(opt.File)
59+
if err != nil {
60+
return fmt.Errorf("failed to find the absolute path for password file: %w", err)
61+
}
62+
63+
_, err = os.Stat(opt.File)
64+
if os.IsNotExist(err) {
65+
return fmt.Errorf("%s does not exist", opt.File)
66+
}
67+
68+
if len(args) == 0 || args[0] == "" {
69+
return fmt.Errorf("repository name not found")
70+
}
71+
repositoryName := args[0]
72+
73+
opt.config, err = clientGetter.ToRESTConfig()
74+
if err != nil {
75+
return errors.Wrap(err, "failed to read kubeconfig")
76+
}
77+
78+
// get source repository
79+
opt.repo, err = stashClient.StashV1alpha1().Repositories(namespace).Get(context.TODO(), repositoryName, metav1.GetOptions{})
80+
if err != nil {
81+
return err
82+
}
83+
84+
if opt.repo.Spec.Backend.Local != nil {
85+
return opt.addResticKeyForLocalRepo()
86+
}
87+
88+
return opt.addResticKey()
89+
},
90+
}
91+
92+
cmd.Flags().StringVar(&opt.Host, "host", opt.Host, "Host for the new key")
93+
cmd.Flags().StringVar(&opt.User, "user", opt.User, "Username for the new key")
94+
cmd.Flags().StringVar(&opt.File, "new-password-file", opt.File, "File from which to read the new password")
95+
96+
return cmd
97+
}
98+
99+
func (opt *keyOptions) addResticKeyForLocalRepo() error {
100+
// get the pod that mount this repository as volume
101+
pod, err := getBackendMountingPod(kubeClient, opt.repo)
102+
if err != nil {
103+
return err
104+
}
105+
106+
if err := opt.copyPasswordFileToPod(pod); err != nil {
107+
return fmt.Errorf("failed to copy password file from local directory to pod: %w", err)
108+
}
109+
110+
command := []string{"/stash-enterprise", "add-key"}
111+
command = append(command, "--repo-name="+opt.repo.Name, "--repo-namespace="+opt.repo.Namespace)
112+
command = append(command, "--new-password-file="+getPodDirForPasswordFile())
113+
114+
if opt.User != "" {
115+
command = append(command, "--user="+opt.User)
116+
}
117+
if opt.Host != "" {
118+
command = append(command, "--host="+opt.Host)
119+
}
120+
121+
_, err = execCommandOnPod(kubeClient, opt.config, pod, command)
122+
if err != nil {
123+
return err
124+
}
125+
126+
if err := opt.removePasswordFileFromPod(pod); err != nil {
127+
return fmt.Errorf("failed to remove password file from pod: %w", err)
128+
}
129+
130+
klog.Infof("Restic key has been added successfully for repository %s/%s", opt.repo.Namespace, opt.repo.Name)
131+
return nil
132+
}
133+
134+
func (opt *keyOptions) removePasswordFileFromPod(pod *core.Pod) error {
135+
cmd := []string{"rm", "-rf", getPodDirForPasswordFile()}
136+
out, err := execCommandOnPod(kubeClient, opt.config, pod, cmd)
137+
if string(out) != "" {
138+
klog.Infoln("Output:", string(out))
139+
}
140+
return err
141+
}
142+
143+
func (opt *keyOptions) copyPasswordFileToPod(pod *core.Pod) error {
144+
_, err := exec.Command(cmdKubectl, "cp", opt.File, fmt.Sprintf("%s/%s:%s", pod.Namespace, pod.Name, getPodDirForPasswordFile())).CombinedOutput()
145+
if err != nil {
146+
return err
147+
}
148+
return nil
149+
}
150+
151+
func getPodDirForPasswordFile() string {
152+
return filepath.Join(apis.ScratchDirMountPath, passwordFile)
153+
}
154+
155+
func (opt *keyOptions) addResticKey() error {
156+
// get source repository secret
157+
secret, err := kubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), opt.repo.Spec.Backend.StorageSecretName, metav1.GetOptions{})
158+
if err != nil {
159+
return err
160+
}
161+
162+
if err = os.MkdirAll(ScratchDir, 0o755); err != nil {
163+
return err
164+
}
165+
defer os.RemoveAll(ScratchDir)
166+
167+
// configure restic wrapper
168+
extraOpt := util.ExtraOptions{
169+
StorageSecret: secret,
170+
ScratchDir: ScratchDir,
171+
}
172+
// configure setupOption
173+
setupOpt, err := util.SetupOptionsForRepository(*opt.repo, extraOpt)
174+
if err != nil {
175+
return fmt.Errorf("setup option for repository failed")
176+
}
177+
// init restic wrapper
178+
resticWrapper, err := restic.NewResticWrapper(setupOpt)
179+
if err != nil {
180+
return err
181+
}
182+
183+
opt.localDirs = cliLocalDirectories{
184+
configDir: filepath.Join(ScratchDir, configDirName),
185+
}
186+
187+
// dump restic's environments into `restic-env` file.
188+
// we will pass this env file to restic docker container.
189+
190+
err = resticWrapper.DumpEnv(opt.localDirs.configDir, ResticEnvs)
191+
if err != nil {
192+
return err
193+
}
194+
195+
args := []string{
196+
"key",
197+
"add",
198+
"--no-cache",
199+
}
200+
201+
// For TLS secured Minio/REST server, specify cert path
202+
if resticWrapper.GetCaPath() != "" {
203+
args = append(args, "--cacert", resticWrapper.GetCaPath())
204+
}
205+
206+
if err = manageKeyViaDocker(opt, args); err != nil {
207+
return err
208+
}
209+
klog.Infof("Restic key has been added successfully for repository %s/%s", opt.repo.Namespace, opt.repo.Name)
210+
return nil
211+
}
212+
213+
func manageKeyViaDocker(opt *keyOptions, args []string) error {
214+
// get current user
215+
currentUser, err := user.Current()
216+
if err != nil {
217+
return err
218+
}
219+
220+
keyArgs := []string{
221+
"run",
222+
"--rm",
223+
"-u", currentUser.Uid,
224+
"-v", ScratchDir + ":" + ScratchDir,
225+
"--env", "HTTP_PROXY=" + os.Getenv("HTTP_PROXY"),
226+
"--env", "HTTPS_PROXY=" + os.Getenv("HTTPS_PROXY"),
227+
"--env-file", filepath.Join(opt.localDirs.configDir, ResticEnvs),
228+
}
229+
230+
if opt.File != "" {
231+
keyArgs = append(keyArgs, "-v", opt.File+":"+opt.File)
232+
}
233+
234+
keyArgs = append(keyArgs, imgRestic.ToContainerImage())
235+
keyArgs = append(keyArgs, args...)
236+
237+
if opt.File != "" {
238+
keyArgs = append(keyArgs, "--new-password-file", opt.File)
239+
}
240+
241+
if opt.User != "" {
242+
keyArgs = append(keyArgs, "--user", opt.User)
243+
}
244+
245+
if opt.Host != "" {
246+
keyArgs = append(keyArgs, "--host", opt.Host)
247+
}
248+
249+
klog.Infoln("Running docker with args:", keyArgs)
250+
251+
out, err := exec.Command("docker", keyArgs...).CombinedOutput()
252+
if err != nil {
253+
return err
254+
}
255+
klog.Infoln(fmt.Sprintf("\n%s", string(out)))
256+
return nil
257+
}

pkg/cli.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ const (
3232
ScratchDir = "/tmp/scratch"
3333
DestinationDir = "/tmp/destination"
3434
configDirName = "config"
35+
passwordFile = "password.txt"
3536

3637
ResticEnvs = "restic-envs"
3738
ResticRegistry = "stashed"
3839
ResticImage = "restic"
3940
ResticTag = "latest"
41+
cmdKubectl = "kubectl"
4042
)
4143

4244
type cliLocalDirectories struct {

0 commit comments

Comments
 (0)