diff --git a/api/backup.go b/api/backup.go index 003994053f..982cc851dd 100644 --- a/api/backup.go +++ b/api/backup.go @@ -1,9 +1,16 @@ package api import ( + "fmt" "net/http" + "strconv" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/gorilla/mux" + longhorn "github.com/longhorn/longhorn-manager/k8s/pkg/apis/longhorn/v1beta2" + "github.com/longhorn/longhorn-manager/util" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -14,11 +21,106 @@ import ( 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 err + } + 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 targets") + } + return toBackupTargetCollection(bts, apiContext), nil +} + +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(toBackupTargetCollection(backupTargets)) + apiContext.Write(toBackupTargetResource(backupTarget, apiContext)) + return 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 (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 newBackupTarget(input BackupTarget) (*longhorn.BackupTargetSpec, error) { + pollInterval, err := strconv.ParseInt(input.PollInterval, 10, 64) + if err != nil { + return nil, err + } + + return &longhorn.BackupTargetSpec{ + BackupTargetURL: input.BackupTargetURL, + CredentialSecret: input.CredentialSecret, + Default: input.Default, + PollInterval: metav1.Duration{Duration: time.Duration(pollInterval) * time.Second}, + ReadOnly: input.ReadyOnly}, 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 } diff --git a/api/model.go b/api/model.go index 162911cddf..57f818f0c5 100644 --- a/api/model.go +++ b/api/model.go @@ -138,6 +138,8 @@ type BackupVolume struct { BackingImageName string `json:"backingImageName"` BackingImageChecksum string `json:"backingImageChecksum"` StorageClassName string `json:"storageClassName"` + BackupTargetName string `json:"backupTargetName"` + VolumeName string `json:"volumeName"` } type Backup struct { @@ -159,6 +161,7 @@ type Backup struct { VolumeCreated string `json:"volumeCreated"` VolumeBackingImageName string `json:"volumeBackingImageName"` CompressionMethod string `json:"compressionMethod"` + BackupTargetName string `json:"backupTargetName"` } type Setting struct { @@ -550,7 +553,6 @@ func NewSchema() *client.Schemas { schemas.AddType("detachInput", DetachInput{}) schemas.AddType("snapshotInput", SnapshotInput{}) schemas.AddType("snapshotCRInput", SnapshotCRInput{}) - schemas.AddType("backupTarget", BackupTarget{}) schemas.AddType("backup", Backup{}) schemas.AddType("backupInput", BackupInput{}) schemas.AddType("backupStatus", BackupStatus{}) @@ -612,6 +614,7 @@ func NewSchema() *client.Schemas { snapshotSchema(schemas.AddType("snapshot", Snapshot{})) snapshotCRSchema(schemas.AddType("snapshotCR", SnapshotCR{})) backupVolumeSchema(schemas.AddType("backupVolume", BackupVolume{})) + backupTargetSchema(schemas.AddType("backupTarget", BackupTarget{})) settingSchema(schemas.AddType("setting", Setting{})) recurringJobSchema(schemas.AddType("recurringJob", RecurringJob{})) engineImageSchema(schemas.AddType("engineImage", EngineImage{})) @@ -788,6 +791,17 @@ func backupVolumeSchema(backupVolume *client.Schema) { } } +func backupTargetSchema(backupTarget *client.Schema) { + 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 +} + func settingSchema(setting *client.Schema) { setting.CollectionMethods = []string{"GET"} setting.ResourceMethods = []string{"GET", "PUT"} @@ -1641,7 +1655,7 @@ func toVolumeRecurringJobCollection(recurringJobs map[string]*longhorn.VolumeRec return &client.GenericCollection{Data: data, Collection: client.Collection{ResourceType: "volumeRecurringJob"}} } -func toBackupTargetResource(bt *longhorn.BackupTarget) *BackupTarget { +func toBackupTargetResource(bt *longhorn.BackupTarget, apiContext *api.ApiContext) *BackupTarget { if bt == nil { return nil } @@ -1653,11 +1667,14 @@ func toBackupTargetResource(bt *longhorn.BackupTarget) *BackupTarget { Links: map[string]string{}, }, BackupTarget: engineapi.BackupTarget{ + Name: bt.Name, BackupTargetURL: bt.Spec.BackupTargetURL, CredentialSecret: bt.Spec.CredentialSecret, + Default: bt.Spec.Default, PollInterval: bt.Spec.PollInterval.Duration.String(), Available: bt.Status.Available, Message: types.GetCondition(bt.Status.Conditions, longhorn.BackupTargetConditionTypeUnavailable).Message, + ReadyOnly: bt.Spec.ReadOnly, }, } return res @@ -1684,6 +1701,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"), @@ -1693,10 +1712,10 @@ func toBackupVolumeResource(bv *longhorn.BackupVolume, apiContext *api.ApiContex return b } -func toBackupTargetCollection(bts []*longhorn.BackupTarget) *client.GenericCollection { +func toBackupTargetCollection(bts []*longhorn.BackupTarget, apiContext *api.ApiContext) *client.GenericCollection { data := []interface{}{} for _, bt := range bts { - data = append(data, toBackupTargetResource(bt)) + data = append(data, toBackupTargetResource(bt, apiContext)) } return &client.GenericCollection{Data: data, Collection: client.Collection{ResourceType: "backupTarget"}} } @@ -1743,6 +1762,7 @@ func toBackupResource(b *longhorn.Backup) *Backup { VolumeCreated: b.Status.VolumeCreated, VolumeBackingImageName: b.Status.VolumeBackingImageName, CompressionMethod: string(b.Status.CompressionMethod), + 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 diff --git a/api/router.go b/api/router.go index 65f6ae1239..bde3a5cbc3 100644 --- a/api/router.go +++ b/api/router.go @@ -118,6 +118,11 @@ func NewRouter(s *Server) *mux.Router { } r.Methods("GET").Path("/v1/backuptargets").Handler(f(schemas, s.BackupTargetList)) + 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("PUT").Path("/v1/backuptargets/{name}").Handler(f(schemas, s.BackupTargetUpdate)) + 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("GET").Path("/v1/backupvolumes/{volName}").Handler(f(schemas, s.fwd.Handler(s.fwd.HandleProxyRequestByNodeID, s.fwd.GetHTTPAddressByNodeID(NodeHasDefaultEngineImage(s.m)), s.BackupVolumeGet))) r.Methods("DELETE").Path("/v1/backupvolumes/{volName}").Handler(f(schemas, s.fwd.Handler(s.fwd.HandleProxyRequestByNodeID, s.fwd.GetHTTPAddressByNodeID(NodeHasDefaultEngineImage(s.m)), s.BackupVolumeDelete))) @@ -225,6 +230,10 @@ func NewRouter(s *Server) *mux.Router { r.Path("/v1/ws/backingimages").Handler(f(schemas, backingImageStream)) r.Path("/v1/ws/{period}/backingimages").Handler(f(schemas, backingImageStream)) + 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)) + backupVolumeStream := NewStreamHandlerFunc("backupvolumes", s.wsc.NewWatcher("backupVolume"), s.backupVolumeList) r.Path("/v1/ws/backupvolumes").Handler(f(schemas, backupVolumeStream)) r.Path("/v1/ws/{period}/backupvolumes").Handler(f(schemas, backupVolumeStream)) diff --git a/client/generated_backup.go b/client/generated_backup.go index ffa76a9f62..846c2455d4 100644 --- a/client/generated_backup.go +++ b/client/generated_backup.go @@ -7,6 +7,8 @@ const ( type Backup struct { Resource `yaml:"-"` + BackupTargetName string `json:"backupTargetName,omitempty" yaml:"backup_target_name,omitempty"` + CompressionMethod string `json:"compressionMethod,omitempty" yaml:"compression_method,omitempty"` Created string `json:"created,omitempty" yaml:"created,omitempty"` diff --git a/client/generated_backup_target.go b/client/generated_backup_target.go index 4393d39462..ea41fbf4c8 100644 --- a/client/generated_backup_target.go +++ b/client/generated_backup_target.go @@ -13,9 +13,15 @@ type BackupTarget struct { CredentialSecret string `json:"credentialSecret,omitempty" yaml:"credential_secret,omitempty"` + Default bool `json:"default,omitempty" yaml:"default,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + PollInterval string `json:"pollInterval,omitempty" yaml:"poll_interval,omitempty"` + + ReadOnly bool `json:"readOnly,omitempty" yaml:"read_only,omitempty"` } type BackupTargetCollection struct { diff --git a/client/generated_backup_volume.go b/client/generated_backup_volume.go index b1936e1988..5727a19ee4 100644 --- a/client/generated_backup_volume.go +++ b/client/generated_backup_volume.go @@ -11,6 +11,8 @@ type BackupVolume struct { BackingImageName string `json:"backingImageName,omitempty" yaml:"backing_image_name,omitempty"` + BackupTargetName string `json:"backupTargetName,omitempty" yaml:"backup_target_name,omitempty"` + Created string `json:"created,omitempty" yaml:"created,omitempty"` DataStored string `json:"dataStored,omitempty" yaml:"data_stored,omitempty"` @@ -28,6 +30,8 @@ type BackupVolume struct { Size string `json:"size,omitempty" yaml:"size,omitempty"` StorageClassName string `json:"storageClassName,omitempty" yaml:"storage_class_name,omitempty"` + + VolumeName string `json:"volumeName,omitempty" yaml:"volume_name,omitempty"` } type BackupVolumeCollection struct { diff --git a/engineapi/types.go b/engineapi/types.go index b6f532e0ae..606181df03 100644 --- a/engineapi/types.go +++ b/engineapi/types.go @@ -132,11 +132,14 @@ type Volume struct { } type BackupTarget struct { + Name string `json:"name"` BackupTargetURL string `json:"backupTargetURL"` CredentialSecret string `json:"credentialSecret"` + Default bool `json:"default"` PollInterval string `json:"pollInterval"` Available bool `json:"available"` Message string `json:"message"` + ReadyOnly bool `json:"readOnly"` } type BackupVolume struct { @@ -152,6 +155,8 @@ type BackupVolume struct { BackingImageName string `json:"backingImageName"` BackingImageChecksum string `json:"backingImageChecksum"` StorageClassName string `json:"storageClassName"` + BackupTargetName string `json:"backupTargetName"` + VolumeName string `json:"volumeName"` } type Backup struct { @@ -169,6 +174,7 @@ type Backup struct { VolumeBackingImageName string `json:"volumeBackingImageName"` Messages map[string]string `json:"messages"` CompressionMethod string `json:"compressionMethod"` + BackupTargetName string `json:"backupTargetName"` } type ConfigMetadata struct { diff --git a/manager/engine.go b/manager/engine.go index a7c3baff33..3f790b5af0 100644 --- a/manager/engine.go +++ b/manager/engine.go @@ -10,6 +10,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/longhorn/longhorn-manager/datastore" "github.com/longhorn/longhorn-manager/engineapi" longhorn "github.com/longhorn/longhorn-manager/k8s/pkg/apis/longhorn/v1beta2" "github.com/longhorn/longhorn-manager/util" @@ -320,6 +321,69 @@ func (m *VolumeManager) ListBackupTargetsSorted() ([]*longhorn.BackupTarget, err return backupTargets, nil } +func (m *VolumeManager) GetBackupTarget(backupTargetName string) (*longhorn.BackupTarget, error) { + backupTarget, err := m.ds.GetBackupTargetRO(backupTargetName) + if err != nil { + if apierrors.IsNotFound(err) { + // If the BackupTarget CR is not found, return succeeded result + + return &longhorn.BackupTarget{ObjectMeta: metav1.ObjectMeta{Name: backupTargetName}}, nil + } + return nil, err + } + return backupTarget, nil +} + +func (m *VolumeManager) CreateBackupTarget(backupTargetName string, backupTargetSpec *longhorn.BackupTargetSpec) (*longhorn.BackupTarget, error) { + backupTarget, err := m.ds.GetBackupTarget(backupTargetName) + if err != nil { + if !datastore.ErrorIsNotFound(err) { + return nil, err + } + + // Create the default BackupTarget CR if not present + backupTarget, err = m.ds.CreateBackupTarget(&longhorn.BackupTarget{ + ObjectMeta: metav1.ObjectMeta{ + Name: backupTargetName, + }, + Spec: *backupTargetSpec, + }) + if err != nil { + return nil, err + } + } + return backupTarget, nil +} + +func (m *VolumeManager) UpdateBackupTarget(backupTargetName string, backupTargetSpec *longhorn.BackupTargetSpec) (*longhorn.BackupTarget, error) { + existingBackupTarget, err := m.ds.GetBackupTarget(backupTargetName) + if err != nil { + return nil, err + } + + if backupTargetSpec.BackupTargetURL != existingBackupTarget.Spec.BackupTargetURL || + backupTargetSpec.CredentialSecret != existingBackupTarget.Spec.CredentialSecret || + backupTargetSpec.Default != existingBackupTarget.Spec.Default || + backupTargetSpec.PollInterval != existingBackupTarget.Spec.PollInterval || + backupTargetSpec.ReadOnly != existingBackupTarget.Spec.ReadOnly { + existingBackupTarget.Spec.BackupTargetURL = backupTargetSpec.BackupTargetURL + existingBackupTarget.Spec.CredentialSecret = backupTargetSpec.CredentialSecret + existingBackupTarget.Spec.Default = backupTargetSpec.Default + existingBackupTarget.Spec.PollInterval = backupTargetSpec.PollInterval + existingBackupTarget.Spec.ReadOnly = backupTargetSpec.ReadOnly + existingBackupTarget, err = m.ds.UpdateBackupTarget(existingBackupTarget) + if err != nil { + return nil, errors.Wrap(err, "failed to update backup target spec") + } + } + + return existingBackupTarget, nil +} + +func (m *VolumeManager) DeleteBackupTarget(backupTargetName string) error { + return m.ds.DeleteBackupTarget(backupTargetName) +} + func (m *VolumeManager) ListBackupVolumes() (map[string]*longhorn.BackupVolume, error) { return m.ds.ListBackupVolumes() } diff --git a/webhook/resources/backup/validator.go b/webhook/resources/backup/validator.go index 4ec8ef98f4..f4f4b1ec1f 100644 --- a/webhook/resources/backup/validator.go +++ b/webhook/resources/backup/validator.go @@ -48,7 +48,7 @@ func (b *backupValidator) Create(request *admission.Request, newObj runtime.Obje if err != nil { return werror.NewInvalidError(err.Error(), "") } - if backupTarget.Status.ReadOnly { + if backupTarget.Spec.ReadOnly && backup.Spec.SnapshotName != "" { return werror.NewInvalidError(fmt.Sprintf("failed to create a new backup %v on a read-only backup target %v ", backup.Name, backupTarget.Name), "") }