Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backup): multiple backup stores support #2182

Draft
wants to merge 29 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
92e04a0
feat(backup): new field of Backup CRD
mantissahz Sep 23, 2024
f6062e2
feat(backup): new fields of BackupVolume CRD
mantissahz Sep 23, 2024
3c838fd
feat(backup): new field of BackupTarget CRD
mantissahz Sep 23, 2024
29686be
feat(backup): new filed of Volume CR
mantissahz Sep 25, 2024
2d6bfb8
feat(backup): methods to get backup volume CRs
mantissahz Sep 23, 2024
a1f5a59
feat(backup): backup target mutation
mantissahz Sep 26, 2024
906cc96
feat(backup): volume mutation and validation for mutiple backup targets
mantissahz Sep 25, 2024
f186402
feat(backup): new fields of BackupBackingImage CRD
mantissahz Sep 25, 2024
ad132bd
feat(backup): modify the backup target controller
mantissahz Sep 24, 2024
ac791c1
feat(backup): move out backup target logic from setting controller
mantissahz Sep 23, 2024
01dbdf5
feat(backup): add backup target validator
mantissahz Sep 25, 2024
be817a7
feat(backup): modify setting and uninstall controllers
mantissahz Sep 23, 2024
4ad096a
feat(backup): new filed of Volume CR
mantissahz Sep 25, 2024
fa1a4dd
feat(backup): modify backup volume controller
mantissahz Sep 25, 2024
6e7a401
feat(backup): modify backup controller
mantissahz Sep 24, 2024
d8971ca
feat(backup): add backup target label when creating a backup
mantissahz Sep 26, 2024
a046770
feat(backup): handle the restoration
mantissahz Sep 24, 2024
c8eef1f
feat(backup): handle triggering the backup volume synchronization
mantissahz Sep 26, 2024
31f4e5f
feat(backup): recurringjob for multiple backupstores
mantissahz Sep 25, 2024
b99190d
feat(backup): add backup target APIs
mantissahz Sep 24, 2024
9e2eea6
feat(backup): backup validation
mantissahz Sep 26, 2024
84dc92a
feat(backup): modify volume controller unit tests for backup targets
mantissahz Sep 24, 2024
0a18099
feat(backup): modify volume controller unit tests for backup targets
mantissahz Sep 26, 2024
3b067ba
feat(backup): modify getting BackupVolume for csi backup
mantissahz Sep 25, 2024
cf734d6
feat(backup): modify controllers related to backup backing image
mantissahz Sep 24, 2024
2158d2b
feat(backup): modify backing image data source controller
mantissahz Sep 24, 2024
1ceae2e
feat(backup): modify system backup controller
mantissahz Sep 26, 2024
99e9b35
feat(backup): upgrade CRs for multiple backup store
mantissahz Sep 25, 2024
442d9f4
feat(backup): add websocket for backup targets
mantissahz Sep 26, 2024
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
134 changes: 132 additions & 2 deletions api/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,129 @@ package api
import (
"fmt"
"net/http"
"strconv"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/rancher/go-rancher/api"
"github.com/rancher/go-rancher/client"

longhorn "github.com/longhorn/longhorn-manager/k8s/pkg/apis/longhorn/v1beta2"
"github.com/longhorn/longhorn-manager/util"
)

const (
BackupTargetDefaultPollInterval = 300
)

func (s *Server) BackupTargetGet(w http.ResponseWriter, req *http.Request) error {
apiContext := api.GetApiContext(req)

backupTargetName := mux.Vars(req)["name"]
backupTarget, err := s.m.GetBackupTarget(backupTargetName)
if err != nil {
return errors.Wrap(err, "failed to list backup targets")
}
apiContext.Write(toBackupTargetResource(backupTarget, apiContext))
return nil
}

