Skip to content

Commit

Permalink
Support remote backends (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
tamalsaha authored Jun 23, 2017
1 parent 6e13780 commit 25d006a
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 82 deletions.
19 changes: 19 additions & 0 deletions api/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package api

import "fmt"

func (r LocalSpec) Repository() string {
return r.Path
}

func (r S3Spec) Repository() string {
return fmt.Sprintf("s3:%s:%s:%s", r.Endpoint, r.Bucket, r.Prefix)
}

func (r GCSSpec) Repository() string {
return fmt.Sprintf("gs:%s:%s:%s", r.Location, r.Bucket, r.Prefix)
}

func (r AzureSpec) Repository() string {
return fmt.Sprintf("azure:%s:%s", r.Container, r.Prefix)
}
63 changes: 43 additions & 20 deletions api/restic_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,10 @@ type Restic struct {
}

type ResticSpec struct {
// Selector is a label query over a set of resources, in this case pods.
// Required.
Selector metav1.LabelSelector `json:"selector,omitempty"`
// Source of the backup volumeName:path
Source Source `json:"source"`
// Destination of the backup
Destination Destination `json:"destination"`
// How frequently restic command will be run
Schedule string `json:"schedule"`
// Tags of a snapshots
Tags []string `json:"tags,omitempty"`
// retention policy of snapshots
RetentionPolicy RetentionPolicy `json:"retentionPolicy,omitempty"`
Selector metav1.LabelSelector `json:"selector,omitempty"`
FileGroups []FileGroup `json:"fileGroups,omitempty"`
Backend Backend `json:"backend,omitempty"`
Schedule string `json:"schedule,omitempty"`
}

type ResticStatus struct {
Expand All @@ -53,15 +44,48 @@ type ResticList struct {
Items []Restic `json:"items,omitempty"`
}

type FileGroup struct {
// Source of the backup volumeName:path
Path string `json:"path,omitempty"`
// Tags of a snapshots
Tags []string `json:"tags,omitempty"`
// retention policy of snapshots
RetentionPolicy RetentionPolicy `json:"retentionPolicy,omitempty"`
}

type Source struct {
VolumeName string `json:"volumeName"`
Path string `json:"path"`
VolumeName string `json:"volumeName,omitempty"`
Path string `json:"path,omitempty"`
}

type Backend struct {
Local *LocalSpec `json:"local"`
S3 *S3Spec `json:"s3,omitempty"`
GCS *GCSSpec `json:"gcs,omitempty"`
Azure *AzureSpec `json:"azure,omitempty"`
RepositorySecretName string `json:"repositorySecretName,omitempty"`
}

type LocalSpec struct {
Volume apiv1.Volume `json:"volume,omitempty"`
Path string `json:"path,omitempty"`
}

type S3Spec struct {
Endpoint string `json:"endpoint,omitempty"`
Bucket string `json:"bucket,omiempty"`
Prefix string `json:"prefix,omitempty"`
}

type GCSSpec struct {
Location string `json:"location,omitempty"`
Bucket string `json:"bucket,omiempty"`
Prefix string `json:"prefix,omitempty"`
}

type Destination struct {
Volume apiv1.Volume `json:"volume"`
Path string `json:"path"`
RepositorySecretName string `json:"repositorySecretName"`
type AzureSpec struct {
Container string `json:"container,omitempty"`
Prefix string `json:"prefix,omitempty"`
}

type RetentionPolicy struct {
Expand All @@ -72,5 +96,4 @@ type RetentionPolicy struct {
KeepMonthlySnapshots int `json:"keepMonthlySnapshots,omitempty"`
KeepYearlySnapshots int `json:"keepYearlySnapshots,omitempty"`
KeepTags []string `json:"keepTags,omitempty"`
RetainHostname string `json:"retainHostname,omitempty"`
}
57 changes: 57 additions & 0 deletions pkg/restic/restic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package restic

import (
"errors"
"os"

sapi "github.com/appscode/stash/api"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientset "k8s.io/client-go/kubernetes"
)

const (
RESTIC_REPOSITORY = "RESTIC_REPOSITORY"
RESTIC_PASSWORD = "RESTIC_PASSWORD"

AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"
AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"

GOOGLE_PROJECT_ID = "GOOGLE_PROJECT_ID"
GOOGLE_APPLICATION_CREDENTIALS = "GOOGLE_APPLICATION_CREDENTIALS"

ACCOUNT_NAME = "ACCOUNT_NAME"
ACCOUNT_KEY = "ACCOUNT_KEY"
)

func PrepareEnv(client clientset.Interface, resource *sapi.Restic) error {
if resource.Spec.Backend.RepositorySecretName == "" {
return errors.New("Missing repository secret name")
}
secret, err := client.CoreV1().Secrets(resource.Namespace).Get(resource.Spec.Backend.RepositorySecretName, metav1.GetOptions{})
if err != nil {
return err
}
if v, ok := secret.Data[RESTIC_PASSWORD]; !ok {
return errors.New("Missing repository password")
} else {
os.Setenv(RESTIC_PASSWORD, string(v))
}

if resource.Spec.Backend.Local != nil {
// TODO: suffix pod for statefulsets
os.Setenv(RESTIC_REPOSITORY, resource.Spec.Backend.Local.Repository())
} else if resource.Spec.Backend.S3 != nil {
os.Setenv(RESTIC_REPOSITORY, resource.Spec.Backend.S3.Repository())
os.Setenv(AWS_ACCESS_KEY_ID, string(secret.Data[AWS_ACCESS_KEY_ID]))
os.Setenv(AWS_SECRET_ACCESS_KEY, string(secret.Data[AWS_SECRET_ACCESS_KEY]))
} else if resource.Spec.Backend.GCS != nil {
os.Setenv(RESTIC_REPOSITORY, resource.Spec.Backend.GCS.Repository())
os.Setenv(GOOGLE_PROJECT_ID, string(secret.Data[GOOGLE_PROJECT_ID]))
os.Setenv(GOOGLE_APPLICATION_CREDENTIALS, string(secret.Data[GOOGLE_APPLICATION_CREDENTIALS])) // TODO; Write the File first
} else if resource.Spec.Backend.S3 != nil {
os.Setenv(RESTIC_REPOSITORY, resource.Spec.Backend.Azure.Repository())
os.Setenv(ACCOUNT_NAME, string(secret.Data[ACCOUNT_NAME]))
os.Setenv(ACCOUNT_KEY, string(secret.Data[ACCOUNT_KEY]))
}
return nil
}
168 changes: 106 additions & 62 deletions pkg/scheduler/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
sapi "github.com/appscode/stash/api"
scs "github.com/appscode/stash/client/clientset"
"github.com/appscode/stash/pkg/eventer"
"github.com/appscode/stash/pkg/restic"
"gopkg.in/robfig/cron.v2"
kerr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -58,6 +59,31 @@ func NewController(kubeClient clientset.Interface, stashClient scs.ExtensionInte
}
}

// Init and/or connect to repo
func (c *controller) InitRepo() error {
resource, err := c.StashClient.Restics(c.resourceNamespace).Get(c.resourceName)
if err != nil {
return err
}
data, err := c.getRepositorySecret(resource.Spec.Backend.RepositorySecretName, resource.Namespace)
if err != nil {
return err
}

err = os.Setenv(RESTIC_PASSWORD, string(data[Password]))
if err != nil {
return err
}
repo := resource.Spec.Backend.Local.Path
_, err = os.Stat(filepath.Join(repo, "config"))
if os.IsNotExist(err) {
if _, err = execLocal(fmt.Sprintf("/restic init --repo %s", repo)); err != nil {
return err
}
}
return nil
}

func (c *controller) RunAndHold() {
c.cron.Start()

Expand Down Expand Up @@ -141,25 +167,32 @@ func (c *controller) configureScheduler() error {
c.cron = cron.New()
}

password, err := getPasswordFromSecret(c.KubeClient, r.Spec.Destination.RepositorySecretName, r.Namespace)
if err != nil {
return err
}
err = os.Setenv(RESTIC_PASSWORD, password)
err := restic.PrepareEnv(c.KubeClient, r)
if err != nil {
return err
}
repo := r.Spec.Destination.Path
_, err = os.Stat(filepath.Join(repo, "config"))
if os.IsNotExist(err) {
if _, err = execLocal(fmt.Sprintf("/restic init --repo %s", repo)); err != nil {
return err
}
}

//password, err := getPasswordFromSecret(c.KubeClient, r.Spec.Backend.RepositorySecretName, r.Namespace)
//if err != nil {
// return err
//}
//err = os.Setenv(RESTIC_PASSWORD, password)
//if err != nil {
// return err
//}
//repo := r.Spec.Backend.Path
//_, err = os.Stat(filepath.Join(repo, "config"))
//if os.IsNotExist(err) {
// if _, err = execLocal(fmt.Sprintf("/restic init --repo %s", repo)); err != nil {
// return err
// }
//}

// Remove previous jobs
for _, v := range c.cron.Entries() {
c.cron.Remove(v.ID)
}

interval := r.Spec.Schedule
if _, err = cron.Parse(interval); err != nil {
log.Errorln(err)
Expand Down Expand Up @@ -210,15 +243,6 @@ func (c *controller) runOnce() error {
return fmt.Errorf("Restic %s@%s version %s does not match expected version %s", resource.Name, resource.Namespace, resource.ResourceVersion, c.resourceVersion)
}

password, err := getPasswordFromSecret(c.KubeClient, resource.Spec.Destination.RepositorySecretName, resource.Namespace)
if err != nil {
return err
}
err = os.Setenv(RESTIC_PASSWORD, password)
if err != nil {
return err
}

err = c.runBackup(resource)
if err != nil {
log.Errorln("Backup operation failed for Reestic %s@%s due to %s", resource.Name, resource.Namespace, err)
Expand All @@ -240,17 +264,19 @@ func (c *controller) runOnce() error {

func (c *controller) runBackup(resource *sapi.Restic) error {
startTime := metav1.Now()
cmd := fmt.Sprintf("/restic -r %s backup %s", resource.Spec.Destination.Path, resource.Spec.Source.Path)
// add tags if any
for _, t := range resource.Spec.Tags {
cmd = cmd + " --tag " + t
}
// Force flag
cmd = cmd + " --force"
for _, fg := range resource.Spec.FileGroups {
cmd := fmt.Sprintf("/restic backup %s", fg.Path)
// add tags if any
for _, tag := range fg.Tags {
cmd = cmd + " --tag " + tag
}
// Force flag
cmd = cmd + " --force"

_, err := execLocal(cmd)
if err != nil {
return err
_, err := execLocal(cmd)
if err != nil {
return err
}
}
endTime := metav1.Now()

Expand All @@ -260,46 +286,52 @@ func (c *controller) runBackup(resource *sapi.Restic) error {
resource.Status.FirstBackupTime = &startTime
}
resource.Status.LastBackupDuration = endTime.Sub(startTime.Time).String()
_, err = c.StashClient.Restics(resource.Namespace).Update(resource)
_, err := c.StashClient.Restics(resource.Namespace).Update(resource)
if err != nil {
log.Errorf("Failed to update status for Restic %s@%s due to %s", resource.Name, resource.Namespace, err)
}
return nil
}

func forgetSnapshots(r *sapi.Restic) error {
cmd := fmt.Sprintf("/restic -r %s forget", r.Spec.Destination.Path)
if r.Spec.RetentionPolicy.KeepLastSnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepLast, r.Spec.RetentionPolicy.KeepLastSnapshots)
}
if r.Spec.RetentionPolicy.KeepHourlySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepHourly, r.Spec.RetentionPolicy.KeepHourlySnapshots)
}
if r.Spec.RetentionPolicy.KeepDailySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepDaily, r.Spec.RetentionPolicy.KeepDailySnapshots)
}
if r.Spec.RetentionPolicy.KeepWeeklySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepWeekly, r.Spec.RetentionPolicy.KeepWeeklySnapshots)
}
if r.Spec.RetentionPolicy.KeepMonthlySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepMonthly, r.Spec.RetentionPolicy.KeepMonthlySnapshots)
}
if r.Spec.RetentionPolicy.KeepYearlySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepYearly, r.Spec.RetentionPolicy.KeepYearlySnapshots)
}
if len(r.Spec.RetentionPolicy.KeepTags) != 0 {
for _, t := range r.Spec.RetentionPolicy.KeepTags {
cmd = cmd + " --keep-tag " + t
for _, fg := range r.Spec.FileGroups {
cmd := "/restic forget"
if fg.RetentionPolicy.KeepLastSnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepLast, fg.RetentionPolicy.KeepLastSnapshots)
}
if fg.RetentionPolicy.KeepHourlySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepHourly, fg.RetentionPolicy.KeepHourlySnapshots)
}
if fg.RetentionPolicy.KeepDailySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepDaily, fg.RetentionPolicy.KeepDailySnapshots)
}
if fg.RetentionPolicy.KeepWeeklySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepWeekly, fg.RetentionPolicy.KeepWeeklySnapshots)
}
if fg.RetentionPolicy.KeepMonthlySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepMonthly, fg.RetentionPolicy.KeepMonthlySnapshots)
}
if fg.RetentionPolicy.KeepYearlySnapshots > 0 {
cmd = fmt.Sprintf("%s --%s %d", cmd, sapi.KeepYearly, fg.RetentionPolicy.KeepYearlySnapshots)
}
if len(fg.RetentionPolicy.KeepTags) != 0 {
for _, t := range fg.RetentionPolicy.KeepTags {
cmd = cmd + " --keep-tag " + t
}
}
// Debug
//if len(fg.RetentionPolicy.RetainHostname) != 0 {
// cmd = cmd + " --hostname " + fg.RetentionPolicy.RetainHostname
//}
for _, t := range fg.Tags {
cmd = cmd + " --tag " + t
}
_, err := execLocal(cmd)
if err != nil {
return err
}
}
if len(r.Spec.RetentionPolicy.RetainHostname) != 0 {
cmd = cmd + " --hostname " + r.Spec.RetentionPolicy.RetainHostname
}
for _, t := range r.Spec.Tags {
cmd = cmd + " --tag " + t
}
_, err := execLocal(cmd)
return err
return nil
}

func execLocal(s string) (string, error) {
Expand All @@ -321,3 +353,15 @@ func getPasswordFromSecret(client clientset.Interface, secretName, namespace str
}
return string(password), nil
}

func (c *controller) getRepositorySecret(secretName, namespace string) (map[string]string, error) {
secret, err := c.KubeClient.CoreV1().Secrets(namespace).Get(secretName, metav1.GetOptions{})
if err != nil {
return nil, err
}
data := make(map[string]string)
for k, v := range secret.Data {
data[k] = string(v)
}
return data, nil
}
File renamed without changes.

0 comments on commit 25d006a

Please sign in to comment.