diff --git a/Dockerfile b/Dockerfile index c4443c1..32ee8e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,10 +27,12 @@ WORKDIR /dist RUN cp /build/formolcli . # Build a small image -FROM scratch +FROM arm32v7/alpine:3.12 -COPY --from=builder /dist/formolcli / +RUN apk add --no-cache restic +COPY bin/restic /usr/local/bin +COPY --from=builder /dist/formolcli /usr/local/bin # Command to run -ENTRYPOINT ["/formolcli"] +ENTRYPOINT ["/usr/local/bin/formolcli"] CMD ["--help"] diff --git a/bin/restic b/bin/restic new file mode 100755 index 0000000..47658a3 Binary files /dev/null and b/bin/restic differ diff --git a/manifests/formolcli-rbac.yaml b/manifests/formolcli-rbac.yaml index 1f55aa2..5795222 100644 --- a/manifests/formolcli-rbac.yaml +++ b/manifests/formolcli-rbac.yaml @@ -13,6 +13,17 @@ metadata: --- 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"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole metadata: name: backupsession-creator labels: @@ -37,3 +48,19 @@ subjects: 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/src/backup/root.go b/src/backup/root.go new file mode 100644 index 0000000..61129f7 --- /dev/null +++ b/src/backup/root.go @@ -0,0 +1,61 @@ +package backup + +import ( + "strings" + "os" + "os/exec" + "log" +) + +var ( + repository string + passwordFile string + aws_access_key_id string + aws_secret_access_key string + resticExec = "/usr/local/bin/restic" +) + +func init() { + if repository = os.Getenv("RESTIC_REPOSITORY"); repository == "" { + log.Fatal("RESTIC_REPOSITORY not set") + } + if passwordFile = os.Getenv("RESTIC_PASSWORD"); passwordFile == "" { + log.Fatal("RESTIC_PASSWORD not set") + } + if aws_access_key_id = os.Getenv("AWS_ACCESS_KEY_ID"); aws_access_key_id == "" { + log.Fatal("AWS_ACCESS_KEY_ID not set") + } + if aws_secret_access_key = os.Getenv("AWS_SECRET_ACCESS_KEY"); aws_secret_access_key == "" { + log.Fatal("AWS_SECRET_ACCESS_KEY not set") + } +} + +func checkRepo(repo string) error { + cmd := exec.Command(resticExec, "check", "-r", repo) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + err := cmd.Run() + if err != nil { + cmd = exec.Command(resticExec, "init", "-r", repo) + err = cmd.Run() + } + return err +} + +func BackupVolume(path string) error { + return nil +} + +func BackupDeployment(prefix string, paths []string) error { + newrepo := repository + if prefix != "" { + newrepo = repository + "/" + prefix + } + if err := checkRepo(newrepo); err != nil { + log.Fatal("unable to setup newrepo", "newrepo", newrepo) + return err + } + cmd := exec.Command(resticExec, "backup", "-r", newrepo, strings.Join(paths, " ")) + + return cmd.Run() +} diff --git a/src/cmd/backup.go b/src/cmd/backup.go new file mode 100644 index 0000000..af23db7 --- /dev/null +++ b/src/cmd/backup.go @@ -0,0 +1,51 @@ +/* +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" +) + +// backupCmd represents the backup command +var backupCmd = &cobra.Command{ + Use: "backup", + 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) { + fmt.Println("backup called") + }, +} + +func init() { + rootCmd.AddCommand(backupCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // backupCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // backupCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/src/cmd/server.go b/src/cmd/server.go new file mode 100644 index 0000000..35917da --- /dev/null +++ b/src/cmd/server.go @@ -0,0 +1,53 @@ +/* +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" + "github.com/desmo999r/formolcli/create" +) + +// serverCmd represents the server command +var serverCmd = &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) { + fmt.Println("server called") + create.Server() + }, +} + +func init() { + createCmd.AddCommand(serverCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // serverCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/src/cmd/volume.go b/src/cmd/volume.go new file mode 100644 index 0000000..3ec244e --- /dev/null +++ b/src/cmd/volume.go @@ -0,0 +1,56 @@ +/* +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" + "github.com/desmo999r/formolcli/backup" +) + +// pvcCmd represents the pvc command +var volumeCmd = &cobra.Command{ + Use: "volume", + 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) { + fmt.Println("volume called") + path, _ := cmd.Flags().GetString("path") + _ = backup.BackupVolume(path) + }, +} + +func init() { + backupCmd.AddCommand(volumeCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // pvcCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // pvcCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + volumeCmd.Flags().String("path", "", "Path to the data to backup") + volumeCmd.MarkFlagRequired("path") +} diff --git a/src/controllers/backupsession_controller.go b/src/controllers/backupsession_controller.go new file mode 100644 index 0000000..400b1a4 --- /dev/null +++ b/src/controllers/backupsession_controller.go @@ -0,0 +1,113 @@ +package controllers + +import ( + "context" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "os" + "path/filepath" + + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "github.com/desmo999r/formolcli/backup" +) + +var ( + deploymentName = "" +) + +func init() { + namespace := os.Getenv("POD_NAMESPACE") + if namespace == "" { + panic("No POD_NAMESPACE env var") + } + config, err := rest.InClusterConfig() + if err != nil { + config, err = clientcmd.BuildConfigFromFlags("", filepath.Join(os.Getenv("HOME"), ".kube", "config",)) + if err != nil { + panic(err.Error()) + } + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + panic("unable to get clientset") + } + + hostname := os.Getenv("POD_NAME") + if hostname == "" { + panic("unable to get hostname") + } + + pod, err := clientset.CoreV1().Pods(namespace).Get(hostname, metav1.GetOptions{}) + if err != nil { + panic("unable to get pod") + } + + podOwner := metav1.GetControllerOf(pod) + replicasetList, err := clientset.AppsV1().ReplicaSets(namespace).List(metav1.ListOptions{ + FieldSelector: "metadata.name=" + string(podOwner.Name), + }) + if err != nil { + panic("unable to get replicaset" + err.Error()) + } + for _, replicaset := range replicasetList.Items { + replicasetOwner := metav1.GetControllerOf(&replicaset) + deploymentName = replicasetOwner.Name + } +} + +// BackupSessionReconciler reconciles a BackupSession object +type BackupSessionReconciler struct { + client.Client + Log logr.Logger + Scheme *runtime.Scheme +} + +func (r *BackupSessionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + ctx := context.Background() + 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 ctrl.Result{}, client.IgnoreNotFound(err) + } + + log.V(1).Info("backupSession.Namespace", "namespace", backupSession.Namespace) + log.V(1).Info("backupSession.Spec.Ref.Name", "name", backupSession.Spec.Ref.Name) + 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 ctrl.Result{}, client.IgnoreNotFound(err) + } + + log.V(1).Info("Found BackupConfiguration", "BackupConfiguration", backupConf) + + // Found the BackupConfiguration. + switch backupConf.Spec.Target.Kind { + case "Deployment": + if err := backup.BackupDeployment("", backupConf.Spec.Paths); err != nil { + log.Error(err, "unable to backup deployment") + return ctrl.Result{}, nil + } + default: + return ctrl.Result{}, nil + } + return ctrl.Result{}, nil +} + +func (r *BackupSessionReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&formolv1alpha1.BackupSession{}). + Complete(r) +} + diff --git a/src/create/root.go b/src/create/root.go index c4a451a..48c4a25 100644 --- a/src/create/root.go +++ b/src/create/root.go @@ -39,7 +39,6 @@ func CreateBackupSession(name string, namespace string) { Spec: formolv1alpha1.BackupSessionSpec{ Ref: formolv1alpha1.Ref{ Name: name, - Namespace: namespace, }, }, Status: formolv1alpha1.BackupSessionStatus{}, diff --git a/src/create/server.go b/src/create/server.go new file mode 100644 index 0000000..bda7a34 --- /dev/null +++ b/src/create/server.go @@ -0,0 +1,62 @@ +package create + +import ( + "os" + "flag" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "github.com/desmo999r/formolcli/controllers" + formolv1alpha1 "github.com/desmo999r/formol/api/v1alpha1" +) + +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", + }) + if err != nil { + setupLog.Error(err, "unable to create manager") + os.Exit(1) + } + + 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) + } + + 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/src/go.mod b/src/go.mod index cff51d0..9f89f1d 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,11 +3,15 @@ module github.com/desmo999r/formolcli go 1.14 require ( - github.com/desmo999r/formol v0.0.0-20201125193419-7fbf7274cbde + github.com/desmo999r/formol v0.1.4 + github.com/go-logr/logr v0.1.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/spf13/cobra v1.1.1 - github.com/spf13/viper v1.7.1 - k8s.io/apimachinery v0.18.6 - k8s.io/client-go v0.18.6 - sigs.k8s.io/controller-runtime v0.6.4 + github.com/onsi/ginkgo v1.11.0 + github.com/onsi/gomega v1.8.1 + github.com/spf13/cobra v0.0.5 + github.com/spf13/viper v1.3.2 + k8s.io/api v0.17.2 + k8s.io/apimachinery v0.17.2 + k8s.io/client-go v0.17.2 + sigs.k8s.io/controller-runtime v0.5.0 )