Skip to content

Commit df5707a

Browse files
committed
Add feature: restore
This commit provides a feature: restore - which will be helpful to restore data from a backup. Some part of restoring solution require kubectl binary to be installed and configured .kube/config file as well. With that way, we don't need to copy the restore file/dir to the pod or create new pod with new PV or mount same PVC into new pod, which might be rejected by some PV drivers. Also mounting local host directory to the OpenShift cluster might be prohibited in some deployments (especially in public deployments where user is not an admin), so that is not a good idea to use. Change-Id: I7c44bb18dcbd55895df30d9ee5add1d6c42a7625
1 parent 6c209cf commit df5707a

File tree

13 files changed

+450
-49
lines changed

13 files changed

+450
-49
lines changed

cli/cmd/backup.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ package cmd
2323
import (
2424
"errors"
2525
"os"
26+
"path/filepath"
2627

2728
cliutils "github.com/softwarefactory-project/sf-operator/cli/cmd/utils"
2829
controllers "github.com/softwarefactory-project/sf-operator/controllers"
@@ -33,8 +34,11 @@ import (
3334
)
3435

3536
const (
36-
zuulBackupPod = "zuul-scheduler-0"
37-
dbBackupPod = "mariadb-0"
37+
zuulBackupPod = "zuul-scheduler-0"
38+
dbBackupPod = "mariadb-0"
39+
DBBackupPath = "mariadb/db-zuul.sql"
40+
ZuulBackupPath = "zuul/zuul.keys"
41+
SecretsBackupPath = "secrets/"
3842
)
3943

4044
// Short legend what to backup
@@ -49,17 +53,15 @@ const (
4953
// This key is added as authorized keys on external system
5054
// - zuul-keystore-password - this is the key used to encrypt/decrypt key pairs stored into zookeeper
5155
// - zuul-auth-secret - this contains the secret for the zuul-client connection
52-
// - mariadb-root-password - this contains MariaDB root password
5356

54-
var secretsToBackup = []string{
57+
var SecretsToBackup = []string{
5558
"ca-cert",
5659
"zookeeper-client-tls",
5760
"zookeeper-server-tls",
5861
"nodepool-builder-ssh-key",
5962
"zuul-ssh-key",
6063
"zuul-keystore-password",
6164
"zuul-auth-secret",
62-
"mariadb-root-password",
6365
}
6466

6567
func prepareBackup(kmd *cobra.Command, backupDir string) (string, *kubernetes.Clientset, string) {
@@ -80,10 +82,10 @@ func prepareBackup(kmd *cobra.Command, backupDir string) (string, *kubernetes.Cl
8082
func createSecretBackup(ns string, backupDir string, kubeClientSet *kubernetes.Clientset) {
8183
ctrl.Log.Info("Creating secrets backup...")
8284

83-
secretsDir := backupDir + "/secrets"
85+
secretsDir := backupDir + "/" + SecretsBackupPath
8486
cliutils.CreateDirectory(secretsDir, 0755)
8587

86-
for _, sec := range secretsToBackup {
88+
for _, sec := range SecretsToBackup {
8789
secret := cliutils.GetSecretByName(sec, ns, kubeClientSet)
8890

8991
// convert secret content to string (was bytes)
@@ -120,10 +122,12 @@ func createZuulKeypairBackup(ns string, backupDir string, kubeClientSet *kuberne
120122

121123
pod := cliutils.GetPodByName(zuulBackupPod, ns, kubeClientSet)
122124

123-
zuulBackupDir := backupDir + "/zuul/"
125+
// https://zuul-ci.org/docs/zuul/latest/client.html
126+
zuulBackupPath := backupDir + "/" + ZuulBackupPath
127+
zuulBackupDir := filepath.Dir(zuulBackupPath)
124128
cliutils.CreateDirectory(zuulBackupDir, 0755)
125129
backupZuulCMD := []string{
126-
"zuul",
130+
"zuul-admin",
127131
"export-keys",
128132
"/tmp/zuul-backup",
129133
}
@@ -143,7 +147,7 @@ func createZuulKeypairBackup(ns string, backupDir string, kubeClientSet *kuberne
143147
commandBuffer := cliutils.RunRemoteCmd(kubeContext, ns, pod.Name, controllers.ZuulSchedulerIdent, backupZuulPrintCMD)
144148

145149
// write stdout to file
146-
cliutils.WriteContentToFile(zuulBackupDir+"zuul.keys", commandBuffer.Bytes(), 0640)
150+
cliutils.WriteContentToFile(zuulBackupPath, commandBuffer.Bytes(), 0640)
147151

148152
// Remove key file from the pod
149153
cliutils.RunRemoteCmd(kubeContext, ns, pod.Name, controllers.ZuulSchedulerIdent, backupZuulRemoveCMD)
@@ -156,7 +160,9 @@ func createMySQLBackup(ns string, backupDir string, kubeClientSet *kubernetes.Cl
156160
ctrl.Log.Info("Doing DB backup...")
157161

158162
// create MariaDB dir
159-
mariaDBBackupDir := backupDir + "/mariadb/"
163+
mariadbBackupPath := backupDir + "/" + DBBackupPath
164+
mariaDBBackupDir := filepath.Dir(mariadbBackupPath)
165+
160166
cliutils.CreateDirectory(mariaDBBackupDir, 0755)
161167

162168
pod := cliutils.GetPodByName(dbBackupPod, ns, kubeClientSet)
@@ -174,7 +180,7 @@ func createMySQLBackup(ns string, backupDir string, kubeClientSet *kubernetes.Cl
174180
commandBuffer := cliutils.RunRemoteCmd(kubeContext, ns, pod.Name, controllers.MariaDBIdent, backupZuulCMD)
175181

176182
// write stdout to file
177-
cliutils.WriteContentToFile(mariaDBBackupDir+"db-zuul.sql", commandBuffer.Bytes(), 0640)
183+
cliutils.WriteContentToFile(mariadbBackupPath, commandBuffer.Bytes(), 0640)
178184
ctrl.Log.Info("Finished doing DBs backup!")
179185
}
180186

cli/cmd/restore.go

Lines changed: 165 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,190 @@ package cmd
2121
*/
2222

2323
import (
24+
"context"
2425
"errors"
26+
"fmt"
2527
"os"
28+
"path/filepath"
29+
30+
cliutils "github.com/softwarefactory-project/sf-operator/cli/cmd/utils"
31+
controllers "github.com/softwarefactory-project/sf-operator/controllers"
2632

2733
"github.com/spf13/cobra"
34+
35+
corev1 "k8s.io/api/core/v1"
36+
"k8s.io/client-go/kubernetes"
2837
ctrl "sigs.k8s.io/controller-runtime"
2938
)
3039

40+
func prepareRestore(kmd *cobra.Command) (string, *kubernetes.Clientset, string) {
41+
42+
cliCtx, err := cliutils.GetCLIContext(kmd)
43+
if err != nil {
44+
ctrl.Log.Error(err, "Error initializing CLI:")
45+
os.Exit(1)
46+
}
47+
48+
kubeContext := cliCtx.KubeContext
49+
_, kubeClientSet := cliutils.GetClientset(kubeContext)
50+
return cliCtx.Namespace, kubeClientSet, kubeContext
51+
}
52+
53+
func restoreSecret(ns string, backupDir string, kubeContext string) {
54+
ctrl.Log.Info("Restoring secrets...")
55+
56+
env := cliutils.ENV{
57+
Cli: cliutils.CreateKubernetesClientOrDie(kubeContext),
58+
Ctx: context.TODO(),
59+
Ns: ns,
60+
}
61+
62+
for _, sec := range SecretsToBackup {
63+
pathToSecret := backupDir + "/" + SecretsBackupPath + "/" + sec + ".yaml"
64+
secretContent := cliutils.ReadYAMLToMapOrDie(pathToSecret)
65+
66+
var secret corev1.Secret
67+
if cliutils.GetMOrDie(&env, sec, &secret) {
68+
secretMap := secretContent["data"].(map[string]interface{})
69+
for key, value := range secretMap {
70+
stringValue, ok := value.(string)
71+
if !ok {
72+
ctrl.Log.Error(errors.New("can not convert secret data value to string"),
73+
"Can not restore secret"+sec)
74+
os.Exit(1)
75+
}
76+
secret.Data[key] = []byte(stringValue)
77+
}
78+
} else {
79+
ctrl.Log.Error(errors.New("the secret does not exist"),
80+
"The secret: "+sec+" should be available before continuing restore")
81+
os.Exit(1)
82+
}
83+
84+
cliutils.UpdateROrDie(&env, &secret)
85+
}
86+
87+
}
88+
89+
func restoreDB(ns string, backupDir string, kubeClientSet *kubernetes.Clientset, kubeContext string) {
90+
ctrl.Log.Info("Restoring DB...")
91+
pod := cliutils.GetPodByName(dbBackupPod, ns, kubeClientSet)
92+
93+
kubectlPath := cliutils.GetKubectlPath()
94+
dropDBCMD := []string{
95+
"mysql",
96+
"-e DROP DATABASE zuul;",
97+
}
98+
cliutils.RunRemoteCmd(kubeContext, ns, pod.Name, controllers.MariaDBIdent, dropDBCMD)
99+
100+
mariadbBackupPath := backupDir + "/" + DBBackupPath
101+
102+
// Below command is executing something like:
103+
// cat backup/mariadb/db-zuul.sql | kubectl -n sf exec -it mariadb-0 -c mariadb -- sh -c "mysql -h0"
104+
// but in that case, we need to do it via system kubernetes client.
105+
executeCommand := fmt.Sprintf(
106+
"cat %s | %s -n %s exec -it %s -c %s -- sh -c \"mysql -h0\"",
107+
mariadbBackupPath, kubectlPath, ns, pod.Name, controllers.MariaDBIdent,
108+
)
109+
110+
cliutils.ExecuteKubectlClient(ns, pod.Name, controllers.MariaDBIdent, executeCommand)
111+
112+
ctrl.Log.Info("Finished restoring DB from backup!")
113+
}
114+
func restoreZuul(ns string, backupDir string, kubeClientSet *kubernetes.Clientset, kubeContext string) {
115+
ctrl.Log.Info("Restoring Zuul...")
116+
pod := cliutils.GetPodByName(zuulBackupPod, ns, kubeClientSet)
117+
118+
// ensure that pod does not have any restore file
119+
restoreZuulRemoveCMD := []string{
120+
"rm",
121+
"-rf",
122+
"/tmp/zuul-import",
123+
}
124+
cliutils.RunRemoteCmd(kubeContext, ns, pod.Name, controllers.ZuulSchedulerIdent, restoreZuulRemoveCMD)
125+
126+
// create empty directory for future restore
127+
restoreZuulCreateDirCMD := []string{
128+
"mkdir",
129+
"-p",
130+
"/tmp/zuul-import",
131+
}
132+
cliutils.RunRemoteCmd(kubeContext, ns, pod.Name, controllers.ZuulSchedulerIdent, restoreZuulCreateDirCMD)
133+
134+
// copy the Zuul private keys backup to pod
135+
// tar cf - -C /tmp/backup/zuul zuul.keys | /usr/bin/kubectl exec -i -n sf zuul-scheduler-0 -c zuul-scheduler -- tar xf - -C /tmp
136+
kubectlPath := cliutils.GetKubectlPath()
137+
basePath := filepath.Dir(backupDir + "/" + ZuulBackupPath)
138+
baseFile := filepath.Base(ZuulBackupPath)
139+
executeCommand := fmt.Sprintf(
140+
"tar cf - -C %s %s | %s exec -i -n %s %s -c %s -- tar xf - -C /tmp/zuul-import",
141+
basePath, baseFile, kubectlPath, ns, pod.Name, controllers.ZuulSchedulerIdent,
142+
)
143+
ctrl.Log.Info("Executing " + executeCommand)
144+
145+
cliutils.ExecuteKubectlClient(ns, pod.Name, controllers.ZuulSchedulerIdent, executeCommand)
146+
147+
// https://zuul-ci.org/docs/zuul/latest/client.html
148+
restoreZuulCMD := []string{
149+
"zuul-admin",
150+
"import-keys",
151+
"--force",
152+
"/tmp/zuul-import/" + baseFile,
153+
}
154+
155+
// Execute command for restore
156+
cliutils.RunRemoteCmd(kubeContext, ns, pod.Name, controllers.ZuulSchedulerIdent, restoreZuulCMD)
157+
158+
// remove after all
159+
cliutils.RunRemoteCmd(kubeContext, ns, pod.Name, controllers.ZuulSchedulerIdent, restoreZuulRemoveCMD)
160+
161+
ctrl.Log.Info("Finished doing Zuul private keys restore!")
162+
163+
}
164+
31165
func restoreCmd(kmd *cobra.Command, args []string) {
32-
err := errors.New("backup is not supported yet")
33-
ctrl.Log.Error(err, "Command error")
34-
os.Exit(1)
166+
167+
// NOTE: Solution for restoring DB and Zuul require kubectl binary to be installed and configured .kube/config
168+
// file as well.
169+
// With that way, we don't need to copy the restore file/dir to the pod or create new pod with new PV or
170+
// mount same PVC into new pod, which might be rejected by some PV drivers. Also mounting local host directory
171+
// to the OpenShift cluster might be prohibited in some deployments (especially in public deployments where
172+
// user is not an admin), so that is not a good idea to use.
173+
174+
backupDir, _ := kmd.Flags().GetString("backup_dir")
175+
176+
if backupDir == "" {
177+
ctrl.Log.Error(errors.New("not enough parameters"),
178+
"The '--backup-dir' parameter needs to be set")
179+
os.Exit(1)
180+
181+
}
182+
183+
// prepare to make restore
184+
ns, kubeClientSet, kubeContext := prepareRestore(kmd)
185+
186+
if ns == "" {
187+
ctrl.Log.Info("You did not specify the namespace!")
188+
os.Exit(1)
189+
}
190+
191+
restoreZuul(ns, backupDir, kubeClientSet, kubeContext)
192+
restoreSecret(ns, backupDir, kubeContext)
193+
restoreDB(ns, backupDir, kubeClientSet, kubeContext)
194+
35195
}
36196

37197
func MkRestoreCmd() *cobra.Command {
38198

39199
var (
200+
backupDir string
40201
restoreCmd = &cobra.Command{
41202
Use: "restore",
42203
Short: "Restore a deployment to a previous backup",
43-
Long: `This isn't implemented yet, this subcommand is a placeholder.`,
44204
Run: restoreCmd,
45205
}
46206
)
207+
restoreCmd.Flags().StringVar(&backupDir, "backup_dir", "", "The path to the dir where backup is located")
47208

48209
return restoreCmd
49210
}

cli/cmd/utils/utils.go

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"context"
2323
"errors"
2424
"fmt"
25+
"gopkg.in/yaml.v3"
2526
"io/fs"
2627
"os"
2728
"os/exec"
@@ -381,29 +382,6 @@ func ConvertMapOfBytesToMapOfStrings(contentMap map[string][]byte) map[string]st
381382
return strMap
382383
}
383384

384-
func getSecrets(ns string, kubeClientSet *kubernetes.Clientset) *apiv1.SecretList {
385-
secrets, err := kubeClientSet.CoreV1().Secrets(ns).List(context.TODO(), metav1.ListOptions{})
386-
if err != nil {
387-
ctrl.Log.Error(err, "Can not get secrets!")
388-
os.Exit(1)
389-
}
390-
return secrets
391-
}
392-
393-
func GetSecretValue(ns string, kubeClientSet *kubernetes.Clientset, secretName string) *string {
394-
secrets := getSecrets(ns, kubeClientSet)
395-
if secrets != nil && len(secrets.Items) > 0 {
396-
for _, secret := range secrets.Items {
397-
if secret.ObjectMeta.Name == secretName {
398-
strMap := ConvertMapOfBytesToMapOfStrings(secret.Data)
399-
secretValue := strMap[secretName]
400-
return &secretValue
401-
}
402-
}
403-
}
404-
return nil
405-
}
406-
407385
func GetClientset(kubeContext string) (*rest.Config, *kubernetes.Clientset) {
408386
restConfig := controllers.GetConfigContextOrDie(kubeContext)
409387
kubeClientset, err := kubernetes.NewForConfig(restConfig)
@@ -459,3 +437,40 @@ func GetSecretByName(secretName string, ns string, kubeClientSet *kubernetes.Cli
459437
}
460438
return secret
461439
}
440+
441+
func ReadYAMLToMapOrDie(filePath string) map[string]interface{} {
442+
readFile, _ := GetFileContent(filePath)
443+
secretContent := make(map[string]interface{})
444+
err := yaml.Unmarshal(readFile, &secretContent)
445+
if err != nil {
446+
ctrl.Log.Error(err, "Problem on reading the file content")
447+
}
448+
if len(secretContent) == 0 {
449+
ctrl.Log.Error(errors.New("file is empty"), "The file is empty or it does not exist!")
450+
os.Exit(1)
451+
}
452+
return secretContent
453+
}
454+
455+
func GetKubectlPath() string {
456+
kubectlPath, err := exec.LookPath("kubectl")
457+
if err != nil {
458+
ctrl.Log.Error(errors.New("no kubectl binary"),
459+
"No 'kubectl' binary found. Please install the 'kubectl' binary before attempting a restore")
460+
os.Exit(1)
461+
}
462+
return kubectlPath
463+
}
464+
465+
func ExecuteKubectlClient(ns string, podName string, containerName string, executeCommand string) {
466+
cmd := exec.Command("sh", "-c", executeCommand)
467+
cmd.Stdin = os.Stdin
468+
cmd.Stdout = os.Stdout
469+
470+
err := cmd.Run()
471+
if err != nil {
472+
ctrl.Log.Error(err, "There is an issue on executing command: "+executeCommand)
473+
os.Exit(1)
474+
}
475+
476+
}

doc/operator/getting_started.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ In order to install the SF Operator on OpenShift, you will need:
1414
1. [OLM](https://olm.operatorframework.io/) running on your cluster. For most flavors of OpenShift [this is already the case](https://docs.openshift.com/container-platform/4.13/operators/understanding/olm/olm-understanding-olm.html#olm-overview_olm-understanding-olm).
1515
1. The community operators CatalogSource, to handle operator dependencies for SF-Operator. For most standard installations of OLM, [this CatalogSource is already installed](https://operatorhub.io/how-to-install-an-operator#How-do-I-get-Operator-Lifecycle-Manager?).
1616
1. A valid kubeconfig file, for a user with enough permissions to create a CatalogSource and a Subscription Custom Resources, on the `olm` and `operators` namespaces respectively.
17-
1. The [kubectl utility](https://kubernetes.io/docs/tasks/tools/#kubectl), to apply and create new resources on the OpenShift cluster.
17+
1. The [kubectl utility](https://kubernetes.io/docs/tasks/tools/#kubectl), to apply and create new resources on the OpenShift cluster. It is also required to use `restore` functionality.
1818

1919
## Installing the operator
2020

@@ -97,4 +97,4 @@ TBD
9797

9898
## Next steps
9999

100-
Your next step is to [deploy a Zuul-based CI with the operator](../deployment/getting_started.md).
100+
Your next step is to [deploy a Zuul-based CI with the operator](../deployment/getting_started.md).

0 commit comments

Comments
 (0)