Skip to content
This repository was archived by the owner on Mar 28, 2020. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkg/apis/etcd/v1beta2/backup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ type BackupPolicy struct {
// BackupIntervalInSecond is to specify how often operator take snapshot
// 0 is magic number to indicate one-shot backup
BackupIntervalInSecond int64 `json:"backupIntervalInSecond,omitempty"`
// MaxBackups is to specify how many backups we want to keep
// 0 is magic number to indicate un-limited backups
MaxBackups int `json:"maxBackups,omitempty"`
}

// BackupStatus represents the status of the EtcdBackup Custom Resource.
Expand Down
22 changes: 22 additions & 0 deletions pkg/backup/backup_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"crypto/tls"
"fmt"
"sort"
"time"

"github.com/coreos/etcd-operator/pkg/backup/writer"
Expand Down Expand Up @@ -77,6 +78,27 @@ func (bm *BackupManager) SaveSnap(ctx context.Context, s3Path string, now time.T
return rev, resp.Version, nil
}

// EnsureMaxbackup to ensure the number of snapshot is under maxcount
// if the number of snapshot exceeded than maxcount, delete oldest snapshot
func (bm *BackupManager) EnsureMaxbackup(ctx context.Context, s3Path string, maxCount int) error {

Comment thread
hasbro17 marked this conversation as resolved.
Outdated
savedSnapShots, err := bm.bw.List(ctx, s3Path)
Comment thread
hexfusion marked this conversation as resolved.
Outdated
if err != nil {
return fmt.Errorf("failed to get exisiting snapashots: %v", err)
}
Comment thread
hexfusion marked this conversation as resolved.
sort.Sort(sort.Reverse(sort.StringSlice(savedSnapShots)))
for i, snapshotPath := range savedSnapShots {
if i < maxCount {
continue
}
err := bm.bw.Delete(ctx, snapshotPath)
if err != nil {
return fmt.Errorf("failed to delete snapshot: %v", err)
}
}
return nil
}

// etcdClientWithMaxRevision gets the etcd endpoint with the maximum kv store revision
// and returns the etcd client of that member.
func (bm *BackupManager) etcdClientWithMaxRevision(ctx context.Context) (*clientv3.Client, int64, error) {
Expand Down
47 changes: 47 additions & 0 deletions pkg/backup/writer/abs_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,50 @@ func (absw *absWriter) Write(ctx context.Context, path string, r io.Reader) (int

return blob.Properties.ContentLength, nil
}

func (absw *absWriter) List(ctx context.Context, basePath string) ([]string, error) {
// TODO: support context.
container, _, err := util.ParseBucketAndKey(basePath)
if err != nil {
return nil, err
}

containerRef := absw.abs.GetContainerReference(container)
containerExists, err := containerRef.Exists()
if err != nil {
return nil, err
}
if !containerExists {
return nil, fmt.Errorf("container %v does not exist", container)
}

blobs, err := containerRef.ListBlobs(
storage.ListBlobsParameters{Prefix: basePath})
if err != nil {
return nil, err
}
blobKeys := []string{}
for _, blob := range blobs.Blobs {
blobKeys = append(blobKeys, container+"/"+blob.Name)
}
return blobKeys, nil
}

func (absw *absWriter) Delete(ctx context.Context, path string) error {
// TODO: support context.
container, key, err := util.ParseBucketAndKey(path)
if err != nil {
return err
}
containerRef := absw.abs.GetContainerReference(container)
containerExists, err := containerRef.Exists()
if err != nil {
return err
}
if !containerExists {
return fmt.Errorf("container %v does not exist", container)
}

blob := containerRef.GetBlobReference(key)
return blob.Delete(&storage.DeleteBlobOptions{})
}
35 changes: 35 additions & 0 deletions pkg/backup/writer/gcs_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"cloud.google.com/go/storage"
"github.com/sirupsen/logrus"
"google.golang.org/api/iterator"
)

var _ Writer = &gcsWriter{}
Expand Down Expand Up @@ -58,3 +59,37 @@ func (gcsw *gcsWriter) Write(ctx context.Context, path string, r io.Reader) (int
}
return n, err
}

func (gcsw *gcsWriter) List(ctx context.Context, basePath string) ([]string, error) {
bucket, key, err := util.ParseBucketAndKey(basePath)
if err != nil {
return nil, err
}
objects := gcsw.gcs.Bucket(bucket).Objects(ctx, &storage.Query{Prefix: key})
if objects == nil {
return nil, fmt.Errorf("failed to get objects having %s prefix", key)
}

objectKeys := []string{}

for {
objAttrs, err := objects.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
objectKeys = append(objectKeys, bucket+"/"+objAttrs.Name)
}
return objectKeys, nil
}