func (s *Server) BackupTargetList(w http.ResponseWriter, req *http.Request) error {
apiContext := api.GetApiContext(req)

backupTargets, err := s.m.ListBackupTargetsSorted()
bts, err := s.backupTargetList(apiContext)
if err != nil {
return errors.Wrap(err, "failed to list backup targets")
}
apiContext.Write(toBackupTargetCollection(backupTargets, apiContext))

apiContext.Write(bts)
return nil
}

func (s *Server) backupTargetList(apiContext *api.ApiContext) (*client.GenericCollection, error) {
bts, err := s.m.ListBackupTargetsSorted()
if err != nil {
return nil, errors.Wrap(err, "failed to list backup target")
}
return toBackupTargetCollection(bts, apiContext), nil
}

func (s *Server) BackupTargetCreate(rw http.ResponseWriter, req *http.Request) error {
var input BackupTarget
apiContext := api.GetApiContext(req)

if err := apiContext.Read(&input); err != nil {
return err
}

backupTargetSpec, err := newBackupTarget(input)
if err != nil {
return err
}

obj, err := s.m.CreateBackupTarget(input.Name, backupTargetSpec)
if err != nil {
return errors.Wrapf(err, "failed to create backup target %v", input.Name)
}
apiContext.Write(toBackupTargetResource(obj, apiContext))
return nil
}

func newBackupTarget(input BackupTarget) (*longhorn.BackupTargetSpec, error) {
pollInterval, err := strconv.ParseInt(input.PollInterval, 10, 64)
if err != nil {
pollInterval = BackupTargetDefaultPollInterval
}

return &longhorn.BackupTargetSpec{
BackupTargetURL: input.BackupTargetURL,
CredentialSecret: input.CredentialSecret,
PollInterval: metav1.Duration{Duration: time.Duration(pollInterval) * time.Second},
ReadOnly: input.ReadyOnly}, nil
}

func (s *Server) BackupTargetUpdate(rw http.ResponseWriter, req *http.Request) error {
var input BackupTarget

apiContext := api.GetApiContext(req)
if err := apiContext.Read(&input); err != nil {
return err
}

name := mux.Vars(req)["name"]

backupTargetSpec, err := newBackupTarget(input)
if err != nil {
return err
}

obj, err := util.RetryOnConflictCause(func() (interface{}, error) {
return s.m.UpdateBackupTarget(input.Name, backupTargetSpec)
})
if err != nil {
return err
}

backupTarget, ok := obj.(*longhorn.BackupTarget)
if !ok {
return fmt.Errorf("failed to convert %v to backup target", name)
}

apiContext.Write(toBackupTargetResource(backupTarget, apiContext))
return nil
}

func (s *Server) BackupTargetDelete(rw http.ResponseWriter, req *http.Request) error {
backupTargetName := mux.Vars(req)["name"]
if err := s.m.DeleteBackupTarget(backupTargetName); err != nil {
return errors.Wrapf(err, "failed to delete backup target %v", backupTargetName)
}

return nil
}

Expand Down Expand Up @@ -174,6 +280,30 @@ func (s *Server) BackupVolumeDelete(w http.ResponseWriter, req *http.Request) er
func (s *Server) BackupList(w http.ResponseWriter, req *http.Request) error {
apiContext := api.GetApiContext(req)

bs, err := s.m.ListAllBackupsSorted()
if err != nil {
return errors.Wrapf(err, "failed to list all backups")
}
apiContext.Write(toBackupCollection(bs))
return nil
}

func (s *Server) BackupListByVolumeName(w http.ResponseWriter, req *http.Request) error {
apiContext := api.GetApiContext(req)

volName := mux.Vars(req)["volName"]

bs, err := s.m.ListBackupsForVolumeSorted(volName)
if err != nil {
return errors.Wrapf(err, "failed to list backups for volume '%s'", volName)
}
apiContext.Write(toBackupCollection(bs))
return nil
}

