snapshots #5

Merged
jandre merged 37 commits from snapshots into master 2023-04-24 07:00:47 +00:00
4 changed files with 215 additions and 41 deletions
Showing only changes of commit 7f227c1ec4 - Show all commits

View File

@ -1,5 +1,5 @@
# Build a small image # Build a small image
FROM alpine:3.17 FROM --platform=linux/amd64 alpine:3
RUN apk add --no-cache su-exec restic postgresql-client RUN apk add --no-cache su-exec restic postgresql-client
COPY ./bin/formolcli /usr/local/bin COPY ./bin/formolcli /usr/local/bin

View File

@ -17,7 +17,7 @@ vet:
.PHONY: docker-build .PHONY: docker-build
docker-build: formolcli docker-build: formolcli
buildah bud --disable-compression --format=docker --platform $(GOOS)/$(GOARCH) --manifest $(IMG) Dockerfile.$(GOARCH) buildah bud --tag $(IMG) Dockerfile.$(GOARCH)
.PHONY: docker-push .PHONY: docker-push
docker-push: docker-build docker-push: docker-build

View File

@ -4,11 +4,14 @@ import (
"context" "context"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"os" "os"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log"
"strings"
"time"
formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1"
) )
@ -23,7 +26,6 @@ type BackupSessionReconciler struct {
func (r *BackupSessionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { func (r *BackupSessionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
r.Log = log.FromContext(ctx) r.Log = log.FromContext(ctx)
r.Context = ctx r.Context = ctx
r.Log.V(0).Info("Enter Reconcile with req", "req", req)
backupSession := formolv1alpha1.BackupSession{} backupSession := formolv1alpha1.BackupSession{}
err := r.Get(ctx, req.NamespacedName, &backupSession) err := r.Get(ctx, req.NamespacedName, &backupSession)
@ -39,70 +41,79 @@ func (r *BackupSessionReconciler) Reconcile(ctx context.Context, req ctrl.Reques
r.Log.V(0).Info("No task has been assigned yet. Wait for the next update...") r.Log.V(0).Info("No task has been assigned yet. Wait for the next update...")
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
// backupConf := formolv1alpha1.BackupConfiguration{} backupConf := formolv1alpha1.BackupConfiguration{}
// err := r.Get(ctx, client.ObjectKey { err = r.Get(ctx, client.ObjectKey{
// Namespace: backupSession.Spec.Ref.Namespace, Namespace: backupSession.Spec.Ref.Namespace,
// Name: backupSession.Spec.Ref.Name, Name: backupSession.Spec.Ref.Name,
// }, &backupConf) }, &backupConf)
// if err != nil { if err != nil {
// if errors.IsNotFound(err) { if errors.IsNotFound(err) {
// return ctrl.Result{}, nil return ctrl.Result{}, nil
// } }
// return ctrl.Result{}, err return ctrl.Result{}, err
// } }
targetName := os.Getenv(formolv1alpha1.TARGET_NAME) targetName := os.Getenv(formolv1alpha1.TARGET_NAME)
// we don't want a copy because we will modify and update it. // we don't want a copy because we will modify and update it.
currentTargetStatus := &(backupSession.Status.Targets[len(backupSession.Status.Targets)-1]) currentTargetStatus := &(backupSession.Status.Targets[len(backupSession.Status.Targets)-1])
currentTarget := backupConf.Spec.Targets[len(backupSession.Status.Targets)-1]
var result error
if currentTargetStatus.TargetName == targetName { if currentTargetStatus.TargetName == targetName {
// The current task is for us // The current task is for us
var newSessionState formolv1alpha1.SessionState
switch currentTargetStatus.SessionState { switch currentTargetStatus.SessionState {
case formolv1alpha1.New: case formolv1alpha1.New:
r.Log.V(0).Info("New session, move to Initializing state") r.Log.V(0).Info("New session, move to Initializing state")
currentTargetStatus.SessionState = formolv1alpha1.Init newSessionState = formolv1alpha1.Init
err := r.Status().Update(ctx, &backupSession)
if err != nil {
r.Log.Error(err, "unable to update BackupSession status")
}
return ctrl.Result{}, err
case formolv1alpha1.Init: case formolv1alpha1.Init:
r.Log.V(0).Info("Start to run the backup initializing steps is any") r.Log.V(0).Info("Start to run the backup initializing steps is any")
// Runs the Steps functions in chroot env // Runs the Steps functions in chroot env
result := formolv1alpha1.Running if result = r.runInitializeBackupSteps(currentTarget); result != nil {
currentTargetStatus.SessionState = result r.Log.Error(result, "unable to run the initialization steps")
err := r.Status().Update(ctx, &backupSession) newSessionState = formolv1alpha1.Finalize
if err != nil { } else {
r.Log.Error(err, "unable to update BackupSession status") newSessionState = formolv1alpha1.Running
} }
return ctrl.Result{}, err
case formolv1alpha1.Running: case formolv1alpha1.Running:
r.Log.V(0).Info("Running state. Do the backup") r.Log.V(0).Info("Running state. Do the backup")
// Actually do the backup with restic // Actually do the backup with restic
currentTargetStatus.SessionState = formolv1alpha1.Finalize backupPaths := strings.Split(os.Getenv(formolv1alpha1.BACKUP_PATHS), string(os.PathListSeparator))
err := r.Status().Update(ctx, &backupSession) if backupResult, result := r.backupPaths(backupSession.Name, backupPaths); result != nil {
if err != nil { r.Log.Error(result, "unable to backup paths", "target name", targetName, "paths", backupPaths)
r.Log.Error(err, "unable to update BackupSession status") } else {
r.Log.V(0).Info("Backup of the paths is over", "target name", targetName, "paths", backupPaths,
"snapshotID", backupResult.SnapshotId, "duration", backupResult.Duration)
currentTargetStatus.SnapshotId = backupResult.SnapshotId
currentTargetStatus.Duration = &metav1.Duration{Duration: time.Now().Sub(currentTargetStatus.StartTime.Time)}
} }
return ctrl.Result{}, err newSessionState = formolv1alpha1.Finalize
case formolv1alpha1.Finalize: case formolv1alpha1.Finalize:
r.Log.V(0).Info("Backup is over. Run the finalize steps is any") r.Log.V(0).Info("Backup is over. Run the finalize steps is any")
// Runs the finalize Steps functions in chroot env // Runs the finalize Steps functions in chroot env
if currentTargetStatus.SnapshotId == "" { if result = r.runFinalizeBackupSteps(currentTarget); result != nil {
currentTargetStatus.SessionState = formolv1alpha1.Failure r.Log.Error(err, "unable to run finalize steps")
} else {
currentTargetStatus.SessionState = formolv1alpha1.Success
} }
if currentTargetStatus.SnapshotId == "" {
newSessionState = formolv1alpha1.Failure
} else {
newSessionState = formolv1alpha1.Success
}
case formolv1alpha1.Success:
r.Log.V(0).Info("Backup is over")
case formolv1alpha1.Failure:
r.Log.V(0).Info("Backup is over")
}
if newSessionState != "" {
currentTargetStatus.SessionState = newSessionState
err := r.Status().Update(ctx, &backupSession) err := r.Status().Update(ctx, &backupSession)
if err != nil { if err != nil {
r.Log.Error(err, "unable to update BackupSession status") r.Log.Error(err, "unable to update BackupSession status")
} }
return ctrl.Result{}, err return ctrl.Result{}, err
case formolv1alpha1.Success:
case formolv1alpha1.Failure:
r.Log.V(0).Info("Backup is over")
} }
} }
return ctrl.Result{}, nil return ctrl.Result{}, result
} }
// SetupWithManager sets up the controller with the Manager. // SetupWithManager sets up the controller with the Manager.

View File

@ -1,12 +1,39 @@
package controllers package controllers
import ( import (
"bufio"
"bytes"
"encoding/json"
formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1"
"io"
"io/ioutil"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"os" "os"
"os/exec"
"path/filepath"
"regexp"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"strconv"
) )
var (
REPOSITORY string
PASSWORD_FILE string
AWS_ACCESS_KEY_ID string
AWS_SECRET_ACCESS_KEY string
)
const (
RESTIC_EXEC = "/usr/bin/restic"
)
func init() {
REPOSITORY = os.Getenv(formolv1alpha1.RESTIC_REPOSITORY)
PASSWORD_FILE = os.Getenv(formolv1alpha1.RESTIC_PASSWORD)
AWS_ACCESS_KEY_ID = os.Getenv(formolv1alpha1.AWS_ACCESS_KEY_ID)
AWS_SECRET_ACCESS_KEY = os.Getenv(formolv1alpha1.AWS_SECRET_ACCESS_KEY)
}
func (r *BackupSessionReconciler) getSecretData(name string) map[string][]byte { func (r *BackupSessionReconciler) getSecretData(name string) map[string][]byte {
secret := corev1.Secret{} secret := corev1.Secret{}
namespace := os.Getenv(formolv1alpha1.POD_NAMESPACE) namespace := os.Getenv(formolv1alpha1.POD_NAMESPACE)
@ -88,12 +115,14 @@ func (r *BackupSessionReconciler) getFuncVars(function formolv1alpha1.Function,
r.getFuncEnv(vars, function.Spec.Env) r.getFuncEnv(vars, function.Spec.Env)
} }
func (r *BackupSessionReconciler) runInitBackupSteps(target formolv1alpha1.Target) error { func (r *BackupSessionReconciler) runBackupSteps(initializeSteps bool, target formolv1alpha1.Target) error {
namespace := os.Getenv(formolv1alpha1.POD_NAMESPACE) namespace := os.Getenv(formolv1alpha1.POD_NAMESPACE)
r.Log.V(0).Info("start to run the backup initializing steps it any") r.Log.V(0).Info("start to run the backup initializing steps it any")
// For every container listed in the target, run the initialization steps
for _, container := range target.Containers { for _, container := range target.Containers {
// Runs the steps one after the other
for _, step := range container.Steps { for _, step := range container.Steps {
if step.Finalize != nil && *step.Finalize == true { if step.Finalize != nil && *step.Finalize == true && initializeSteps {
continue continue
} }
function := formolv1alpha1.Function{} function := formolv1alpha1.Function{}
@ -107,7 +136,141 @@ func (r *BackupSessionReconciler) runInitBackupSteps(target formolv1alpha1.Targe
vars := make(map[string]string) vars := make(map[string]string)
r.getFuncVars(function, vars) r.getFuncVars(function, vars)
// Loop through the function.Spec.Command arguments to replace ${ARG}|$(ARG)|$ARG
// with the environment variable value
pattern := regexp.MustCompile(`^\$(\{(?P<env>\w+)\}|^\$\((?P<env>\w+)\)|(?P<env>\w+))$`)
for i, arg := range function.Spec.Command[1:] {
if pattern.MatchString(arg) {
arg = pattern.ReplaceAllString(arg, "$env")
function.Spec.Command[i] = vars[arg]
}
}
if err := r.runTargetContainerChroot(function.Spec.Command[0],
function.Spec.Command[1:]...); err != nil {
r.Log.Error(err, "unable to run command", "command", function.Spec.Command)
return err
}
} }
} }
return nil return nil
} }
func (r *BackupSessionReconciler) runFinalizeBackupSteps(target formolv1alpha1.Target) error {
return r.runBackupSteps(true, target)
}
func (r *BackupSessionReconciler) runInitializeBackupSteps(target formolv1alpha1.Target) error {
return r.runBackupSteps(true, target)
}
func (r *BackupSessionReconciler) runTargetContainerChroot(runCmd string, args ...string) error {
env := regexp.MustCompile(`/proc/[0-9]+/env`)
if err := filepath.Walk("/proc", func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
// Skip process 1 and ourself
if info.IsDir() && (info.Name() == "1" || info.Name() == strconv.Itoa(os.Getpid())) {
return filepath.SkipDir
}
// Found an environ file. Start looking for TARGETCONTAINER_TAG
if env.MatchString(path) {
r.Log.V(0).Info("Looking for tag", "file", path, "TARGETCONTAINER_TAG", formolv1alpha1.TARGETCONTAINER_TAG)
content, err := ioutil.ReadFile(path)
// cannot read environ file. not the process we want to backup
if err != nil {
r.Log.Error(err, "unable to read file", "file", path)
return filepath.SkipDir
}
// Loops over the process environement variable looking for TARGETCONTAINER_TAG
for _, env := range bytes.Split(content, []byte{'\000'}) {
matched, err := regexp.Match(formolv1alpha1.TARGETCONTAINER_TAG, env)
if err != nil {
r.Log.Error(err, "unable to regexp", "env", string(env))
return err
}
if matched {
// Found the right process. Now run the command in its 'root'
r.Log.V(0).Info("Found the tag", "file", path)
root := filepath.Join(filepath.Dir(path), "root")
if _, err := filepath.EvalSymlinks(root); err != nil {
r.Log.Error(err, "cannot EvalSymlink.")
return err
}
r.Log.V(0).Info("running cmd in chroot", "path", root)
cmd := exec.Command("chroot", append([]string{root, runCmd}, args...)...)
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
_ = cmd.Start()
scanner := bufio.NewScanner(io.MultiReader(stdout, stderr))
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
r.Log.V(0).Info("cmd output", "output", scanner.Text())
}
return cmd.Wait()
}
}
}
return nil
}); err != nil {
r.Log.Error(err, "cannot walk /proc")
return err
}
return nil
}
func (r *BackupSessionReconciler) checkRepo(repo string) error {
r.Log.V(0).Info("Checking repo", "repo", repo)
if err := exec.Command(RESTIC_EXEC, "unlock", "-r", repo).Run(); err != nil {
r.Log.Error(err, "unable to unlock repo", "repo", repo)
return err
}
output, err := exec.Command(RESTIC_EXEC, "check", "-r", repo).CombinedOutput()
if err != nil {
r.Log.V(0).Info("Initializing new repo", "repo", repo)
output, err = exec.Command(RESTIC_EXEC, "init", "-r", repo).CombinedOutput()
if err != nil {
r.Log.Error(err, "something went wrong during repo init", "output", output)
}
}
return err
}
type BackupResult struct {
SnapshotId string
Duration float64
}
func (r *BackupSessionReconciler) backupPaths(tag string, paths []string) (result BackupResult, err error) {
if err = r.checkRepo(REPOSITORY); err != nil {
r.Log.Error(err, "unable to setup repo", "repo", REPOSITORY)
return
}
r.Log.V(0).Info("backing up paths", "paths", paths)
cmd := exec.Command(RESTIC_EXEC, append([]string{"backup", "--json", "--tag", tag, "-r", REPOSITORY}, paths...)...)
stdout, _ := cmd.StdoutPipe()
stderr, _ := cmd.StderrPipe()
_ = cmd.Start()
scanner := bufio.NewScanner(io.MultiReader(stdout, stderr))
scanner.Split(bufio.ScanLines)
var data map[string]interface{}
for scanner.Scan() {
if err := json.Unmarshal(scanner.Bytes(), &data); err != nil {
r.Log.Error(err, "unable to unmarshal json", "data", scanner.Text())
continue
}
switch data["message_type"].(string) {
case "summary":
result.SnapshotId = data["snapshot_id"].(string)
result.Duration = data["total_duration"].(float64)
case "status":
r.Log.V(0).Info("backup running", "percent done", data["percent_done"].(float64))
}
}
err = cmd.Wait()
return
}