func (gcsw *gcsWriter) Delete(ctx context.Context, path string) error {
bucket, key, err := util.ParseBucketAndKey(path)
if err != nil {
return err
}

return gcsw.gcs.Bucket(bucket).Object(key).Delete(ctx)
}
36 changes: 36 additions & 0 deletions pkg/backup/writer/s3_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,39 @@ func (s3w *s3Writer) Write(ctx context.Context, path string, r io.Reader) (int64
}
return *resp.ContentLength, nil
}

// List return the file paths which match the given s3 path
func (s3w *s3Writer) List(ctx context.Context, basePath string) ([]string, error) {
bk, key, err := util.ParseBucketAndKey(basePath)
if err != nil {
return nil, err
}

objects, err := s3w.s3.ListObjectsWithContext(ctx,
&s3.ListObjectsInput{
Bucket: aws.String(bk),
Prefix: aws.String(key),
})
if err != nil {
return nil, err
}
objectKeys := []string{}
for _, object := range objects.Contents {
objectKeys = append(objectKeys, bk+"/"+ *object.Key)
}
Comment thread
hexfusion marked this conversation as resolved.
return objectKeys, nil
}

func (s3w *s3Writer) Delete(ctx context.Context, path string) error {
bk, key, err := util.ParseBucketAndKey(path)
if err != nil {
return err
}

_, err = s3w.s3.DeleteObjectWithContext(ctx,
&s3.DeleteObjectInput{
Bucket: aws.String(bk),
Key: aws.String(key),
})
return err
}
6 changes: 6 additions & 0 deletions pkg/backup/writer/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ import (
type Writer interface {
// Write writes a backup file to the given path and returns size of written file.
Write(ctx context.Context, path string, r io.Reader) (int64, error)

// List a backup files
List(ctx context.Context, basePath string) ([]string, error)
Comment thread
hexfusion marked this conversation as resolved.

// Delete a backup file
Delete(ctx context.Context, path string) error
}
8 changes: 7 additions & 1 deletion pkg/controller/backup-operator/abs_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
)