func (s *Server) BackupListByBVName(w http.ResponseWriter, req *http.Request) error {
apiContext := api.GetApiContext(req)

volName := mux.Vars(req)["volName"]

bs, err := s.m.ListBackupsForVolumeSorted(volName)
Expand Down
13 changes: 10 additions & 3 deletions api/backupbackingimage.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,16 @@ func (s *Server) BackupBackingImageRestore(w http.ResponseWriter, req *http.Requ
}

func (s *Server) BackupBackingImageCreate(w http.ResponseWriter, req *http.Request) error {
backupBackingImageName := mux.Vars(req)["name"]
if err := s.m.CreateBackupBackingImage(backupBackingImageName); err != nil {
return errors.Wrapf(err, "failed to create backup backing image '%s'", backupBackingImageName)
var input BackupBackingImage

apiContext := api.GetApiContext(req)
if err := apiContext.Read(&input); err != nil {
return err
}

backingImageName := mux.Vars(req)["name"]
if err := s.m.CreateBackupBackingImage(input.Name, backingImageName, input.BackupTargetName); err != nil {
return errors.Wrapf(err, "failed to create backup backing image '%s'", input.Name)
}
return nil
}
50 changes: 47 additions & 3 deletions api/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type Volume struct {
SnapshotMaxCount int `json:"snapshotMaxCount"`
SnapshotMaxSize string `json:"snapshotMaxSize"`
FreezeFilesystemForSnapshot longhorn.FreezeFilesystemForSnapshot `json:"freezeFilesystemForSnapshot"`
BackupTargetName string `json:"backupTargetName"`

DiskSelector []string `json:"diskSelector"`
NodeSelector []string `json:"nodeSelector"`
Expand Down Expand Up @@ -139,6 +140,8 @@ type BackupVolume struct {
BackingImageName string `json:"backingImageName"`
BackingImageChecksum string `json:"backingImageChecksum"`
StorageClassName string `json:"storageClassName"`
BackupTargetName string `json:"backupTargetName"`
VolumeName string `json:"volumeName"`
}

// SyncBackupResource is used for the Backup*Sync* actions
Expand Down Expand Up @@ -171,6 +174,7 @@ type Backup struct {
CompressionMethod string `json:"compressionMethod"`
NewlyUploadedDataSize string `json:"newlyUploadDataSize"`
ReUploadedDataSize string `json:"reUploadedDataSize"`
BackupTargetName string `json:"backupTargetName"`
}

type BackupBackingImage struct {
Expand All @@ -187,6 +191,8 @@ type BackupBackingImage struct {
CompressionMethod string `json:"compressionMethod"`
Secret string `json:"secret"`
SecretNamespace string `json:"secretNamespace"`
BackingImageName string `json:"backingImageName"`
BackupTargetName string `json:"backupTargetName"`
}

type Setting struct {
Expand Down Expand Up @@ -871,14 +877,44 @@ func kubernetesStatusSchema(status *client.Schema) {
}

func backupTargetSchema(backupTarget *client.Schema) {
backupTarget.CollectionMethods = []string{"GET"}
backupTarget.ResourceMethods = []string{"GET", "PUT"}
backupTarget.CollectionMethods = []string{"GET", "POST"}
backupTarget.ResourceMethods = []string{"GET", "PUT", "DELETE"}

backupTargetName := backupTarget.ResourceFields["name"]
backupTargetName.Required = true
backupTargetName.Unique = true
backupTargetName.Create = true
backupTarget.ResourceFields["name"] = backupTargetName

backupTargetURL := backupTarget.ResourceFields["backupTargetURL"]
backupTargetURL.Create = true
backupTargetURL.Default = ""
backupTarget.ResourceFields["backupTargetURL"] = backupTargetURL

credentialSecret := backupTarget.ResourceFields["credentialSecret"]
credentialSecret.Create = true
credentialSecret.Default = ""
backupTarget.ResourceFields["credentialSecret"] = credentialSecret

backupTargetPollInterval := backupTarget.ResourceFields["pollInterval"]
backupTargetPollInterval.Create = true
backupTargetPollInterval.Default = "300"
backupTarget.ResourceFields["pollInterval"] = backupTargetPollInterval

backupTargetReadOnly := backupTarget.ResourceFields["readOnly"]
backupTargetReadOnly.Create = true
backupTargetReadOnly.Default = false
backupTarget.ResourceFields["readOnly"] = backupTargetReadOnly

backupTarget.ResourceActions = map[string]client.Action{
"backupTargetSync": {
Input: "syncBackupResource",
Output: "backupTargetListOutput",
},
"backupTargetUpdate": {
Input: "BackupTarget",
Output: "backupTargetListOutput",
},
}
}

Expand Down Expand Up @@ -1562,6 +1598,7 @@ func toVolumeResource(v *longhorn.Volume, ves []*longhorn.Engine, vrs []*longhor
NodeSelector: v.Spec.NodeSelector,
RestoreVolumeRecurringJob: v.Spec.RestoreVolumeRecurringJob,
FreezeFilesystemForSnapshot: v.Spec.FreezeFilesystemForSnapshot,
BackupTargetName: v.Spec.BackupTargetName,

State: v.Status.State,
Robustness: v.Status.Robustness,
Expand Down Expand Up @@ -1797,11 +1834,13 @@ func toBackupTargetResource(bt *longhorn.BackupTarget, apiContext *api.ApiContex
CredentialSecret: bt.Spec.CredentialSecret,
PollInterval: bt.Spec.PollInterval.Duration.String(),
Available: bt.Status.Available,
ReadyOnly: bt.Spec.ReadOnly,
Message: types.GetCondition(bt.Status.Conditions, longhorn.BackupTargetConditionTypeUnavailable).Message,
},
}
res.Actions = map[string]string{
"backupTargetSync": apiContext.UrlBuilder.ActionLink(res.Resource, "backupTargetSync"),
"backupTargetSync": apiContext.UrlBuilder.ActionLink(res.Resource, "backupTargetSync"),
"backupTargetUpdate": apiContext.UrlBuilder.ActionLink(res.Resource, "backupTargetUpdate"),
}

return res
Expand All @@ -1828,6 +1867,8 @@ func toBackupVolumeResource(bv *longhorn.BackupVolume, apiContext *api.ApiContex
BackingImageName: bv.Status.BackingImageName,
BackingImageChecksum: bv.Status.BackingImageChecksum,
StorageClassName: bv.Status.StorageClassName,
BackupTargetName: bv.Spec.BackupTargetName,
VolumeName: bv.Spec.VolumeName,
}
b.Actions = map[string]string{
"backupList": apiContext.UrlBuilder.ActionLink(b.Resource, "backupList"),
Expand Down Expand Up @@ -1877,6 +1918,8 @@ func toBackupBackingImageResource(bbi *longhorn.BackupBackingImage, apiContext *
CompressionMethod: string(bbi.Status.CompressionMethod),
Secret: bbi.Status.Secret,
SecretNamespace: bbi.Status.SecretNamespace,
BackingImageName: bbi.Spec.BackingImage,
BackupTargetName: bbi.Spec.BackupTargetName,
}

backupBackingImage.Actions = map[string]string{
Expand Down Expand Up @@ -1932,6 +1975,7 @@ func toBackupResource(b *longhorn.Backup) *Backup {
CompressionMethod: string(b.Status.CompressionMethod),
NewlyUploadedDataSize: b.Status.NewlyUploadedDataSize,
ReUploadedDataSize: b.Status.ReUploadedDataSize,
BackupTargetName: b.Spec.BackupTargetName,
}
// Set the volume name from backup CR's label if it's empty.
// This field is empty probably because the backup state is not Ready
Expand Down
10 changes: 9 additions & 1 deletion api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,15 @@ func NewRouter(s *Server) *mux.Router {
r.Methods("GET").Path("/v1/backuptargets").Handler(f(schemas, s.BackupTargetList))
r.Methods("PUT").Path("/v1/backuptargets").Handler(f(schemas, s.BackupTargetSyncAll))
backupTargetActions := map[string]func(http.ResponseWriter, *http.Request) error{
"backupTargetSync": s.fwd.Handler(s.fwd.HandleProxyRequestByNodeID, s.fwd.GetHTTPAddressByNodeID(NodeHasDefaultEngineImage(s.m)), s.BackupTargetSync),
"backupTargetSync": s.fwd.Handler(s.fwd.HandleProxyRequestByNodeID, s.fwd.GetHTTPAddressByNodeID(NodeHasDefaultEngineImage(s.m)), s.BackupTargetSync),
"backupTargetUpdate": s.fwd.Handler(s.fwd.HandleProxyRequestByNodeID, s.fwd.GetHTTPAddressByNodeID(NodeHasDefaultEngineImage(s.m)), s.BackupTargetUpdate),
}
for name, action := range backupTargetActions {
r.Methods("POST").Path("/v1/backuptargets/{backupTargetName}").Queries("action", name).Handler(f(schemas, action))
}
r.Methods("GET").Path("/v1/backuptargets/{name}").Handler(f(schemas, s.BackupTargetGet))
r.Methods("POST").Path("/v1/backuptargets").Handler(f(schemas, s.BackupTargetCreate))
r.Methods("DELETE").Path("/v1/backuptargets/{name}").Handler(f(schemas, s.BackupTargetDelete))
r.Methods("GET").Path("/v1/backupvolumes").Handler(f(schemas, s.fwd.Handler(s.fwd.HandleProxyRequestByNodeID, s.fwd.GetHTTPAddressByNodeID(NodeHasDefaultEngineImage(s.m)), s.BackupVolumeList)))
r.Methods("PUT").Path("/v1/backupvolumes").Handler(f(schemas, s.fwd.Handler(s.fwd.HandleProxyRequestByNodeID, s.fwd.GetHTTPAddressByNodeID(NodeHasDefaultEngineImage(s.m)), s.BackupVolumeSyncAll)))
r.Methods("GET").Path("/v1/backupvolumes/{volName}").Handler(f(schemas, s.fwd.Handler(s.fwd.HandleProxyRequestByNodeID, s.fwd.GetHTTPAddressByNodeID(NodeHasDefaultEngineImage(s.m)), s.BackupVolumeGet)))
Expand Down Expand Up @@ -255,6 +259,10 @@ func NewRouter(s *Server) *mux.Router {
r.Path("/v1/ws/backupvolumes").Handler(f(schemas, backupVolumeStream))
r.Path("/v1/ws/{period}/backupvolumes").Handler(f(schemas, backupVolumeStream))

backupTargetStream := NewStreamHandlerFunc("backuptargets", s.wsc.NewWatcher("backupTarget"), s.backupTargetList)
r.Path("/v1/ws/backuptargets").Handler(f(schemas, backupTargetStream))
r.Path("/v1/ws/{period}/backuptargets").Handler(f(schemas, backupTargetStream))

// TODO:
// We haven't found a way to allow passing the volume name as a parameter to filter
// per-backup volume's backups change thru. WebSocket endpoint. Either by:
Expand Down
2 changes: 1 addition & 1 deletion api/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func (s *Server) SnapshotBackup(w http.ResponseWriter, req *http.Request) (err e
labels[types.KubernetesStatusLabel] = string(kubeStatus)
}

if err := s.m.BackupSnapshot(bsutil.GenerateName("backup"), volName, input.Name, labels, input.BackupMode); err != nil {
if err := s.m.BackupSnapshot(bsutil.GenerateName("backup"), vol.Spec.BackupTargetName, volName, input.Name, labels, input.BackupMode); err != nil {
return err
}

Expand Down
2 changes: 1 addition & 1 deletion api/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (s *Server) responseWithVolume(rw http.ResponseWriter, req *http.Request, i
if err != nil {
return err
}
backups, err := s.m.ListBackupsForVolumeSorted(id)
backups, err := s.m.ListBackupsForVolumeSorted(v.Name)
if err != nil {
return err
}
Expand Down
Loading