diff --git a/.gitignore b/.gitignore index c0e88cc..6a8579a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +*~ +bin/ go.sum -bin diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dd965f7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "formol"] + path = formol + url = ../formol diff --git a/Dockerfile b/Dockerfile index 15086dc..314c5bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,23 @@ # Build a small image -FROM arm64v8/alpine:3.14 +FROM --platform=${BUILDPLATFORM} golang:alpine3.17 AS builder +ARG TARGETOS +ARG TARGETARCH +ARG TARGETPLATFORM -RUN apk add --no-cache su-exec restic postgresql-client -COPY bin/formolcli /usr/local/bin +WORKDIR /go/src +COPY go.mod go.mod +COPY go.sum go.sum +COPY formol/ formol/ +RUN go mod download +COPY main.go main.go +COPY cmd/ cmd/ +COPY standalone/ standalone/ +COPY controllers/ controllers/ +RUN GO111MODULE=on CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o bin/formolcli main.go + +FROM --platform=${TARGETPLATFORM} alpine:3 +RUN apk add --no-cache su-exec restic +COPY --from=builder /go/src/bin/formolcli /usr/local/bin # Command to run ENTRYPOINT ["/usr/local/bin/formolcli"] diff --git a/Dockerfile.deployment b/Dockerfile.deployment deleted file mode 100644 index 3e7b6e8..0000000 --- a/Dockerfile.deployment +++ /dev/null @@ -1,39 +0,0 @@ -FROM golang:alpine AS builder - -# Set necessary environmet variables needed for our image -ENV GO111MODULE=on \ - CGO_ENABLED=0 \ - GOOS=linux \ - GOARCH=arm \ - GOARM=7 - -# Move to working directory /build -WORKDIR /build - -# Copy and download dependency using go mod -COPY src/go.mod . -COPY src/go.sum . -RUN go mod download - -# Copy the code into the container -COPY src . - -# Build the application -RUN go build -o formolcli . - -# Move to /dist directory as the place for resulting binary folder -WORKDIR /dist - -# Copy binary from build to main folder -RUN cp /build/formolcli . - -# Build a small image -FROM arm32v7/alpine:3.12 - -RUN apk add --no-cache restic postgresql-client -#COPY bin/restic /usr/local/bin -COPY --from=builder /dist/formolcli /usr/local/bin - -# Command to run -ENTRYPOINT ["/usr/local/bin/formolcli"] -CMD ["--help"] diff --git a/LICENSE b/LICENSE index d645695..e69de29 100644 --- a/LICENSE +++ b/LICENSE @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/Makefile b/Makefile index c64d981..f1364a9 100644 --- a/Makefile +++ b/Makefile @@ -1,25 +1,30 @@ -.PHONY: all formolcli docker docker-build docker-push - -IMG ?= desmo999r/formolcli:latest +GOARCH ?= amd64 +GOOS ?= linux +VERSION ?= latest +IMG ?= docker.io/desmo999r/formolcli:$(VERSION) +MANIFEST = formolcli-multiarch +BINDIR = ./bin +PLATFORMS ?= linux/arm64,linux/amd64 +.PHONY: formolcli formolcli: fmt vet - GO111MODULE=on CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/formolcli main.go - -test: fmt vet - go test ./... -coverprofile cover.out + GO111MODULE=on CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(BINDIR)/formolcli main.go +.PHONY: fmt fmt: go fmt ./... +.PHONY: vet vet: go vet ./... -docker-build: - buildah bud --disable-compression --format=docker -t ${IMG} . +.PHONY: docker-build-multiarch +docker-build-multiarch: + buildah bud --manifest $(MANIFEST) --platform=$(PLATFORMS) --layers . -docker-push: - buildah push ${IMG} +.PHONY: docker-push +docker-push: + buildah manifest push --all --rm $(MANIFEST) "docker://$(IMG)" -docker: formolcli docker-build docker-push - -all: docker +.PHONY: all +all: formolcli docker-build diff --git a/README.md b/README.md new file mode 100644 index 0000000..0fa00c9 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +buildah bud --platform linux/arm64,linux/amd64 --manifest docker.io/desmo999r/formolcli:0.4.0 . diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..9bca20a --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,141 @@ +/* +Copyright © 2023 NAME HERE +*/ +package cmd + +import ( + "fmt" + "github.com/desmo999r/formolcli/controllers" + "github.com/desmo999r/formolcli/standalone" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "os" +) + +var createBackupSessionCmd = &cobra.Command{ + Use: "create", + Short: "Create a backupsession", + Run: func(cmd *cobra.Command, args []string) { + name, _ := cmd.Flags().GetString("name") + namespace, _ := cmd.Flags().GetString("namespace") + fmt.Println("create backupsession called") + standalone.CreateBackupSession(corev1.ObjectReference{ + Namespace: namespace, + Name: name, + }) + }, +} + +var backupCmd = &cobra.Command{ + Use: "backup", + Short: "Backup paths", + Run: func(cmd *cobra.Command, args []string) { + backupSessionName, _ := cmd.Flags().GetString("name") + backupSessionNamespace, _ := cmd.Flags().GetString("namespace") + targetName, _ := cmd.Flags().GetString("target-name") + standalone.BackupPaths(backupSessionName, backupSessionNamespace, targetName, args...) + }, +} + +var startRestoreSessionCmd = &cobra.Command{ + Use: "start", + Short: "Restore a restic snapshot", + Run: func(cmd *cobra.Command, args []string) { + restoreSessionName, _ := cmd.Flags().GetString("name") + restoreSessionNamespace, _ := cmd.Flags().GetString("namespace") + targetName, _ := cmd.Flags().GetString("target-name") + standalone.StartRestore(restoreSessionName, restoreSessionNamespace, targetName) + }, +} + +var startServerCmd = &cobra.Command{ + Use: "server", + Short: "Start a BackupSession / RestoreSession controller", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("starts backupsession controller") + controllers.StartServer() + }, +} + +var restoreSessionCmd = &cobra.Command{ + Use: "restoresession", + Short: "All the RestoreSession related commands", +} + +var backupSessionCmd = &cobra.Command{ + Use: "backupsession", + Short: "All the BackupSession related commands", +} + +var snapshotCmd = &cobra.Command{ + Use: "snapshot", + Short: "All the snapshot related commands", +} + +var deleteSnapshotCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a snapshot", + Run: func(cmd *cobra.Command, args []string) { + name, _ := cmd.Flags().GetString("name") + namespace, _ := cmd.Flags().GetString("namespace") + snapshotId, _ := cmd.Flags().GetString("snapshot-id") + standalone.DeleteSnapshot(namespace, name, snapshotId) + }, +} + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "formolcli", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.AddCommand(backupSessionCmd) + rootCmd.AddCommand(restoreSessionCmd) + rootCmd.AddCommand(snapshotCmd) + backupSessionCmd.AddCommand(createBackupSessionCmd) + backupSessionCmd.AddCommand(backupCmd) + restoreSessionCmd.AddCommand(startRestoreSessionCmd) + snapshotCmd.AddCommand(deleteSnapshotCmd) + rootCmd.AddCommand(startServerCmd) + createBackupSessionCmd.Flags().String("namespace", "", "The namespace of the BackupConfiguration containing the information about the backup.") + createBackupSessionCmd.Flags().String("name", "", "The name of the BackupConfiguration containing the information about the backup.") + createBackupSessionCmd.MarkFlagRequired("namespace") + createBackupSessionCmd.MarkFlagRequired("name") + backupCmd.Flags().String("target-name", "", "The name of target being restored") + backupCmd.Flags().String("namespace", "", "The namespace of the BackupConfiguration containing the information about the backup.") + backupCmd.Flags().String("name", "", "The name of the BackupConfiguration containing the information about the backup.") + backupCmd.MarkFlagRequired("namespace") + backupCmd.MarkFlagRequired("name") + backupCmd.MarkFlagRequired("target-name") + startRestoreSessionCmd.Flags().String("namespace", "", "The namespace of RestoreSession") + startRestoreSessionCmd.Flags().String("name", "", "The name of RestoreSession") + startRestoreSessionCmd.Flags().String("target-name", "", "The name of target being restored") + startRestoreSessionCmd.MarkFlagRequired("namespace") + startRestoreSessionCmd.MarkFlagRequired("name") + startRestoreSessionCmd.MarkFlagRequired("target-name") + deleteSnapshotCmd.Flags().String("snapshot-id", "", "The snapshot id to delete") + deleteSnapshotCmd.Flags().String("namespace", "", "The namespace of the BackupConfiguration containing the information about the backup.") + deleteSnapshotCmd.Flags().String("name", "", "The name of the BackupConfiguration containing the information about the backup.") + deleteSnapshotCmd.MarkFlagRequired("snapshot-id") + deleteSnapshotCmd.MarkFlagRequired("namespace") + deleteSnapshotCmd.MarkFlagRequired("name") +} diff --git a/controllers/backupsession_controller.go b/controllers/backupsession_controller.go new file mode 100644 index 0000000..1c487e0 --- /dev/null +++ b/controllers/backupsession_controller.go @@ -0,0 +1,177 @@ +package controllers + +import ( + "context" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "os" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "strings" + "time" + + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" +) + +type BackupSessionReconciler struct { + Session + backupSession formolv1alpha1.BackupSession + backupConf formolv1alpha1.BackupConfiguration +} + +func (r *BackupSessionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Log = log.FromContext(ctx) + r.Context = ctx + r.Namespace = req.NamespacedName.Namespace + r.Name = req.NamespacedName.Name + + backupSession := formolv1alpha1.BackupSession{} + err := r.Get(ctx, req.NamespacedName, &backupSession) + if err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + r.backupSession = backupSession + if len(backupSession.Status.Targets) == 0 { + // The main BackupSession controller hasn't assigned a backup task yet + // Wait a bit + r.Log.V(0).Info("No task has been assigned yet. Wait for the next update...") + return ctrl.Result{}, nil + } + backupConf := formolv1alpha1.BackupConfiguration{} + err = r.Get(ctx, client.ObjectKey{ + Namespace: backupSession.Spec.Ref.Namespace, + Name: backupSession.Spec.Ref.Name, + }, &backupConf) + if err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + r.backupConf = backupConf + + // we don't want a copy because we will modify and update it. + var target formolv1alpha1.Target + var targetStatus *formolv1alpha1.TargetStatus + var result error + targetName := os.Getenv(formolv1alpha1.TARGET_NAME) + if targetName == "" { + panic("targetName is empty. That should not happen") + } + + for i, t := range backupConf.Spec.Targets { + if t.TargetName == targetName { + target = t + targetStatus = &(backupSession.Status.Targets[i]) + break + } + } + + // Do preliminary checks with the repository + if err = r.SetResticEnv(backupConf); err != nil { + r.Log.Error(err, "unable to set restic env") + return ctrl.Result{}, err + } + + var newSessionState formolv1alpha1.SessionState + switch targetStatus.SessionState { + case formolv1alpha1.New: + // New session move to Initializing + r.Log.V(0).Info("New session. Move to Initializing state") + newSessionState = formolv1alpha1.Initializing + case formolv1alpha1.Initializing: + // Run the initializing Steps and then move to Initialized or Failure + r.Log.V(0).Info("Start to run the backup initializing steps is any") + // Runs the Steps functions in chroot env + if err := r.runInitializeSteps(target); err != nil { + r.Log.Error(err, "unable to run the initialization steps") + newSessionState = formolv1alpha1.Failure + } else { + r.Log.V(0).Info("Done with the initializing Steps. Move to Initialized state") + newSessionState = formolv1alpha1.Initialized + } + case formolv1alpha1.Running: + // Actually do the backup and move to Waiting or Failure + r.Log.V(0).Info("Running state. Do the backup") + // Actually do the backup with restic + newSessionState = formolv1alpha1.Waiting + switch target.BackupType { + case formolv1alpha1.JobKind: + if backupResult, err := r.backupJob(target); err != nil { + r.Log.Error(err, "unable to run backup job", "target", targetName) + newSessionState = formolv1alpha1.Failure + } else { + r.Log.V(0).Info("Backup Job is over", "target", targetName, "snapshotID", backupResult.SnapshotId, "duration", backupResult.Duration) + targetStatus.SnapshotId = backupResult.SnapshotId + targetStatus.Duration = &metav1.Duration{Duration: time.Now().Sub(targetStatus.StartTime.Time)} + } + case formolv1alpha1.OnlineKind: + backupPaths := strings.Split(os.Getenv(formolv1alpha1.BACKUP_PATHS), string(os.PathListSeparator)) + if backupResult, result := r.BackupPaths(backupPaths); result != nil { + r.Log.Error(result, "unable to backup paths", "target name", targetName, "paths", backupPaths) + newSessionState = formolv1alpha1.Failure + } else { + r.Log.V(0).Info("Backup of the paths is over", "target name", targetName, "paths", backupPaths, + "snapshotID", backupResult.SnapshotId, "duration", backupResult.Duration) + targetStatus.SnapshotId = backupResult.SnapshotId + targetStatus.Duration = &metav1.Duration{Duration: time.Now().Sub(targetStatus.StartTime.Time)} + } + case formolv1alpha1.SnapshotKind: + if err := r.backupSnapshot(target); err != nil { + if IsNotReadyToUse(err) { + r.Log.V(0).Info("Volume snapshots are not ready. Requeueing") + return ctrl.Result{ + Requeue: true, + }, nil + } else { + r.Log.Error(err, "unable to do snapshot backup") + return ctrl.Result{}, err + } + } + } + r.Log.V(0).Info("Backup is over and is a success. Move to Waiting state") + case formolv1alpha1.Finalize: + // Run the finalize Steps and move to Success or Failure + r.Log.V(0).Info("Backup is over. Run the finalize steps is any") + // Runs the finalize Steps functions in chroot env + if result = r.runFinalizeSteps(target); result != nil { + r.Log.Error(err, "unable to run finalize steps") + } + if target.BackupType == formolv1alpha1.SnapshotKind { + // SnapshotKind special state where we wait for the backup Job to finish + newSessionState = formolv1alpha1.WaitingForJob + } else { + if targetStatus.SnapshotId == "" { + newSessionState = formolv1alpha1.Failure + } else { + newSessionState = formolv1alpha1.Success + } + } + case formolv1alpha1.Success: + // Target backup is a success + r.Log.V(0).Info("Backup was a success") + case formolv1alpha1.Failure: + // Target backup is a failure + } + if newSessionState != "" { + targetStatus.SessionState = newSessionState + err := r.Status().Update(ctx, &backupSession) + if err != nil { + r.Log.Error(err, "unable to update BackupSession status") + } + return ctrl.Result{}, err + } + + return ctrl.Result{}, result +} + +// SetupWithManager sets up the controller with the Manager. +func (r *BackupSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&formolv1alpha1.BackupSession{}). + Complete(r) +} diff --git a/controllers/backupsession_controller_helpers.go b/controllers/backupsession_controller_helpers.go new file mode 100644 index 0000000..d94154d --- /dev/null +++ b/controllers/backupsession_controller_helpers.go @@ -0,0 +1,291 @@ +package controllers + +import ( + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" + volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "os" + "sigs.k8s.io/controller-runtime/pkg/client" + "strings" +) + +const ( + JOBTTL int32 = 7200 +) + +func (r *BackupSessionReconciler) backupJob(target formolv1alpha1.Target) (result BackupResult, err error) { + paths := []string{} + for _, container := range target.Containers { + for _, job := range container.Job { + if err = r.runFunction(*job.Backup); err != nil { + r.Log.Error(err, "unable to run job") + return + } + + } + addPath := true + for _, path := range paths { + if path == container.SharePath { + addPath = false + } + } + if addPath { + paths = append(paths, container.SharePath) + } + } + result, err = r.BackupPaths(paths) + return +} + +func (r *BackupSessionReconciler) backupSnapshot(target formolv1alpha1.Target) (e error) { + targetObject, targetPodSpec := formolv1alpha1.GetTargetObjects(target.TargetKind) + if err := r.Get(r.Context, client.ObjectKey{ + Namespace: r.Namespace, + Name: target.TargetName, + }, targetObject); err != nil { + r.Log.Error(err, "cannot get target", "target", target.TargetName) + return err + } + for _, container := range targetPodSpec.Containers { + for _, targetContainer := range target.Containers { + if targetContainer.Name == container.Name { + // Now snapshot all the container PVC that support snapshots + // then create new volumes from the snapshots + // replace the volumes in the container struct with the snapshot volumes + // use formolv1alpha1.GetVolumeMounts to get the volume mounts for the Job + // sidecar := formolv1alpha1.GetSidecar(backupConf, target) + paths, vms := formolv1alpha1.GetVolumeMounts(container, targetContainer) + if err := r.snapshotVolumes(vms, targetPodSpec); err != nil { + if IsNotReadyToUse(err) { + r.Log.V(0).Info("Some volumes are still not ready to use") + defer func() { e = &NotReadyToUseError{} }() + } else { + r.Log.Error(err, "cannot snapshot the volumes") + return err + } + } else { + r.Log.V(1).Info("Creating a Job to backup the Snapshot volumes") + sidecar := formolv1alpha1.GetSidecar(r.backupConf, target) + sidecar.Args = append([]string{"backupsession", "backup", "--namespace", r.Namespace, "--name", r.Name, "--target-name", target.TargetName}, paths...) + sidecar.VolumeMounts = vms + if env, err := r.getResticEnv(r.backupConf); err != nil { + r.Log.Error(err, "unable to get restic env") + return err + } else { + sidecar.Env = append(sidecar.Env, env...) + } + sidecar.Env = append(sidecar.Env, corev1.EnvVar{ + Name: formolv1alpha1.BACKUP_PATHS, + Value: strings.Join(paths, string(os.PathListSeparator)), + }) + job := batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.Namespace, + Name: "backupsnapshot-" + r.Name, + }, + Spec: batchv1.JobSpec{ + TTLSecondsAfterFinished: func() *int32 { ttl := JOBTTL; return &ttl }(), + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Volumes: targetPodSpec.Volumes, + Containers: []corev1.Container{ + sidecar, + }, + RestartPolicy: corev1.RestartPolicyNever, + }, + }, + }, + } + if err := r.Create(r.Context, &job); err != nil { + r.Log.Error(err, "unable to create the snapshot volumes backup job", "job", job, "container", sidecar) + return err + } + r.Log.V(1).Info("snapshot volumes backup job created", "job", job.Name) + } + } + } + } + return nil +} + +type NotReadyToUseError struct{} + +func (e *NotReadyToUseError) Error() string { + return "Snapshot is not ready to use" +} + +func IsNotReadyToUse(err error) bool { + switch err.(type) { + case *NotReadyToUseError: + return true + default: + return false + } +} + +func (r *BackupSessionReconciler) snapshotVolume(volume corev1.Volume) (*volumesnapshotv1.VolumeSnapshot, error) { + r.Log.V(0).Info("Preparing snapshot", "volume", volume.Name) + if volume.VolumeSource.PersistentVolumeClaim != nil { + pvc := corev1.PersistentVolumeClaim{} + if err := r.Get(r.Context, client.ObjectKey{ + Namespace: r.Namespace, + Name: volume.VolumeSource.PersistentVolumeClaim.ClaimName, + }, &pvc); err != nil { + r.Log.Error(err, "unable to get pvc", "volume", volume) + return nil, err + } + pv := corev1.PersistentVolume{} + if err := r.Get(r.Context, client.ObjectKey{ + Name: pvc.Spec.VolumeName, + }, &pv); err != nil { + r.Log.Error(err, "unable to get pv", "volume", pvc.Spec.VolumeName) + return nil, err + } + if pv.Spec.PersistentVolumeSource.CSI != nil { + // This volume is supported by a CSI driver. Let's see if we can snapshot it. + volumeSnapshotClassList := volumesnapshotv1.VolumeSnapshotClassList{} + if err := r.List(r.Context, &volumeSnapshotClassList); err != nil { + r.Log.Error(err, "unable to get VolumeSnapshotClass list") + return nil, err + } + for _, volumeSnapshotClass := range volumeSnapshotClassList.Items { + if volumeSnapshotClass.Driver == pv.Spec.PersistentVolumeSource.CSI.Driver { + // Check if a snapshot exist + volumeSnapshot := volumesnapshotv1.VolumeSnapshot{} + volumeSnapshotName := strings.Join([]string{"vs", r.Name, pv.Name}, "-") + + if err := r.Get(r.Context, client.ObjectKey{ + Namespace: r.Namespace, + Name: volumeSnapshotName, + }, &volumeSnapshot); errors.IsNotFound(err) { + // No snapshot found. Create a new one. + // We want to snapshot using this VolumeSnapshotClass + r.Log.V(0).Info("Create a volume snapshot", "pvc", pvc.Name) + volumeSnapshot = volumesnapshotv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.Namespace, + Name: volumeSnapshotName, + Labels: map[string]string{ + "backupsession": r.Name, + }, + }, + Spec: volumesnapshotv1.VolumeSnapshotSpec{ + VolumeSnapshotClassName: &volumeSnapshotClass.Name, + Source: volumesnapshotv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: &pvc.Name, + }, + }, + } + if err := r.Create(r.Context, &volumeSnapshot); err != nil { + r.Log.Error(err, "unable to create the snapshot", "pvc", pvc.Name) + return nil, err + } + // We just created the snapshot. We have to assume it's not yet ready and reschedule + return nil, &NotReadyToUseError{} + } else { + if err != nil { + r.Log.Error(err, "Something went very wrong here") + return nil, err + } + // The VolumeSnapshot exists. Is it ReadyToUse? + if volumeSnapshot.Status == nil || volumeSnapshot.Status.ReadyToUse == nil || *volumeSnapshot.Status.ReadyToUse == false { + r.Log.V(0).Info("Volume snapshot exists but it is not ready", "volume", volumeSnapshot.Name) + return nil, &NotReadyToUseError{} + } + r.Log.V(0).Info("Volume snapshot is ready to use", "volume", volumeSnapshot.Name) + return &volumeSnapshot, nil + } + } + } + } + } + return nil, nil +} + +func (r *BackupSessionReconciler) createVolumeFromSnapshot(vs *volumesnapshotv1.VolumeSnapshot) (backupPVCName string, err error) { + backupPVCName = strings.Replace(vs.Name, "vs", "bak", 1) + backupPVC := corev1.PersistentVolumeClaim{} + if err = r.Get(r.Context, client.ObjectKey{ + Namespace: r.Namespace, + Name: backupPVCName, + }, &backupPVC); errors.IsNotFound(err) { + // The Volume does not exist. Create it. + pv := corev1.PersistentVolume{} + pvName, _ := strings.CutPrefix(vs.Name, strings.Join([]string{"vs", r.Name}, "-")) + pvName = pvName[1:] + if err = r.Get(r.Context, client.ObjectKey{ + Name: pvName, + }, &pv); err != nil { + r.Log.Error(err, "unable to find pv", "pv", pvName) + return + } + backupPVC = corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: r.Namespace, + Name: backupPVCName, + Labels: map[string]string{ + "backupsession": r.Name, + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: &pv.Spec.StorageClassName, + //AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadOnlyMany}, + AccessModes: pv.Spec.AccessModes, + Resources: corev1.ResourceRequirements{ + Requests: pv.Spec.Capacity, + }, + DataSource: &corev1.TypedLocalObjectReference{ + APIGroup: func() *string { s := "snapshot.storage.k8s.io"; return &s }(), + Kind: "VolumeSnapshot", + Name: vs.Name, + }, + }, + } + if err = r.Create(r.Context, &backupPVC); err != nil { + r.Log.Error(err, "unable to create backup PVC", "backupPVC", backupPVC) + return + } + } + if err != nil { + r.Log.Error(err, "something went very wrong here") + } + return +} + +func (r *BackupSessionReconciler) snapshotVolumes(vms []corev1.VolumeMount, podSpec *corev1.PodSpec) (err error) { + // We snapshot/check all the volumes. If at least one of the snapshot is not ready to use. We reschedule. + for _, vm := range vms { + for i, volume := range podSpec.Volumes { + if vm.Name == volume.Name { + var vs *volumesnapshotv1.VolumeSnapshot + vs, err = r.snapshotVolume(volume) + if IsNotReadyToUse(err) { + defer func() { + err = &NotReadyToUseError{} + }() + continue + } + if err != nil { + return + } + if vs != nil { + // The snapshot is ready. We create a PVC from it. + backupPVCName, err := r.createVolumeFromSnapshot(vs) + if err != nil { + r.Log.Error(err, "unable to create volume from snapshot", "vs", vs) + return err + } + podSpec.Volumes[i].VolumeSource.PersistentVolumeClaim = &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: backupPVCName, + ReadOnly: true, + } + // The snapshot and the volume will be deleted by the Job when the backup is over + } + } + } + } + return +} diff --git a/controllers/restoresession_controller.go b/controllers/restoresession_controller.go new file mode 100644 index 0000000..8ebdcc0 --- /dev/null +++ b/controllers/restoresession_controller.go @@ -0,0 +1,142 @@ +package controllers + +import ( + "context" + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "os" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type RestoreSessionReconciler struct { + Session + backupConf formolv1alpha1.BackupConfiguration + restoreSession formolv1alpha1.RestoreSession +} + +func (r *RestoreSessionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.Log = log.FromContext(ctx) + r.Context = ctx + r.Namespace = req.NamespacedName.Namespace + r.Name = req.NamespacedName.Name + + restoreSession := formolv1alpha1.RestoreSession{} + err := r.Get(r.Context, req.NamespacedName, &restoreSession) + if err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + if len(restoreSession.Status.Targets) == 0 { + r.Log.V(0).Info("RestoreSession still being initialized by the main controller. Wait for the next update...") + return ctrl.Result{}, nil + } + r.restoreSession = restoreSession + // We need the BackupConfiguration to get information about our restore target + backupSession := formolv1alpha1.BackupSession{ + Spec: restoreSession.Spec.BackupSessionRef.Spec, + Status: restoreSession.Spec.BackupSessionRef.Status, + } + backupConf := formolv1alpha1.BackupConfiguration{} + err = r.Get(r.Context, client.ObjectKey{ + Namespace: backupSession.Spec.Ref.Namespace, + Name: backupSession.Spec.Ref.Name, + }, &backupConf) + if err != nil { + if errors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + r.backupConf = backupConf + + // we don't want a copy because we will modify and update it. + var target formolv1alpha1.Target + var restoreTargetStatus *formolv1alpha1.TargetStatus + var backupTargetStatus formolv1alpha1.TargetStatus + targetName := os.Getenv(formolv1alpha1.TARGET_NAME) + + for i, t := range backupConf.Spec.Targets { + if t.TargetName == targetName { + target = t + restoreTargetStatus = &(restoreSession.Status.Targets[i]) + backupTargetStatus = backupSession.Status.Targets[i] + break + } + } + + // Do preliminary checks with the repository + if err = r.SetResticEnv(backupConf); err != nil { + r.Log.Error(err, "unable to set restic env") + return ctrl.Result{}, err + } + + var newSessionState formolv1alpha1.SessionState + switch restoreTargetStatus.SessionState { + case formolv1alpha1.New: + // New session move to Initializing + r.Log.V(0).Info("New session. Move to Initializing state") + newSessionState = formolv1alpha1.Initializing + case formolv1alpha1.Initializing: + // Run the initializing Steps and then move to Initialized or Failure + r.Log.V(0).Info("Start to run the backup initializing steps is any") + // Runs the Steps functions in chroot env + if err := r.runInitializeSteps(target); err != nil { + r.Log.Error(err, "unable to run the initialization steps") + newSessionState = formolv1alpha1.Failure + } else { + r.Log.V(0).Info("Done with the initializing Steps. Move to Initialized state") + newSessionState = formolv1alpha1.Initialized + } + case formolv1alpha1.Running: + // Do the restore and move to Waiting once it is done. + // The restore is different if the Backup was an OnlineKind or a JobKind + switch target.BackupType { + case formolv1alpha1.JobKind: + r.Log.V(0).Info("restoring job backup", "target", target) + if err := r.restoreJob(target, backupTargetStatus); err != nil { + r.Log.Error(err, "unable to restore job", "target", target) + newSessionState = formolv1alpha1.Failure + } else { + r.Log.V(0).Info("job backup restore was a success", "target", target) + newSessionState = formolv1alpha1.Success + } + case formolv1alpha1.OnlineKind: + // The initContainer will update the SessionState of the target + // once it is done with the restore + r.Log.V(0).Info("restoring online backup", "target", target) + if err := r.restoreInitContainer(target); err != nil { + r.Log.Error(err, "unable to create restore initContainer", "target", target) + newSessionState = formolv1alpha1.Failure + } + } + case formolv1alpha1.Finalize: + r.Log.V(0).Info("We are done with the restore. Run the finalize steps") + // Runs the finalize Steps functions in chroot env + if err := r.runFinalizeSteps(target); err != nil { + r.Log.Error(err, "unable to run finalize steps") + newSessionState = formolv1alpha1.Failure + } else { + r.Log.V(0).Info("Ran the finalize steps. Restore was a success") + newSessionState = formolv1alpha1.Success + } + } + if newSessionState != "" { + restoreTargetStatus.SessionState = newSessionState + err := r.Status().Update(ctx, &restoreSession) + if err != nil { + r.Log.Error(err, "unable to update RestoreSession status") + } + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} + +func (r *RestoreSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&formolv1alpha1.RestoreSession{}). + Complete(r) +} diff --git a/controllers/restoresession_controller_helper.go b/controllers/restoresession_controller_helper.go new file mode 100644 index 0000000..dd0e05a --- /dev/null +++ b/controllers/restoresession_controller_helper.go @@ -0,0 +1,75 @@ +package controllers + +import ( + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + "os/exec" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (r *RestoreSessionReconciler) restoreInitContainer(target formolv1alpha1.Target) error { + // The restore has to be done by an initContainer since the data is mounted RO + // We create the initContainer here + // Once the the container has rebooted and the initContainer has done its job, it will change the restoreTargetStatus to Waiting. + targetObject, targetPodSpec := formolv1alpha1.GetTargetObjects(target.TargetKind) + if err := r.Get(r.Context, client.ObjectKey{ + Namespace: r.backupConf.Namespace, + Name: target.TargetName, + }, targetObject); err != nil { + r.Log.Error(err, "unable to get target objects", "target", target.TargetName) + return err + } + initContainer := corev1.Container{} + for _, c := range targetPodSpec.Containers { + if c.Name == formolv1alpha1.SIDECARCONTAINER_NAME { + // We copy the existing formol sidecar container to keep the VolumeMounts + // We just have to change the name + // Change the VolumeMounts to RW + // Change the command so the initContainer restores the snapshot + c.DeepCopyInto(&initContainer) + break + } + } + initContainer.Name = formolv1alpha1.RESTORECONTAINER_NAME + for i, _ := range initContainer.VolumeMounts { + initContainer.VolumeMounts[i].ReadOnly = false + } + if env, err := r.getResticEnv(r.backupConf); err != nil { + r.Log.Error(err, "unable to get restic env") + return err + } else { + initContainer.Env = append(initContainer.Env, env...) + } + initContainer.Args = []string{"restoresession", "start", + "--name", r.restoreSession.Name, + "--namespace", r.restoreSession.Namespace, + "--target-name", target.TargetName, + } + targetPodSpec.InitContainers = append(targetPodSpec.InitContainers, initContainer) + // This will kill this Pod and start a new one with the initContainer + // the initContainer will restore the snapshot + // If everything goes well the initContainer will change the restoreTargetStatus to Waiting + if err := r.Update(r.Context, targetObject); err != nil { + r.Log.Error(err, "unable to add the restore init container", "targetObject", targetObject) + return err + } + return nil +} + +func (r *RestoreSessionReconciler) restoreJob(target formolv1alpha1.Target, targetStatus formolv1alpha1.TargetStatus) error { + cmd := exec.Command(RESTIC_EXEC, "restore", targetStatus.SnapshotId, "--target", "/") + // the restic restore command does not support JSON output + if output, err := cmd.CombinedOutput(); err != nil { + r.Log.Error(err, "unable to restore snapshot", "output", output) + return err + } + for _, container := range target.Containers { + for _, job := range container.Job { + if err := r.runFunction(*job.Restore); err != nil { + r.Log.Error(err, "unable to run restore job") + return err + } + } + } + return nil +} diff --git a/controllers/server.go b/controllers/server.go new file mode 100644 index 0000000..782dfb9 --- /dev/null +++ b/controllers/server.go @@ -0,0 +1,77 @@ +package controllers + +import ( + volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "os" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("StartServer") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + utilruntime.Must(formolv1alpha1.AddToScheme(scheme)) + utilruntime.Must(volumesnapshotv1.AddToScheme(scheme)) + utilruntime.Must(corev1.AddToScheme(scheme)) +} + +func StartServer() { + opts := zap.Options{ + Development: true, + } + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + MetricsBindAddress: "0", // disabling prometheus metrics + Scheme: scheme, + Namespace: os.Getenv("POD_NAMESPACE"), + }) + if err != nil { + setupLog.Error(err, "unable to create manager") + os.Exit(1) + } + if err = (&RestoreSessionReconciler{ + Session: Session{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "RestoreSession") + os.Exit(1) + } + + if err = (&BackupSessionReconciler{ + Session: Session{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "BackupSession") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + setupLog.Error(err, "unable to set up ready check") + os.Exit(1) + } + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem starting manager") + os.Exit(1) + } +} diff --git a/controllers/session.go b/controllers/session.go new file mode 100644 index 0000000..07c3be2 --- /dev/null +++ b/controllers/session.go @@ -0,0 +1,346 @@ +package controllers + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" + "github.com/go-logr/logr" + "io" + "io/fs" + "io/ioutil" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "os" + "os/exec" + "path/filepath" + "regexp" + "sigs.k8s.io/controller-runtime/pkg/client" + "strconv" + "strings" +) + +type Session struct { + client.Client + context.Context + Log logr.Logger + Scheme *runtime.Scheme + Namespace string + Name string +} + +type BackupResult struct { + SnapshotId string + Duration float64 +} + +const ( + RESTIC_EXEC = "/usr/bin/restic" +) + +func (s Session) getResticEnv(backupConf formolv1alpha1.BackupConfiguration) (envs []corev1.EnvVar, err error) { + repo := formolv1alpha1.Repo{} + if err = s.Get(s.Context, client.ObjectKey{ + Namespace: backupConf.Namespace, + Name: backupConf.Spec.Repository, + }, &repo); err != nil { + s.Log.Error(err, "unable to get repo", "backupconf", backupConf) + return + } + if repo.Spec.Backend.S3 != nil { + envs = append(envs, corev1.EnvVar{ + Name: formolv1alpha1.RESTIC_REPOSITORY, + Value: fmt.Sprintf("s3:http://%s/%s/%s-%s", + repo.Spec.Backend.S3.Server, + repo.Spec.Backend.S3.Bucket, + strings.ToUpper(backupConf.Namespace), + strings.ToLower(backupConf.Name)), + }) + + data := s.getSecretData(repo.Spec.RepositorySecrets) + envs = append(envs, corev1.EnvVar{ + Name: formolv1alpha1.AWS_ACCESS_KEY_ID, + Value: string(data[formolv1alpha1.AWS_ACCESS_KEY_ID]), + }) + envs = append(envs, corev1.EnvVar{ + Name: formolv1alpha1.AWS_SECRET_ACCESS_KEY, + Value: string(data[formolv1alpha1.AWS_SECRET_ACCESS_KEY]), + }) + envs = append(envs, corev1.EnvVar{ + Name: formolv1alpha1.RESTIC_PASSWORD, + Value: string(data[formolv1alpha1.RESTIC_PASSWORD]), + }) + } + return +} + +func (s Session) SetResticEnv(backupConf formolv1alpha1.BackupConfiguration) error { + envs, err := s.getResticEnv(backupConf) + for _, env := range envs { + os.Setenv(env.Name, env.Value) + } + return err +} + +func (s Session) CheckRepo() error { + s.Log.V(0).Info("Checking repo") + if err := exec.Command(RESTIC_EXEC, "unlock").Run(); err != nil { + s.Log.Error(err, "unable to unlock repo", "repo", os.Getenv(formolv1alpha1.RESTIC_REPOSITORY)) + } + output, err := exec.Command(RESTIC_EXEC, "check").CombinedOutput() + if err != nil { + s.Log.V(0).Info("Initializing new repo") + output, err = exec.Command(RESTIC_EXEC, "init").CombinedOutput() + if err != nil { + s.Log.Error(err, "something went wrong during repo init", "output", output) + } + } + return err +} + +func (s Session) BackupPaths(paths []string) (result BackupResult, err error) { + if err = s.CheckRepo(); err != nil { + s.Log.Error(err, "unable to setup repo", "repo", os.Getenv(formolv1alpha1.RESTIC_REPOSITORY)) + return + } + s.Log.V(0).Info("backing up paths", "paths", paths) + cmd := exec.Command(RESTIC_EXEC, append([]string{"backup", "--json", "--tag", s.Name}, 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 { + s.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": + s.Log.V(0).Info("backup running", "percent done", data["percent_done"].(float64)) + } + } + + err = cmd.Wait() + return +} + +func (s Session) getSecretData(name string) map[string][]byte { + secret := corev1.Secret{} + if err := s.Get(s.Context, client.ObjectKey{ + Namespace: s.Namespace, + Name: name, + }, &secret); err != nil { + s.Log.Error(err, "unable to get Secret", "Secret", name) + return nil + } + return secret.Data +} + +func (s Session) getEnvFromSecretKeyRef(name string, key string) string { + if data := s.getSecretData(name); data != nil { + return string(data[key]) + } + return "" +} + +func (s Session) getConfigMapData(name string) map[string]string { + configMap := corev1.ConfigMap{} + namespace := os.Getenv(formolv1alpha1.POD_NAMESPACE) + if err := s.Get(s.Context, client.ObjectKey{ + Namespace: namespace, + Name: name, + }, &configMap); err != nil { + s.Log.Error(err, "unable to get ConfigMap", "configmap", name) + return nil + } + return configMap.Data +} + +func (s Session) getEnvFromConfigMapKeyRef(name string, key string) string { + if data := s.getConfigMapData(name); data != nil { + return string(data[key]) + } + return "" +} + +func (s Session) getFuncEnv(vars map[string]string, envVars []corev1.EnvVar) { + for _, env := range envVars { + if env.ValueFrom != nil { + if env.ValueFrom.ConfigMapKeyRef != nil { + vars[env.Name] = s.getEnvFromConfigMapKeyRef(env.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name, env.ValueFrom.ConfigMapKeyRef.Key) + } + if env.ValueFrom.SecretKeyRef != nil { + vars[env.Name] = s.getEnvFromSecretKeyRef(env.ValueFrom.SecretKeyRef.LocalObjectReference.Name, env.ValueFrom.SecretKeyRef.Key) + } + } else { + vars[env.Name] = env.Value + } + } +} + +func (s Session) getEnvFromSecretEnvSource(vars map[string]string, name string) { + for key, value := range s.getSecretData(name) { + vars[key] = string(value) + } +} + +func (s Session) getEnvFromConfigMapEnvSource(vars map[string]string, name string) { + for key, value := range s.getConfigMapData(name) { + vars[key] = value + } +} + +func (s Session) getFuncEnvFrom(vars map[string]string, envVars []corev1.EnvFromSource) { + for _, env := range envVars { + if env.ConfigMapRef != nil { + s.getEnvFromConfigMapEnvSource(vars, env.ConfigMapRef.LocalObjectReference.Name) + } + if env.SecretRef != nil { + s.getEnvFromSecretEnvSource(vars, env.SecretRef.LocalObjectReference.Name) + } + } +} + +func (s Session) getFuncVars(function formolv1alpha1.Function, vars map[string]string) { + s.getFuncEnvFrom(vars, function.Spec.EnvFrom) + s.getFuncEnv(vars, function.Spec.Env) +} + +func (s Session) runFunction(name string) error { + namespace := os.Getenv(formolv1alpha1.POD_NAMESPACE) + function := formolv1alpha1.Function{} + if err := s.Get(s.Context, client.ObjectKey{ + Namespace: namespace, + Name: name, + }, &function); err != nil { + s.Log.Error(err, "unable to get Function", "Function", name) + return err + } + vars := make(map[string]string) + s.getFuncVars(function, vars) + + s.Log.V(0).Info("function vars", "vars", vars) + // Loop through the function.Spec.Command arguments to replace ${ARG}|$(ARG)|$ARG + // with the environment variable value + pattern := regexp.MustCompile(`^\$\((?P\w+)\)$`) + for i, arg := range function.Spec.Args { + if pattern.MatchString(arg) { + s.Log.V(0).Info("arg matches $()", "arg", arg) + arg = pattern.ReplaceAllString(arg, "$env") + function.Spec.Args[i] = vars[arg] + } + } + s.Log.V(1).Info("about to run Function", "Function", name, "command", function.Spec.Command, "args", function.Spec.Args) + if err := s.runTargetContainerChroot(function.Spec.Command[0], + function.Spec.Args...); err != nil { + s.Log.Error(err, "unable to run command", "command", function.Spec.Command) + return err + } + return nil +} + +// Runs the given command in the target container chroot +func (s Session) runTargetContainerChroot(runCmd string, args ...string) error { + env := regexp.MustCompile(`/proc/[0-9]+/environ`) + if err := filepath.WalkDir("/proc", func(path string, info fs.DirEntry, 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) { + content, err := ioutil.ReadFile(path) + // cannot read environ file. not the process we want to backup + if err != nil { + return fs.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 { + s.Log.Error(err, "unable to regexp", "env", string(env)) + return err + } + if matched { + // Found the right process. Now run the command in its 'root' + s.Log.V(0).Info("Found the tag", "file", path) + root := filepath.Join(filepath.Dir(path), "root") + if _, err := filepath.EvalSymlinks(root); err != nil { + s.Log.Error(err, "cannot EvalSymlink.") + return err + } + s.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() { + s.Log.V(0).Info("cmd output", "output", scanner.Text()) + } + + if err := cmd.Wait(); err != nil { + return err + } else { + return filepath.SkipAll + } + } + } + } + return nil + }); err != nil { + s.Log.Error(err, "cannot walk /proc") + return err + } + return nil +} + +type selectStep func(formolv1alpha1.Step) *string + +func (s Session) runSteps(target formolv1alpha1.Target, fn selectStep) error { + // For every container listed in the target, run the initialization steps + for _, container := range target.Containers { + // Runs the steps one after the other + for _, step := range container.Steps { + if fn(step) != nil { + if err := s.runFunction(*fn(step)); err != nil { + return err + } + } + } + } + s.Log.V(0).Info("Done running steps") + return nil +} + +// Run the initializing steps in the INITIALIZING state of the controller +// before actualy doing the backup in the RUNNING state +func (s Session) runInitializeSteps(target formolv1alpha1.Target) error { + s.Log.V(0).Info("start to run the finalize steps it any") + return s.runSteps(target, func(step formolv1alpha1.Step) *string { + return step.Initialize + }) +} + +// Run the finalizing steps in the FINALIZE state of the controller +// after the backup in the RUNNING state. +// The finalize happens whatever the result of the backup. +func (s Session) runFinalizeSteps(target formolv1alpha1.Target) error { + s.Log.V(0).Info("start to run the initialize steps it any") + return s.runSteps(target, func(step formolv1alpha1.Step) *string { + return step.Finalize + }) +} diff --git a/formol b/formol new file mode 160000 index 0000000..ea1c1bd --- /dev/null +++ b/formol @@ -0,0 +1 @@ +Subproject commit ea1c1bd2e31cc6f67621ed71659e738ca5f5d8c8 diff --git a/go.mod b/go.mod index dd846cf..b2794e1 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,74 @@ module github.com/desmo999r/formolcli -go 1.14 +go 1.19 require ( - github.com/desmo999r/formol v0.7.1 - github.com/go-logr/logr v0.3.0 - github.com/go-logr/zapr v0.2.0 - github.com/mitchellh/go-homedir v1.1.0 - github.com/onsi/ginkgo v1.14.1 - github.com/onsi/gomega v1.10.2 - github.com/spf13/cobra v1.1.1 - github.com/spf13/viper v1.7.0 - go.uber.org/zap v1.15.0 - k8s.io/api v0.20.2 - k8s.io/apimachinery v0.20.2 - k8s.io/client-go v0.20.2 - sigs.k8s.io/controller-runtime v0.8.3 + github.com/desmo999r/formol v0.8.0 + github.com/go-logr/logr v1.2.3 + github.com/kubernetes-csi/external-snapshotter/client/v6 v6.2.0 + github.com/spf13/cobra v1.6.1 + k8s.io/api v0.26.1 + k8s.io/apimachinery v0.26.1 + k8s.io/client-go v0.26.1 + sigs.k8s.io/controller-runtime v0.14.2 ) -replace github.com/desmo999r/formol => /home/jandre/devel/golang/formol +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/zapr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/gnostic v0.6.9 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.37.0 // indirect + github.com/prometheus/procfs v0.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10 // indirect + golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect + golang.org/x/sys v0.3.0 // indirect + golang.org/x/term v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect + golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.26.1 // indirect + k8s.io/component-base v0.26.1 // indirect + k8s.io/klog/v2 v2.80.1 // indirect + k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect + k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect +) + +replace github.com/desmo999r/formol => ./formol diff --git a/main.go b/main.go index 81f4c1a..74459e8 100644 --- a/main.go +++ b/main.go @@ -1,21 +1,9 @@ /* -Copyright © 2020 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +Copyright © 2023 NAME HERE */ package main -import "github.com/desmo999r/formolcli/pkg/cmd" +import "github.com/desmo999r/formolcli/cmd" func main() { cmd.Execute() diff --git a/manifests/formolcli-rbac.yaml b/manifests/formolcli-rbac.yaml deleted file mode 100644 index 51cb035..0000000 --- a/manifests/formolcli-rbac.yaml +++ /dev/null @@ -1,69 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: backup ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: backupsession-creator - namespace: backup - labels: - app: backupsession-creator ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRole -metadata: - name: backup-listener - labels: - app: backup-listener -rules: - - apiGroups: ["formol.desmojim.fr"] - resources: ["backupsessions", "backupconfigurations"] - verbs: ["get", "list", "watch"] - - apiGroups: ["formol.desmojim.fr"] - resources: ["backupsessions/status"] - verbs: ["update"] ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRole -metadata: - name: backupsession-creator - labels: - app: backupsession-creator -rules: - - apiGroups: ["formol.desmojim.fr"] - resources: ["backupsessions"] - verbs: ["get", "list", "create", "delete"] ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRoleBinding -metadata: - name: backupsession-creator - labels: - app: backupsession-creator -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: backupsession-creator -subjects: - - name: backupsession-creator - namespace: backup - kind: ServiceAccount - ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRoleBinding -metadata: - name: backup-listener - labels: - app: backup-listener -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: backup-listener -subjects: - - name: default - namespace: default - kind: ServiceAccount - diff --git a/pkg/backup/root.go b/pkg/backup/root.go deleted file mode 100644 index c813036..0000000 --- a/pkg/backup/root.go +++ /dev/null @@ -1,58 +0,0 @@ -package backup - -import ( - "fmt" - formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - "github.com/desmo999r/formolcli/pkg/restic" - "github.com/desmo999r/formolcli/pkg/session" - "github.com/go-logr/logr" - "github.com/go-logr/zapr" - "go.uber.org/zap" - "io/ioutil" - "os" - "os/exec" -) - -var ( - pg_dumpExec = "/usr/bin/pg_dump" - logger logr.Logger -) - -func init() { - zapLog, _ := zap.NewDevelopment() - logger = zapr.NewLogger(zapLog) -} - -func BackupVolume(tag string, paths []string) error { - log := logger.WithName("backup-volume") - state := formolv1alpha1.Success - output, err := restic.BackupPaths(tag, paths) - var snapshotId string - if err != nil { - log.Error(err, "unable to backup volume", "output", string(output)) - state = formolv1alpha1.Failure - } else { - snapshotId = restic.GetBackupResults(output) - } - session.BackupSessionUpdateTargetStatus(state, snapshotId) - return err -} - -func BackupPostgres(file string, hostname string, database string, username string, password string) error { - log := logger.WithName("backup-postgres") - pgpass := []byte(fmt.Sprintf("%s:*:%s:%s:%s", hostname, database, username, password)) - if err := ioutil.WriteFile("/output/.pgpass", pgpass, 0600); err != nil { - log.Error(err, "unable to write password to /output/.pgpass") - return err - } - defer os.Remove("/output/.pgpass") - cmd := exec.Command(pg_dumpExec, "--format=custom", "--clean", "--create", "--file", file, "--host", hostname, "--dbname", database, "--username", username, "--no-password") - cmd.Env = append(os.Environ(), "PGPASSFILE=/output/.pgpass") - output, err := cmd.CombinedOutput() - log.V(1).Info("postgres backup output", "output", string(output)) - if err != nil { - log.Error(err, "something went wrong during the backup") - return err - } - return nil -} diff --git a/pkg/cmd/backupsession.go b/pkg/cmd/backupsession.go deleted file mode 100644 index 12e3a26..0000000 --- a/pkg/cmd/backupsession.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright © 2020 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cmd - -import ( - "github.com/desmo999r/formolcli/pkg/server" - "github.com/desmo999r/formolcli/pkg/session" - "github.com/spf13/cobra" -) - -var serverBackupSessionCmd = &cobra.Command{ - Use: "server", - Short: "A brief description of your command", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - server.Server() - }, -} - -var createBackupSessionCmd = &cobra.Command{ - Use: "create", - Short: "Creates a backupsession", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - name, _ := cmd.Flags().GetString("name") - namespace, _ := cmd.Flags().GetString("namespace") - session.CreateBackupSession(name, namespace) - }, -} - -var backupSessionCmd = &cobra.Command{ - Use: "backupsession", - Short: "backupsession related commands", -} - -func init() { - rootCmd.AddCommand(backupSessionCmd) - backupSessionCmd.AddCommand(createBackupSessionCmd) - backupSessionCmd.AddCommand(serverBackupSessionCmd) - createBackupSessionCmd.Flags().String("namespace", "", "The referenced BackupSessionConfiguration namespace") - createBackupSessionCmd.Flags().String("name", "", "The referenced BackupSessionConfiguration name") - createBackupSessionCmd.MarkFlagRequired("namespace") - createBackupSessionCmd.MarkFlagRequired("name") -} diff --git a/pkg/cmd/postgres.go b/pkg/cmd/postgres.go deleted file mode 100644 index 91bf264..0000000 --- a/pkg/cmd/postgres.go +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright © 2020 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cmd - -import ( - "fmt" - - "github.com/desmo999r/formolcli/pkg/backup" - "github.com/desmo999r/formolcli/pkg/restore" - "github.com/spf13/cobra" -) - -// postgresCmd represents the postgres command -var postgresRestoreCmd = &cobra.Command{ - Use: "restore", - Short: "restore a PostgreSQL database", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("postgres called") - file, _ := cmd.Flags().GetString("file") - hostname, _ := cmd.Flags().GetString("hostname") - database, _ := cmd.Flags().GetString("database") - username, _ := cmd.Flags().GetString("username") - password, _ := cmd.Flags().GetString("password") - _ = restore.RestorePostgres(file, hostname, database, username, password) - }, -} - -var postgresBackupCmd = &cobra.Command{ - Use: "backup", - Short: "backup a PostgreSQL database", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("postgres called") - file, _ := cmd.Flags().GetString("file") - hostname, _ := cmd.Flags().GetString("hostname") - database, _ := cmd.Flags().GetString("database") - username, _ := cmd.Flags().GetString("username") - password, _ := cmd.Flags().GetString("password") - _ = backup.BackupPostgres(file, hostname, database, username, password) - }, -} - -var postgresCmd = &cobra.Command{ - Use: "postgres", - Short: "postgres actions", -} - -func init() { - rootCmd.AddCommand(postgresCmd) - postgresCmd.AddCommand(postgresBackupCmd) - postgresCmd.AddCommand(postgresRestoreCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // backupPostgresCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // backupPostgresCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - postgresBackupCmd.Flags().String("file", "", "The file the backup will be stored") - postgresBackupCmd.Flags().String("hostname", "", "The postgresql server host") - postgresBackupCmd.Flags().String("database", "", "The postgresql database") - postgresBackupCmd.Flags().String("username", "", "The postgresql username") - postgresBackupCmd.Flags().String("password", "", "The postgresql password") - postgresBackupCmd.MarkFlagRequired("path") - postgresBackupCmd.MarkFlagRequired("hostname") - postgresBackupCmd.MarkFlagRequired("database") - postgresBackupCmd.MarkFlagRequired("username") - postgresBackupCmd.MarkFlagRequired("password") - postgresRestoreCmd.Flags().String("file", "", "The file the database will be restored from") - postgresRestoreCmd.Flags().String("hostname", "", "The postgresql server host") - postgresRestoreCmd.Flags().String("database", "", "The postgresql database") - postgresRestoreCmd.Flags().String("username", "", "The postgresql username") - postgresRestoreCmd.Flags().String("password", "", "The postgresql password") - postgresRestoreCmd.MarkFlagRequired("path") - postgresRestoreCmd.MarkFlagRequired("hostname") - postgresRestoreCmd.MarkFlagRequired("database") - postgresRestoreCmd.MarkFlagRequired("username") - postgresRestoreCmd.MarkFlagRequired("password") -} diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go deleted file mode 100644 index 86f2468..0000000 --- a/pkg/cmd/root.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright © 2020 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cmd - -import ( - "fmt" - "github.com/spf13/cobra" - "os" - - homedir "github.com/mitchellh/go-homedir" - "github.com/spf13/viper" -) - -var cfgFile string - -// rootCmd represents the base command when called without any subcommands -var rootCmd = &cobra.Command{ - Use: "formolcli", - Short: "A brief description of your application", - Long: `A longer description that spans multiple lines and likely contains -examples and usage of using your application. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, -} - -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) - } -} - -func init() { - cobra.OnInitialize(initConfig) - - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.formolcli.yaml)") - - // Cobra also supports local flags, which will only run - // when this action is called directly. - rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") -} - -// initConfig reads in config file and ENV variables if set. -func initConfig() { - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Find home directory. - home, err := homedir.Dir() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - // Search config in home directory with name ".formolcli" (without extension). - viper.AddConfigPath(home) - viper.SetConfigName(".formolcli") - } - - viper.AutomaticEnv() // read in environment variables that match - - // If a config file is found, read it in. - if err := viper.ReadInConfig(); err == nil { - fmt.Println("Using config file:", viper.ConfigFileUsed()) - } -} diff --git a/pkg/cmd/snapshot.go b/pkg/cmd/snapshot.go deleted file mode 100644 index 0cadb76..0000000 --- a/pkg/cmd/snapshot.go +++ /dev/null @@ -1,46 +0,0 @@ -/* Copyright © 2020 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cmd - -import ( - "os" - - "github.com/desmo999r/formolcli/pkg/restic" - "github.com/spf13/cobra" -) - -// snapshotCmd represents the snapshot command -var snapshotCmd = &cobra.Command{ - Use: "snapshot", - Short: "A brief description of your command", -} - -var snapshotDeleteCmd = &cobra.Command{ - Use: "delete", - Short: "delete a snapshot", - Run: func(cmd *cobra.Command, args []string) { - snapshot, _ := cmd.Flags().GetString("snapshot-id") - if err := restic.DeleteSnapshot(snapshot); err != nil { - os.Exit(1) - } - }, -} - -func init() { - rootCmd.AddCommand(snapshotCmd) - snapshotCmd.AddCommand(snapshotDeleteCmd) - snapshotDeleteCmd.Flags().String("snapshot-id", "", "The snapshot to delete") - snapshotDeleteCmd.MarkFlagRequired("snapshot-id") -} diff --git a/pkg/cmd/target.go b/pkg/cmd/target.go deleted file mode 100644 index f7a63c7..0000000 --- a/pkg/cmd/target.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright © 2021 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cmd - -import ( - "fmt" - - formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - "github.com/desmo999r/formolcli/pkg/session" - "github.com/spf13/cobra" -) - -// targetCmd represents the target command -var targetFinalizeCmd = &cobra.Command{ - Use: "finalize", - Short: "Update the session target status", - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("target called") - session.RestoreSessionUpdateTargetStatus(formolv1alpha1.Success) - }, -} - -var targetCmd = &cobra.Command{ - Use: "target", - Short: "A brief description of your command", -} - -func init() { - rootCmd.AddCommand(targetCmd) - targetCmd.AddCommand(targetFinalizeCmd) -} diff --git a/pkg/cmd/volume.go b/pkg/cmd/volume.go deleted file mode 100644 index 4cd2e7f..0000000 --- a/pkg/cmd/volume.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright © 2020 NAME HERE - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package cmd - -import ( - "os" - - "github.com/desmo999r/formolcli/pkg/backup" - "github.com/desmo999r/formolcli/pkg/restore" - "github.com/spf13/cobra" -) - -var volumeRestoreCmd = &cobra.Command{ - Use: "restore", - Short: "restore a volume", - Run: func(cmd *cobra.Command, args []string) { - snapshotId, _ := cmd.Flags().GetString("snapshot-id") - if err := restore.RestoreVolume(snapshotId); err != nil { - os.Exit(1) - } - }, -} - -var volumeBackupCmd = &cobra.Command{ - Use: "backup", - Short: "backup a volume", - Run: func(cmd *cobra.Command, args []string) { - paths, _ := cmd.Flags().GetStringSlice("path") - tag, _ := cmd.Flags().GetString("tag") - if err := backup.BackupVolume(tag, paths); err != nil { - os.Exit(1) - } - }, -} - -var volumeCmd = &cobra.Command{ - Use: "volume", - Short: "volume actions", - Long: `A longer description that spans multiple lines and likely contains examples -and usage of using your command. For example: - -Cobra is a CLI library for Go that empowers applications. -This application is a tool to generate the needed files -to quickly create a Cobra application.`, -} - -func init() { - rootCmd.AddCommand(volumeCmd) - volumeCmd.AddCommand(volumeBackupCmd) - volumeCmd.AddCommand(volumeRestoreCmd) - - volumeBackupCmd.Flags().StringSlice("path", nil, "Path to the data to backup") - volumeBackupCmd.Flags().String("tag", "", "Tag associated to the backup") - volumeBackupCmd.MarkFlagRequired("path") - volumeRestoreCmd.Flags().String("snapshot-id", "", "snapshot id associated to the backup") - volumeRestoreCmd.MarkFlagRequired("snapshot-id") -} diff --git a/pkg/controllers/backupsession_controller.go b/pkg/controllers/backupsession_controller.go deleted file mode 100644 index faecf7c..0000000 --- a/pkg/controllers/backupsession_controller.go +++ /dev/null @@ -1,225 +0,0 @@ -package controllers - -import ( - "context" - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/runtime" - "os" - "regexp" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "time" - - formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - "github.com/desmo999r/formolcli/pkg/restic" - formolcliutils "github.com/desmo999r/formolcli/pkg/utils" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// BackupSessionReconciler reconciles a BackupSession object -type BackupSessionReconciler struct { - client.Client - Log logr.Logger - Scheme *runtime.Scheme -} - -var _ reconcile.Reconciler = &BackupSessionReconciler{} - -func (r *BackupSessionReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { - time.Sleep(2 * time.Second) - log := r.Log.WithValues("backupsession", req.NamespacedName) - - // your logic here - backupSession := &formolv1alpha1.BackupSession{} - if err := r.Get(ctx, req.NamespacedName, backupSession); err != nil { - log.Error(err, "unable to get backupsession") - return reconcile.Result{}, client.IgnoreNotFound(err) - } - backupConf := &formolv1alpha1.BackupConfiguration{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: backupSession.Namespace, - Name: backupSession.Spec.Ref.Name, - }, backupConf); err != nil { - log.Error(err, "unable to get backupConfiguration") - return reconcile.Result{}, client.IgnoreNotFound(err) - } - - deploymentName := os.Getenv(formolv1alpha1.TARGET_NAME) - for _, target := range backupConf.Spec.Targets { - switch target.Kind { - case formolv1alpha1.SidecarKind: - if target.Name == deploymentName { - // We are involved in that Backup, let's see if it's our turn - status := &(backupSession.Status.Targets[len(backupSession.Status.Targets)-1]) - if status.Name == deploymentName { - log.V(0).Info("It's for us!", "target", target) - switch status.SessionState { - case formolv1alpha1.New: - log.V(0).Info("New session, move to Initializing state") - status.SessionState = formolv1alpha1.Init - if err := r.Status().Update(ctx, backupSession); err != nil { - log.Error(err, "unable to update backupsession status") - return reconcile.Result{}, err - } - case formolv1alpha1.Init: - log.V(0).Info("Start to run the backup initializing steps if any") - result := formolv1alpha1.Running - for _, step := range target.Steps { - if step.Finalize != nil && *step.Finalize == true { - continue - } - function := &formolv1alpha1.Function{} - if err := r.Get(ctx, client.ObjectKey{ - Name: step.Name, - Namespace: backupConf.Namespace, - }, function); err != nil { - log.Error(err, "unable to get function", "function", step.Name) - return reconcile.Result{}, err - } - // TODO: check the command arguments for $(VAR_NAME) arguments. If some are found, try to expand them from - // the Function.Spec EnvFrom and Env in that order - pattern := regexp.MustCompile(`^\$\((\w+)\)$`) - for i, arg := range function.Spec.Command[1:] { - i++ - if match, _ := regexp.MatchString(`^\$\$`, arg); match { - continue - } - if pattern.MatchString(arg) { - arg = pattern.ReplaceAllString(arg, "$1") - // TODO: Find arg in EnvFrom key and replace it by the value in Command[i] - for _, envFrom := range function.Spec.EnvFrom { - if envFrom.SecretRef != nil { - secret := &corev1.Secret{} - if err := r.Get(ctx, client.ObjectKey{ - Name: envFrom.SecretRef.Name, - Namespace: backupConf.Namespace, - }, secret); err != nil { - log.Error(err, "unable to get secret", "secret", envFrom.SecretRef.Name) - return reconcile.Result{}, err - } - if val, ok := secret.Data[arg]; ok { - log.V(1).Info("Found EnvFrom value for arg", "arg", arg) - function.Spec.Command[i] = string(val) - } - - } - if envFrom.ConfigMapRef != nil { - configMap := &corev1.ConfigMap{} - if err := r.Get(ctx, client.ObjectKey{ - Name: envFrom.ConfigMapRef.Name, - Namespace: backupConf.Namespace, - }, configMap); err != nil { - log.Error(err, "unable to get configMap", "configMap", envFrom.ConfigMapRef.Name) - return reconcile.Result{}, err - } - if val, ok := configMap.Data[arg]; ok { - log.V(1).Info("Found EnvFrom value for arg", "arg", arg) - function.Spec.Command[i] = val - } - } - } - for _, env := range function.Spec.Env { - if env.Name == arg { - if env.Value == "" { - if env.ValueFrom != nil { - if env.ValueFrom.SecretKeyRef != nil { - secret := &corev1.Secret{} - if err := r.Get(ctx, client.ObjectKey{ - Name: env.ValueFrom.SecretKeyRef.Name, - Namespace: backupConf.Namespace, - }, secret); err != nil { - log.Error(err, "unable to get secret", "secret", env.ValueFrom.SecretKeyRef.Name) - return reconcile.Result{}, err - } - log.V(1).Info("Found Env value for arg", "arg", arg) - function.Spec.Command[i] = string(secret.Data[env.ValueFrom.SecretKeyRef.Key]) - } - } - } else { - function.Spec.Command[i] = env.Value - } - } - } - } - } - if err := formolcliutils.RunChroot(target.ContainerName != "", function.Spec.Command[0], function.Spec.Command[1:]...); err != nil { - log.Error(err, "unable to run function command", "command", function.Spec.Command) - result = formolv1alpha1.Failure - break - } - } - status.SessionState = result - - if err := r.Status().Update(ctx, backupSession); err != nil { - log.Error(err, "unable to update backupsession status") - return reconcile.Result{}, err - } - case formolv1alpha1.Running: - log.V(0).Info("Running session. Do the backup") - result := formolv1alpha1.Finalize - status.StartTime = &metav1.Time{Time: time.Now()} - output, err := restic.BackupPaths(backupSession.Name, target.Paths) - if err != nil { - log.Error(err, "unable to backup deployment", "output", string(output)) - result = formolv1alpha1.Failure - } else { - snapshotId := restic.GetBackupResults(output) - status.SnapshotId = snapshotId - status.Duration = &metav1.Duration{Duration: time.Now().Sub(status.StartTime.Time)} - } - status.SessionState = result - log.V(1).Info("current backupSession status", "status", backupSession.Status) - if err := r.Status().Update(ctx, backupSession); err != nil { - log.Error(err, "unable to update backupsession status") - return reconcile.Result{}, err - } - case formolv1alpha1.Finalize: - log.V(0).Info("Start to run the backup finalizing steps if any") - result := formolv1alpha1.Success - for _, step := range target.Steps { - if step.Finalize != nil && *step.Finalize == true { - function := &formolv1alpha1.Function{} - if err := r.Get(ctx, client.ObjectKey{ - Name: step.Name, - Namespace: backupConf.Namespace, - }, function); err != nil { - log.Error(err, "unable to get function", "function", step.Name) - return reconcile.Result{}, err - } - if err := formolcliutils.RunChroot(target.ContainerName != "", function.Spec.Command[0], function.Spec.Command[1:]...); err != nil { - log.Error(err, "unable to run function command", "command", function.Spec.Command) - result = formolv1alpha1.Failure - break - } - } - } - status.SessionState = result - - if err := r.Status().Update(ctx, backupSession); err != nil { - log.Error(err, "unable to update backupsession status") - return reconcile.Result{}, err - } - - case formolv1alpha1.Success, formolv1alpha1.Failure: - log.V(0).Info("Backup is over") - } - } - } - } - } - return reconcile.Result{}, nil -} - -func (r *BackupSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&formolv1alpha1.BackupSession{}). - WithEventFilter(predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { return false }, - DeleteFunc: func(e event.DeleteEvent) bool { return false }, - }). - Complete(r) -} diff --git a/pkg/controllers/restoresession_controller.go b/pkg/controllers/restoresession_controller.go deleted file mode 100644 index 6cb7877..0000000 --- a/pkg/controllers/restoresession_controller.go +++ /dev/null @@ -1,158 +0,0 @@ -package controllers - -import ( - "context" - formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - formolcliutils "github.com/desmo999r/formolcli/pkg/utils" - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "os" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "strings" - "time" -) - -type RestoreSessionReconciler struct { - client.Client - Log logr.Logger - Scheme *runtime.Scheme -} - -var _ reconcile.Reconciler = &RestoreSessionReconciler{} - -func (r *RestoreSessionReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { - log := r.Log.WithValues("restoresession", req.NamespacedName) - - restoreSession := &formolv1alpha1.RestoreSession{} - if err := r.Get(ctx, req.NamespacedName, restoreSession); err != nil { - log.Error(err, "unable to get restoresession") - return reconcile.Result{}, client.IgnoreNotFound(err) - } - backupSession := &formolv1alpha1.BackupSession{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: restoreSession.Namespace, - Name: restoreSession.Spec.BackupSessionRef.Ref.Name, - }, backupSession); err != nil { - if errors.IsNotFound(err) { - backupSession = &formolv1alpha1.BackupSession{ - Spec: restoreSession.Spec.BackupSessionRef.Spec, - Status: restoreSession.Spec.BackupSessionRef.Status, - } - log.V(1).Info("generated backupsession", "backupsession", backupSession) - } else { - log.Error(err, "unable to get backupsession", "restoresession", restoreSession.Spec) - return reconcile.Result{}, client.IgnoreNotFound(err) - } - } - backupConf := &formolv1alpha1.BackupConfiguration{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: restoreSession.Namespace, // we use the BackupConfiguration in RestoreSession namespace. - Name: backupSession.Spec.Ref.Name, - }, backupConf); err != nil { - log.Error(err, "unable to get backupConfiguration") - return reconcile.Result{}, client.IgnoreNotFound(err) - } - deploymentName := os.Getenv(formolv1alpha1.TARGET_NAME) - currentTargetStatus := &(restoreSession.Status.Targets[len(restoreSession.Status.Targets)-1]) - currentTarget := backupConf.Spec.Targets[len(restoreSession.Status.Targets)-1] - switch currentTarget.Kind { - case formolv1alpha1.SidecarKind: - if currentTarget.Name == deploymentName { - switch currentTargetStatus.SessionState { - case formolv1alpha1.Finalize: - log.V(0).Info("It's for us!", "target", currentTarget.Name) - podName := os.Getenv(formolv1alpha1.POD_NAME) - podNamespace := os.Getenv(formolv1alpha1.POD_NAMESPACE) - pod := &corev1.Pod{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: podNamespace, - Name: podName, - }, pod); err != nil { - log.Error(err, "unable to get pod", "name", podName, "namespace", podNamespace) - return reconcile.Result{}, err - } - for _, containerStatus := range pod.Status.ContainerStatuses { - if !containerStatus.Ready { - log.V(0).Info("Not all the containers in the pod are ready. Reschedule", "name", containerStatus.Name) - return reconcile.Result{RequeueAfter: 10 * time.Second}, nil - } - } - log.V(0).Info("All the containers in the pod are ready. Time to run the restore steps (in reverse order)") - // We iterate through the steps in reverse order - result := formolv1alpha1.Success - for i := range currentTarget.Steps { - step := currentTarget.Steps[len(currentTarget.Steps)-1-i] - log.V(1).Info("current step", "step", step.Name) - backupFunction := &formolv1alpha1.Function{} - if err := r.Get(ctx, client.ObjectKey{ - Namespace: backupConf.Namespace, - Name: step.Name, - }, backupFunction); err != nil { - log.Error(err, "unable to get backup function") - return reconcile.Result{}, err - } - // We got the backup function corresponding to the step from the BackupConfiguration - // Now let's try to get the restore function is there is one - restoreFunction := &formolv1alpha1.Function{} - if restoreFunctionName, exists := backupFunction.Annotations[formolv1alpha1.RESTORE_ANNOTATION]; exists { - log.V(0).Info("got restore function", "name", restoreFunctionName) - if err := r.Get(ctx, client.ObjectKey{ - Namespace: backupConf.Namespace, - Name: restoreFunctionName, - }, restoreFunction); err != nil { - log.Error(err, "unable to get restore function") - continue - } - } else { - if strings.HasPrefix(backupFunction.Name, "backup-") { - log.V(0).Info("backupFunction starts with 'backup-'", "name", backupFunction.Name) - if err := r.Get(ctx, client.ObjectKey{ - Namespace: backupConf.Namespace, - Name: strings.Replace(backupFunction.Name, "backup-", "restore-", 1), - }, restoreFunction); err != nil { - log.Error(err, "unable to get restore function") - continue - } - } - log.V(1).Info("No associated restore function", "step", step.Name) - } - if len(restoreFunction.Spec.Command) > 1 { - log.V(0).Info("Running the restore function", "name", restoreFunction.Name, "command", restoreFunction.Spec.Command) - if err := formolcliutils.RunChroot(currentTarget.ContainerName != "", restoreFunction.Spec.Command[0], restoreFunction.Spec.Command[1:]...); err != nil { - log.Error(err, "unable to run function command", "command", restoreFunction.Spec.Command) - result = formolv1alpha1.Failure - break - } else { - log.V(0).Info("Restore command is successful") - } - } - } - // We are done with the restore of this target. We flag it as success or failure - // so that we can move to the next step - log.V(0).Info("Finalize is over", "target", currentTarget.Name) - currentTargetStatus.SessionState = result - if err := r.Status().Update(ctx, restoreSession); err != nil { - log.Error(err, "unable to update restoresession") - } - } - } - } - - return reconcile.Result{}, nil -} - -func (r *RestoreSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&formolv1alpha1.RestoreSession{}). - WithEventFilter(predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { return false }, - DeleteFunc: func(e event.DeleteEvent) bool { return false }, - }). - Complete(r) -} diff --git a/pkg/restic/root.go b/pkg/restic/root.go deleted file mode 100644 index d4effe4..0000000 --- a/pkg/restic/root.go +++ /dev/null @@ -1,111 +0,0 @@ -package restic - -import ( - "bufio" - "bytes" - "encoding/json" - "github.com/go-logr/logr" - "github.com/go-logr/zapr" - "go.uber.org/zap" - "os" - "os/exec" - "time" -) - -var ( - repository string - passwordFile string - aws_access_key_id string - aws_secret_access_key string - resticExec = "/usr/bin/restic" - logger logr.Logger -) - -func init() { - zapLog, _ := zap.NewDevelopment() - logger = zapr.NewLogger(zapLog) - repository = os.Getenv("RESTIC_REPOSITORY") - passwordFile = os.Getenv("RESTIC_PASSWORD") - aws_access_key_id = os.Getenv("AWS_ACCESS_KEY_ID") - aws_secret_access_key = os.Getenv("AWS_SECRET_ACCESS_KEY") -} - -func checkRepo(repo string) error { - log := logger.WithValues("backup-checkrepo", repo) - cmd := exec.Command(resticExec, "unlock", "-r", repo) - if err := cmd.Run(); err != nil { - log.Error(err, "unable to unlock repo", "repo", repo) - } - cmd = exec.Command(resticExec, "check", "-r", repo) - output, err := cmd.CombinedOutput() - log.V(1).Info("restic check output", "output", string(output)) - if err != nil { - log.V(0).Info("initializing new repo", "repo", repo) - cmd = exec.Command(resticExec, "init", "-r", repo) - output, err = cmd.CombinedOutput() - log.V(1).Info("restic init repo", "output", string(output)) - if err != nil { - log.Error(err, "something went wrong during repo init") - return err - } - } - return err -} - -func GetBackupResults(output []byte) (snapshotId string) { - log := logger.WithName("backup-getbackupresults") - scanner := bufio.NewScanner(bytes.NewReader(output)) - var dat map[string]interface{} - for scanner.Scan() { - if err := json.Unmarshal(scanner.Bytes(), &dat); err != nil { - log.Error(err, "unable to unmarshal json", "msg", string(scanner.Bytes())) - continue - } - log.V(1).Info("message on stdout", "stdout", dat) - if message_type, ok := dat["message_type"]; ok && message_type == "summary" { - snapshotId = dat["snapshot_id"].(string) - //duration = time.Duration(dat["total_duration"].(float64)*1000) * time.Millisecond - } - } - return -} - -func GetRestoreResults(output []byte) time.Duration { - return 0 * time.Millisecond -} - -func BackupPaths(tag string, paths []string) ([]byte, error) { - log := logger.WithName("backup-deployment") - if err := checkRepo(repository); err != nil { - log.Error(err, "unable to setup newrepo", "newrepo", repository) - return []byte{}, err - } - cmd := exec.Command(resticExec, append([]string{"backup", "--json", "--tag", tag, "-r", repository}, paths...)...) - output, err := cmd.CombinedOutput() - return output, err -} - -func RestorePaths(snapshotId string) ([]byte, error) { - log := logger.WithName("restore-deployment") - if err := checkRepo(repository); err != nil { - log.Error(err, "unable to setup repo", "repo", repository) - return []byte{}, err - } - cmd := exec.Command(resticExec, "restore", "-r", repository, snapshotId, "--target", "/") - output, err := cmd.CombinedOutput() - log.V(1).Info("restic restore output", "output", string(output)) - return output, err -} - -func DeleteSnapshot(snapshot string) error { - log := logger.WithValues("delete-snapshot", snapshot) - cmd := exec.Command(resticExec, "forget", "-r", repository, "--prune", snapshot) - log.V(0).Info("deleting snapshot", "snapshot", snapshot) - output, err := cmd.CombinedOutput() - log.V(1).Info("delete snapshot output", "output", string(output)) - if err != nil { - log.Error(err, "unable to delete the snapshot") - return err - } - return nil -} diff --git a/pkg/restore/root.go b/pkg/restore/root.go deleted file mode 100644 index 3327ba0..0000000 --- a/pkg/restore/root.go +++ /dev/null @@ -1,61 +0,0 @@ -package restore - -import ( - "fmt" - formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - "github.com/desmo999r/formolcli/pkg/restic" - "github.com/desmo999r/formolcli/pkg/session" - "github.com/go-logr/logr" - "github.com/go-logr/zapr" - "go.uber.org/zap" - "io/ioutil" - "os" - "os/exec" -) - -var ( - //psqlExec = "/usr/bin/psql" - pg_restoreExec = "/usr/bin/pg_restore" - logger logr.Logger -) - -func init() { - zapLog, _ := zap.NewDevelopment() - logger = zapr.NewLogger(zapLog) -} - -func RestoreVolume(snapshotId string) error { - log := logger.WithName("restore-volume") - if err := session.RestoreSessionUpdateTargetStatus(formolv1alpha1.Init); err != nil { - return err - } - state := formolv1alpha1.Waiting - output, err := restic.RestorePaths(snapshotId) - if err != nil { - log.Error(err, "unable to restore volume", "output", string(output)) - state = formolv1alpha1.Failure - } - log.V(1).Info("restic restore output", "output", string(output)) - session.RestoreSessionUpdateTargetStatus(state) - return err -} - -func RestorePostgres(file string, hostname string, database string, username string, password string) error { - log := logger.WithName("restore-postgres") - pgpass := []byte(fmt.Sprintf("%s:*:%s:%s:%s", hostname, database, username, password)) - if err := ioutil.WriteFile("/output/.pgpass", pgpass, 0600); err != nil { - log.Error(err, "unable to write password to /output/.pgpass") - return err - } - defer os.Remove("/output/.pgpass") - //cmd := exec.Command(psqlExec, "--file", file, "--host", hostname, "--dbname", database, "--username", username, "--no-password") - cmd := exec.Command(pg_restoreExec, "--format=custom", "--clean", "--host", hostname, "--dbname", database, "--username", username, "--no-password", file) - cmd.Env = append(os.Environ(), "PGPASSFILE=/output/.pgpass") - output, err := cmd.CombinedOutput() - log.V(1).Info("postgres restore output", "output", string(output)) - if err != nil { - log.Error(err, "something went wrong during the restore") - return err - } - return nil -} diff --git a/pkg/server/root.go b/pkg/server/root.go deleted file mode 100644 index e181019..0000000 --- a/pkg/server/root.go +++ /dev/null @@ -1,74 +0,0 @@ -package server - -import ( - // "flag" - formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - "github.com/desmo999r/formolcli/pkg/controllers" - "k8s.io/apimachinery/pkg/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "os" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log/zap" -) - -var ( - scheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("server") -) - -func init() { - _ = clientgoscheme.AddToScheme(scheme) - _ = formolv1alpha1.AddToScheme(scheme) -} - -func Server() { - // var metricsAddr string - // var enableLeaderElection bool - // flag.StringVar(&metricsAddr, "metrics-addr", ":8082", "The address the metric endpoint binds to.") - // flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, - // "Enable leader election for controller manager. "+ - // "Enabling this will ensure there is only one active controller manager.") - // flag.Parse() - - ctrl.SetLogger(zap.New(zap.UseDevMode(true))) - - config, err := ctrl.GetConfig() - mgr, err := ctrl.NewManager(config, ctrl.Options{ - Scheme: scheme, - // MetricsBindAddress: metricsAddr, - // Port: 9443, - // LeaderElection: enableLeaderElection, - // LeaderElectionID: "12345.desmojim.fr", - Namespace: os.Getenv("POD_NAMESPACE"), - }) - if err != nil { - setupLog.Error(err, "unable to create manager") - os.Exit(1) - } - - // BackupSession controller - if err = (&controllers.BackupSessionReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("BackupSession"), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "BackupSession") - os.Exit(1) - } - - // RestoreSession controller - if err = (&controllers.RestoreSessionReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("RestoreSession"), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "RestoreSession") - os.Exit(1) - } - - setupLog.Info("starting manager") - if err = mgr.Start(ctrl.SetupSignalHandler()); err != nil { - setupLog.Error(err, "problem running the manager") - os.Exit(1) - } -} diff --git a/pkg/session/root.go b/pkg/session/root.go deleted file mode 100644 index 43d8e8a..0000000 --- a/pkg/session/root.go +++ /dev/null @@ -1,142 +0,0 @@ -package session - -import ( - "context" - "errors" - formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - "github.com/go-logr/logr" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "os" - "path/filepath" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "strconv" - "strings" - "time" -) - -var ( - config *rest.Config - scheme *runtime.Scheme - cl client.Client - logger logr.Logger - //backupSession *formolv1alpha1.BackupSession -) - -func init() { - logger = zap.New(zap.UseDevMode(true)) - log := logger.WithName("InitBackupSession") - ctrl.SetLogger(logger) - config, err := rest.InClusterConfig() - if err != nil { - config, err = clientcmd.BuildConfigFromFlags("", filepath.Join(os.Getenv("HOME"), ".kube", "config")) - if err != nil { - log.Error(err, "unable to get config") - os.Exit(1) - } - } - scheme = runtime.NewScheme() - _ = formolv1alpha1.AddToScheme(scheme) - _ = clientgoscheme.AddToScheme(scheme) - cl, err = client.New(config, client.Options{Scheme: scheme}) - if err != nil { - log.Error(err, "unable to get client") - os.Exit(1) - } -} - -func BackupSessionUpdateTargetStatus(state formolv1alpha1.SessionState, snapshotId string) error { - log := logger.WithName("BackupSessionUpdateStatus") - targetName := os.Getenv("TARGET_NAME") - backupSession := &formolv1alpha1.BackupSession{} - if err := cl.Get(context.Background(), client.ObjectKey{ - Namespace: os.Getenv("BACKUPSESSION_NAMESPACE"), - Name: os.Getenv("BACKUPSESSION_NAME"), - }, backupSession); err != nil { - log.Error(err, "unable to get backupsession", "BACKUPSESSION_NAME", os.Getenv("BACKUPSESSION_NAME"), "BACKUPSESSION_NAMESPACE", os.Getenv("BACKUPSESSION_NAMESPACE")) - return err - } - target := &(backupSession.Status.Targets[len(backupSession.Status.Targets)-1]) - if target.Name == targetName { - target.SessionState = state - target.SnapshotId = snapshotId - target.Duration = &metav1.Duration{Duration: time.Now().Sub(target.StartTime.Time)} - } - - if err := cl.Status().Update(context.Background(), backupSession); err != nil { - log.Error(err, "unable to update status", "backupsession", backupSession) - return err - } - return nil -} - -func RestoreSessionUpdateTargetStatus(state formolv1alpha1.SessionState) error { - log := logger.WithName("RestoreSessionUpdateStatus") - targetName := os.Getenv("TARGET_NAME") - restoreSession := &formolv1alpha1.RestoreSession{} - if err := cl.Get(context.Background(), client.ObjectKey{ - Namespace: os.Getenv("RESTORESESSION_NAMESPACE"), - Name: os.Getenv("RESTORESESSION_NAME"), - }, restoreSession); err != nil { - log.Error(err, "unable to get backupsession", "RESTORESESSION_NAME", os.Getenv("RESTORESESSION_NAME"), "RESTORESESSION_NAMESPACE", os.Getenv("RESTORESESSION_NAMESPACE")) - return err - } - for i, target := range restoreSession.Status.Targets { - if target.Name == targetName { - if target.SessionState == formolv1alpha1.Success { - return errors.New("the restore has already been done. Skipping") - } - restoreSession.Status.Targets[i].SessionState = state - if state == formolv1alpha1.Success || state == formolv1alpha1.Failure { - restoreSession.Status.Targets[i].Duration = &metav1.Duration{Duration: time.Now().Sub(restoreSession.Status.Targets[i].StartTime.Time)} - } - } - } - - if err := cl.Status().Update(context.Background(), restoreSession); err != nil { - log.Error(err, "unable to update restoresession status", "restoresession", restoreSession) - return err - } - return nil -} - -func CreateBackupSession(name string, namespace string) { - log := logger.WithName("CreateBackupSession") - log.V(0).Info("CreateBackupSession called") - backupConfList := &formolv1alpha1.BackupConfigurationList{} - if err := cl.List(context.TODO(), backupConfList, client.InNamespace(namespace)); err != nil { - log.Error(err, "unable to get backupconf") - os.Exit(1) - } - backupConf := &formolv1alpha1.BackupConfiguration{} - for _, bc := range backupConfList.Items { - if bc.Name == name { - *backupConf = bc - } - } - log.V(0).Info("got backupConf", "backupConf", backupConf) - - backupSession := &formolv1alpha1.BackupSession{ - ObjectMeta: metav1.ObjectMeta{ - Name: strings.Join([]string{"backupsession", name, strconv.FormatInt(time.Now().Unix(), 10)}, "-"), - Namespace: namespace, - }, - Spec: formolv1alpha1.BackupSessionSpec{ - Ref: corev1.ObjectReference{ - Name: name, - Namespace: namespace, - }, - }, - } - log.V(1).Info("create backupsession", "backupSession", backupSession) - if err := cl.Create(context.TODO(), backupSession); err != nil { - log.Error(err, "unable to create backupsession") - os.Exit(1) - } -} diff --git a/pkg/utils/root.go b/pkg/utils/root.go deleted file mode 100644 index 7bf5ce7..0000000 --- a/pkg/utils/root.go +++ /dev/null @@ -1,95 +0,0 @@ -package utils - -import ( - "bytes" - formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" - "github.com/go-logr/logr" - "github.com/go-logr/zapr" - "go.uber.org/zap" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "regexp" - "strconv" -) - -var logger logr.Logger - -func init() { - zapLog, _ := zap.NewDevelopment() - logger = zapr.NewLogger(zapLog) -} - -func Run(runCmd string, args []string) error { - log := logger.WithValues("Run", runCmd, "Args", args) - cmd := exec.Command(runCmd, args...) - output, err := cmd.CombinedOutput() - log.V(1).Info("result", "output", string(output)) - if err != nil { - log.Error(err, "something went wrong") - return err - } - return nil -} - -func RunChroot(lookForTag bool, runCmd string, args ...string) error { - log := logger.WithValues("RunChroot", runCmd, "Args", args) - root := regexp.MustCompile(`/proc/[0-9]+/root`) - env := regexp.MustCompile(`/proc/[0-9]+/environ`) - pid := strconv.Itoa(os.Getpid()) - skip := false - if err := filepath.Walk("/proc", func(path string, info os.FileInfo, err error) error { - if skip { - return filepath.SkipDir - } - if err != nil { - return nil - } - if info.IsDir() && (info.Name() == "1" || info.Name() == pid) { - return filepath.SkipDir - } - if lookForTag && env.MatchString(path) { - log.V(0).Info("Looking for tag", "file", path) - content, err := ioutil.ReadFile(path) - if err != nil { - return filepath.SkipDir - } - - var matched bool - for _, envVar := range bytes.Split(content, []byte{'\000'}) { - matched, err = regexp.Match(formolv1alpha1.TARGETCONTAINER_TAG, envVar) - if err != nil { - log.Error(err, "cannot regexp") - return err - } - if matched { - log.V(0).Info("Found the target tag", "file", path) - break - } - } - if matched == false { - return filepath.SkipDir - } - } - if root.MatchString(path) { - if _, err := filepath.EvalSymlinks(path); err != nil { - return filepath.SkipDir - } - log.V(0).Info("running chroot in", "path", path) - cmd := exec.Command("chroot", append([]string{path, runCmd}, args...)...) - output, err := cmd.CombinedOutput() - log.V(0).Info("result", "output", string(output)) - if err != nil { - log.Error(err, "something went wrong") - return err - } - skip = true - return filepath.SkipDir - } - return nil - }); err != nil { - return err - } - return nil -} diff --git a/standalone/root.go b/standalone/root.go new file mode 100644 index 0000000..e4cd051 --- /dev/null +++ b/standalone/root.go @@ -0,0 +1,223 @@ +package standalone + +import ( + "context" + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" + "github.com/desmo999r/formolcli/controllers" + volumesnapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "os" + "os/exec" + "path/filepath" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "strconv" + "strings" + "time" +) + +const ( + BACKUPSESSION_PREFIX = "bs" +) + +var ( + session controllers.Session +) + +func init() { + session.Log = zap.New(zap.UseDevMode(true)) + session.Context = context.Background() + log := session.Log.WithName("InitBackupSession") + ctrl.SetLogger(session.Log) + config, err := rest.InClusterConfig() + if err != nil { + config, err = clientcmd.BuildConfigFromFlags("", filepath.Join(os.Getenv("HOME"), ".kube", "config")) + if err != nil { + log.Error(err, "unable to get config") + os.Exit(1) + } + } + session.Scheme = runtime.NewScheme() + utilruntime.Must(formolv1alpha1.AddToScheme(session.Scheme)) + utilruntime.Must(volumesnapshotv1.AddToScheme(session.Scheme)) + utilruntime.Must(clientgoscheme.AddToScheme(session.Scheme)) + session.Client, err = client.New(config, client.Options{Scheme: session.Scheme}) + if err != nil { + log.Error(err, "unable to get client") + os.Exit(1) + } +} + +func BackupPaths( + backupSessionName string, + backupSessionNamespace string, + targetName string, + paths ...string) error { + log := session.Log.WithName("BackupPaths") + backupResult, err := session.BackupPaths(paths) + log.V(0).Info("Backup Job is over", "target", targetName, "snapshotID", backupResult.SnapshotId, "duration", backupResult.Duration) + if err != nil { + log.Error(err, "unable to backup paths", "paths", paths) + return err + } + backupSession := formolv1alpha1.BackupSession{} + if err := session.Get(session.Context, client.ObjectKey{ + Name: backupSessionName, + Namespace: backupSessionNamespace, + }, &backupSession); err != nil { + log.Error(err, "unable to get backupsession", "name", backupSessionName, "namespace", backupSessionNamespace) + return err + } + for i, target := range backupSession.Status.Targets { + if target.TargetName == targetName { + backupSession.Status.Targets[i].SessionState = formolv1alpha1.Success + backupSession.Status.Targets[i].SnapshotId = backupResult.SnapshotId + backupSession.Status.Targets[i].Duration = &metav1.Duration{Duration: time.Now().Sub(backupSession.Status.Targets[i].StartTime.Time)} + if err := session.Status().Update(session.Context, &backupSession); err != nil { + log.Error(err, "unable to update backupSession status") + return err + } + } + } + // Now find the PVC, VolumeSnapshots with the right label backupsession + // and delete them + vss := volumesnapshotv1.VolumeSnapshotList{} + if err := session.List(session.Context, &vss, client.InNamespace(backupSessionNamespace), client.MatchingLabels{"backupsession": backupSessionName}); err != nil { + log.Error(err, "unable to list the volumesnapshots", "backupsession", backupSessionName) + return err + } + for _, vs := range vss.Items { + if err := session.Delete(session.Context, &vs); err != nil { + log.Error(err, "unable to delete volumesnapshot", "vs", vs.Name) + return err + } + log.V(0).Info("volumesnapshot deleted", "vs", vs.Name) + } + pvcs := corev1.PersistentVolumeClaimList{} + if err := session.List(session.Context, &pvcs, client.InNamespace(backupSessionNamespace), client.MatchingLabels{"backupsession": backupSessionName}); err != nil { + log.Error(err, "unable to list the PVCs", "backupsession", backupSessionName) + return err + } + for _, pvc := range pvcs.Items { + if err := session.Delete(session.Context, &pvc); err != nil { + log.Error(err, "unable to delete PVC", "pvc", pvc.Name) + return err + } + log.V(0).Info("PVC deleted", "pvc", pvc.Name) + } + return nil +} + +func StartRestore( + restoreSessionName string, + restoreSessionNamespace string, + targetName string) { + log := session.Log.WithName("StartRestore") + if err := session.CheckRepo(); err != nil { + log.Error(err, "unable to check Repo") + return + } + restoreSession := formolv1alpha1.RestoreSession{} + if err := session.Get(session.Context, client.ObjectKey{ + Name: restoreSessionName, + Namespace: restoreSessionNamespace, + }, &restoreSession); err != nil { + log.Error(err, "unable to get restoresession", "name", restoreSessionName, "namespace", restoreSessionNamespace) + return + } + backupSession := formolv1alpha1.BackupSession{ + Spec: restoreSession.Spec.BackupSessionRef.Spec, + Status: restoreSession.Spec.BackupSessionRef.Status, + } + for i, target := range backupSession.Status.Targets { + if target.TargetName == targetName { + + log.V(0).Info("StartRestore called", "restoring snapshot", target.SnapshotId) + cmd := exec.Command(controllers.RESTIC_EXEC, "restore", target.SnapshotId, "--target", "/") + // the restic restore command does not support JSON output + if output, err := cmd.CombinedOutput(); err != nil { + log.Error(err, "unable to restore snapshot", "output", output) + restoreSession.Status.Targets[i].SessionState = formolv1alpha1.Failure + } else { + restoreSession.Status.Targets[i].SessionState = formolv1alpha1.Waiting + log.V(0).Info("restore was a success. Moving to waiting state", "target", target.TargetName) + } + if err := session.Status().Update(session.Context, &restoreSession); err != nil { + log.Error(err, "unable to update RestoreSession", "restoreSession", restoreSession) + return + } + log.V(0).Info("restore over. removing the initContainer") + targetObject, targetPodSpec := formolv1alpha1.GetTargetObjects(target.TargetKind) + if err := session.Get(session.Context, client.ObjectKey{ + Namespace: restoreSessionNamespace, + Name: target.TargetName, + }, targetObject); err != nil { + log.Error(err, "unable to get target objects", "target", target.TargetName) + return + } + initContainers := []corev1.Container{} + for _, c := range targetPodSpec.InitContainers { + if c.Name == formolv1alpha1.RESTORECONTAINER_NAME { + continue + } + initContainers = append(initContainers, c) + } + targetPodSpec.InitContainers = initContainers + if err := session.Update(session.Context, targetObject); err != nil { + log.Error(err, "unable to remove the restore initContainer", "targetObject", targetObject) + return + } + break + } + } +} + +func CreateBackupSession(ref corev1.ObjectReference) { + log := session.Log.WithName("CreateBackupSession") + log.V(0).Info("CreateBackupSession called") + + backupSession := &formolv1alpha1.BackupSession{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.Join([]string{BACKUPSESSION_PREFIX, ref.Name, strconv.FormatInt(time.Now().Unix(), 10)}, "-"), + Namespace: ref.Namespace, + }, + Spec: formolv1alpha1.BackupSessionSpec{ + Ref: ref, + }, + } + log.V(1).Info("create backupsession", "backupSession", backupSession) + if err := session.Create(session.Context, backupSession); err != nil { + log.Error(err, "unable to create backupsession") + os.Exit(1) + } +} + +func DeleteSnapshot(namespace string, name string, snapshotId string) { + log := session.Log.WithName("DeleteSnapshot") + session.Namespace = namespace + backupConf := formolv1alpha1.BackupConfiguration{} + if err := session.Get(session.Context, client.ObjectKey{ + Namespace: namespace, + Name: name, + }, &backupConf); err != nil { + log.Error(err, "unable to get the BackupConf") + return + } + if err := session.SetResticEnv(backupConf); err != nil { + log.Error(err, "unable to set the restic env") + return + } + log.V(0).Info("deleting restic snapshot", "snapshotId", snapshotId) + cmd := exec.Command(controllers.RESTIC_EXEC, "forget", "--prune", snapshotId) + _, err := cmd.CombinedOutput() + if err != nil { + log.Error(err, "unable to delete snapshot", "snapshoId", snapshotId) + } +}