// handleABS saves etcd cluster's backup to specificed ABS path.
func handleABS(ctx context.Context, kubecli kubernetes.Interface, s *api.ABSBackupSource, endpoints []string, clientTLSSecret, namespace string) (*api.BackupStatus, error) {
func handleABS(ctx context.Context, kubecli kubernetes.Interface, s *api.ABSBackupSource, endpoints []string, clientTLSSecret, namespace string, maxBackup int) (*api.BackupStatus, error) {
// TODO: controls NewClientFromSecret with ctx. This depends on upstream kubernetes to support API calls with ctx.
cli, err := absfactory.NewClientFromSecret(kubecli, namespace, s.ABSSecret)
if err != nil {
Expand All @@ -47,5 +47,11 @@ func handleABS(ctx context.Context, kubecli kubernetes.Interface, s *api.ABSBack
if err != nil {
Comment thread
hexfusion marked this conversation as resolved.
return nil, fmt.Errorf("failed to save snapshot (%v)", err)
}
if maxBackup > 0 {
err := bm.EnsureMaxbackup(ctx, s.Path, maxBackup)
if err != nil {
return nil, fmt.Errorf("succeeded in saving snapshot but failed to delete old snapshot (%v)", err)
}
}
return &api.BackupStatus{EtcdVersion: etcdVersion, EtcdRevision: rev, LastSuccessDate: now}, nil
}
8 changes: 7 additions & 1 deletion pkg/controller/backup-operator/gcs_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
)

// handleGCS saves etcd cluster's backup to specificed GCS path.
func handleGCS(ctx context.Context, kubecli kubernetes.Interface, s *api.GCSBackupSource, endpoints []string, clientTLSSecret, namespace string) (*api.BackupStatus, error) {
func handleGCS(ctx context.Context, kubecli kubernetes.Interface, s *api.GCSBackupSource, endpoints []string, clientTLSSecret, namespace string, maxBackup int) (*api.BackupStatus, error) {
// TODO: controls NewClientFromSecret with ctx. This depends on upstream kubernetes to support API calls with ctx.
cli, err := gcsfactory.NewClientFromSecret(ctx, kubecli, namespace, s.GCPSecret)
if err != nil {
Expand All @@ -48,5 +48,11 @@ func handleGCS(ctx context.Context, kubecli kubernetes.Interface, s *api.GCSBack
if err != nil {
return nil, fmt.Errorf("failed to save snapshot (%v)", err)
}
if maxBackup > 0 {
err := bm.EnsureMaxbackup(ctx, s.Path, maxBackup)
if err != nil {
return nil, fmt.Errorf("succeeded in saving snapshot but failed to delete old snapshot (%v)", err)
}
}
return &api.BackupStatus{EtcdVersion: etcdVersion, EtcdRevision: rev, LastSuccessDate: now}, nil
}
8 changes: 7 additions & 1 deletion pkg/controller/backup-operator/s3_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (

// TODO: replace this with generic backend interface for other options (PV, Azure)
// handleS3 saves etcd cluster's backup to specificed S3 path.
func handleS3(ctx context.Context, kubecli kubernetes.Interface, s *api.S3BackupSource, endpoints []string, clientTLSSecret, namespace string) (*api.BackupStatus, error) {
func handleS3(ctx context.Context, kubecli kubernetes.Interface, s *api.S3BackupSource, endpoints []string, clientTLSSecret, namespace string, maxBackup int) (*api.BackupStatus, error) {
// TODO: controls NewClientFromSecret with ctx. This depends on upstream kubernetes to support API calls with ctx.
cli, err := s3factory.NewClientFromSecret(kubecli, namespace, s.Endpoint, s.AWSSecret)
if err != nil {
Expand All @@ -49,5 +49,11 @@ func handleS3(ctx context.Context, kubecli kubernetes.Interface, s *api.S3Backup
if err != nil {
return nil, fmt.Errorf("failed to save snapshot (%v)", err)
}
if maxBackup > 0 {
err := bm.EnsureMaxbackup(ctx, s.Path, maxBackup)
if err != nil {
return nil, fmt.Errorf("succeeded in saving snapshot but failed to delete old snapshot (%v)", err)
}
}
return &api.BackupStatus{EtcdVersion: etcdVersion, EtcdRevision: rev, LastSuccessDate: now}, nil
}
19 changes: 14 additions & 5 deletions pkg/controller/backup-operator/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ func (b *Backup) handleBackup(parentContext *context.Context, spec *api.BackupSp
if spec.BackupPolicy != nil && spec.BackupPolicy.TimeoutInSecond > 0 {
backupTimeout = time.Duration(spec.BackupPolicy.TimeoutInSecond) * time.Second
}
backupMaxCount := 0
if spec.BackupPolicy != nil && spec.BackupPolicy.MaxBackups > 0 {
backupMaxCount = spec.BackupPolicy.MaxBackups
}

if parentContext == nil {
tmpParent := context.Background()
Expand All @@ -259,19 +263,19 @@ func (b *Backup) handleBackup(parentContext *context.Context, spec *api.BackupSp
defer cancel()
switch spec.StorageType {
case api.BackupStorageTypeS3:
bs, err := handleS3(ctx, b.kubecli, spec.S3, spec.EtcdEndpoints, spec.ClientTLSSecret, b.namespace)
bs, err := handleS3(ctx, b.kubecli, spec.S3, spec.EtcdEndpoints, spec.ClientTLSSecret, b.namespace, backupMaxCount)
if err != nil {
Comment thread
hexfusion marked this conversation as resolved.
return nil, err
}
return bs, nil
case api.BackupStorageTypeABS:
bs, err := handleABS(ctx, b.kubecli, spec.ABS, spec.EtcdEndpoints, spec.ClientTLSSecret, b.namespace)
bs, err := handleABS(ctx, b.kubecli, spec.ABS, spec.EtcdEndpoints, spec.ClientTLSSecret, b.namespace, backupMaxCount)
if err != nil {
return nil, err
}
return bs, nil
case api.BackupStorageTypeGCS:
bs, err := handleGCS(ctx, b.kubecli, spec.GCS, spec.EtcdEndpoints, spec.ClientTLSSecret, b.namespace)
bs, err := handleGCS(ctx, b.kubecli, spec.GCS, spec.EtcdEndpoints, spec.ClientTLSSecret, b.namespace, backupMaxCount)
if err != nil {
return nil, err
}
Expand All @@ -287,8 +291,13 @@ func validate(spec *api.BackupSpec) error {
if len(spec.EtcdEndpoints) == 0 {
return errors.New("spec.etcdEndpoints should not be empty")
}
if spec.BackupPolicy != nil && spec.BackupPolicy.BackupIntervalInSecond < 0 {
return errros.New("spec.backupPoloicy.backupIntervalInSecond should not be lower than 0")
if spec.BackupPolicy != nil {
if spec.BackupPolicy.BackupIntervalInSecond < 0 {
return errors.New("spec.backupPoloicy.backupIntervalInSecond should not be lower than 0")
}
Comment thread
hexfusion marked this conversation as resolved.
Comment thread
hexfusion marked this conversation as resolved.
if spec.BackupPolicy.MaxBackups < 0 {
return errors.New("spec.backupPolicy.MaxBackups should not be lower than 0")
}
}
return nil
Comment thread
hexfusion marked this conversation as resolved.
}