diff --git a/types/types.go b/types/types.go index 80f9255169..7d4ceca8e3 100644 --- a/types/types.go +++ b/types/types.go @@ -220,6 +220,7 @@ const ( BackupStoreTypeS3 = "s3" BackupStoreTypeCIFS = "cifs" + BackupStoreTypeNFS = "nfs" BackupStoreTypeAZBlob = "azblob" AWSIAMRoleAnnotation = "iam.amazonaws.com/role" diff --git a/webhook/resources/backuptarget/validator.go b/webhook/resources/backuptarget/validator.go new file mode 100644 index 0000000000..9b70da4149 --- /dev/null +++ b/webhook/resources/backuptarget/validator.go @@ -0,0 +1,244 @@ +package backuptarget + +import ( + "fmt" + "net/url" + "regexp" + "strings" + + "github.com/pkg/errors" + + admissionregv1 "k8s.io/api/admissionregistration/v1" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/longhorn/longhorn-manager/datastore" + "github.com/longhorn/longhorn-manager/types" + "github.com/longhorn/longhorn-manager/util" + "github.com/longhorn/longhorn-manager/webhook/admission" + + longhorn "github.com/longhorn/longhorn-manager/k8s/pkg/apis/longhorn/v1beta2" + werror "github.com/longhorn/longhorn-manager/webhook/error" +) + +type backupTargetURLType string + +const ( + backupTargetURLTypeBucket = backupTargetURLType("bucket") + backupTargetURLTypeOptions = backupTargetURLType("options") +) + +type backupTargetValidator struct { + admission.DefaultValidator + ds *datastore.DataStore +} + +func NewValidator(ds *datastore.DataStore) admission.Validator { + return &backupTargetValidator{ds: ds} +} + +func (b *backupTargetValidator) Resource() admission.Resource { + return admission.Resource{ + Name: "backuptargets", + Scope: admissionregv1.NamespacedScope, + APIGroup: longhorn.SchemeGroupVersion.Group, + APIVersion: longhorn.SchemeGroupVersion.Version, + ObjectType: &longhorn.BackupTarget{}, + OperationTypes: []admissionregv1.OperationType{ + admissionregv1.Create, + admissionregv1.Update, + }, + } +} + +func (b *backupTargetValidator) Create(request *admission.Request, newObj runtime.Object) error { + backupTarget := newObj.(*longhorn.BackupTarget) + + if !util.ValidateName(backupTarget.Name) { + return werror.NewInvalidError(fmt.Sprintf("invalid name %v", backupTarget.Name), "") + } + + if err := b.validateBackupTargetURL(backupTarget.Spec.BackupTargetURL); err != nil { + return werror.NewInvalidError(err.Error(), "") + } + + if err := b.validateCredentialSecret(backupTarget.Spec.CredentialSecret); err != nil { + return werror.NewInvalidError(err.Error(), "") + } + + return nil +} + +func (b *backupTargetValidator) Update(request *admission.Request, oldObj runtime.Object, newObj runtime.Object) error { + oldBackupTarget := oldObj.(*longhorn.BackupTarget) + newBackupTarget := newObj.(*longhorn.BackupTarget) + + urlChanged := oldBackupTarget.Spec.BackupTargetURL != newBackupTarget.Spec.BackupTargetURL + secretChanged := oldBackupTarget.Spec.CredentialSecret != newBackupTarget.Spec.CredentialSecret + + if urlChanged || secretChanged { + if err := b.validateDRVolume(newBackupTarget); err != nil { + return werror.NewInvalidError(err.Error(), "") + } + } + + if urlChanged { + if err := b.validateBackupTargetURL(newBackupTarget.Spec.BackupTargetURL); err != nil { + return werror.NewInvalidError(err.Error(), "") + } + } + + if secretChanged { + if err := b.validateCredentialSecret(newBackupTarget.Spec.CredentialSecret); err != nil { + return werror.NewInvalidError(err.Error(), "") + } + } + + return nil +} + +func (b *backupTargetValidator) validateBackupTargetURL(backupTargetURL string) error { + if backupTargetURL == "" { + return nil + } + + u, err := url.Parse(backupTargetURL) + if err != nil { + return errors.Wrapf(err, "failed to parse %v as url", backupTargetURL) + } + + if err := checkBackupTargetURLFormat(u); err != nil { + return err + } + + if err := b.checkBackupTargetURLExisting(u); err != nil { + return err + } + + return nil +} + +func checkBackupTargetURLFormat(u *url.URL) error { + // Check whether have $ or , have been set in BackupTarget path + regStr := `[\$\,]` + if u.Scheme == "cifs" { + // The $ in SMB/CIFS URIs means that the share is hidden. + regStr = `[\,]` + } + + reg := regexp.MustCompile(regStr) + findStr := reg.FindAllString(u.Path, -1) + if len(findStr) != 0 { + return fmt.Errorf("url %s, contains %v", u.String(), strings.Join(findStr, " or ")) + } + return nil +} + +func (b *backupTargetValidator) checkBackupTargetURLExisting(u *url.URL) error { + var btURLType backupTargetURLType + var creatingBackupTargetPath string + + switch u.Scheme { + case types.BackupStoreTypeAZBlob, types.BackupStoreTypeS3: + btURLType = backupTargetURLTypeBucket + creatingBackupTargetPath = u.String() + case types.BackupStoreTypeCIFS, types.BackupStoreTypeNFS: + btURLType = backupTargetURLTypeOptions + creatingBackupTargetPath = strings.TrimRight(u.Host+u.Path, "/") + default: + return fmt.Errorf("url %s with the unsupported protocol %v", u.String(), u.Scheme) + } + + bts, err := b.ds.ListBackupTargetsRO() + if err != nil { + return err + } + for _, bt := range bts { + uExisting, err := url.Parse(bt.Spec.BackupTargetURL) + if err != nil { + return errors.Wrapf(err, "failed to parse %v as url", bt.Spec.BackupTargetURL) + } + if uExisting.Scheme != u.Scheme { + continue + } + var backupTargetPath string + if btURLType == backupTargetURLTypeBucket { + backupTargetPath = uExisting.String() + } + if btURLType == backupTargetURLTypeOptions { + backupTargetPath = strings.TrimRight(uExisting.Host+uExisting.Path, "/") + } + if backupTargetPath == creatingBackupTargetPath { + return fmt.Errorf("url %s is the same to backup target %v", u.String(), bt.Name) + } + } + + return nil +} + +func (b *backupTargetValidator) validateCredentialSecret(secretName string) error { + namespace, err := b.ds.GetLonghornNamespace() + if err != nil { + return errors.Wrapf(err, "failed to get Longhorn namespace") + } + secret, err := b.ds.GetSecretRO(namespace.Name, secretName) + if err != nil { + if !apierrors.IsNotFound(err) { + return errors.Wrapf(err, "failed to get the secret before modifying backup target credential secret") + } + return nil + } + checkKeyList := []string{ + types.AWSAccessKey, + types.AWSIAMRoleAnnotation, + types.AWSIAMRoleArn, + types.AWSAccessKey, + types.AWSSecretKey, + types.AWSEndPoint, + types.AWSCert, + types.CIFSUsername, + types.CIFSPassword, + types.AZBlobAccountName, + types.AZBlobAccountKey, + types.AZBlobEndpoint, + types.AZBlobCert, + types.HTTPSProxy, + types.HTTPProxy, + types.NOProxy, + types.VirtualHostedStyle, + } + for _, checkKey := range checkKeyList { + if value, ok := secret.Data[checkKey]; ok { + if strings.TrimSpace(string(value)) != string(value) { + return fmt.Errorf("there is space or new line in %s", checkKey) + } + } + } + return nil +} + +func (b *backupTargetValidator) validateDRVolume(backupTarget *longhorn.BackupTarget) error { + vs, err := b.ds.ListDRVolumesRO() + if err != nil { + return errors.Wrapf(err, "failed to list standby volume when modifying BackupTarget") + } + + backupTargetURL := backupTarget.Spec.BackupTargetURL + if len(vs) != 0 { + standbyVolumeNames := sets.NewString() + for k, v := range vs { + specCharIndex := strings.Index(v.Spec.FromBackup, "?") + fromBackupURL := v.Spec.FromBackup[:specCharIndex] + fromBackupTargetName, isFound := v.Labels[types.LonghornLabelBackupTarget] + if (isFound && fromBackupTargetName == backupTarget.Name) || fromBackupURL == backupTargetURL { + standbyVolumeNames.Insert(k) + } + } + if standbyVolumeNames.Len() != 0 { + return fmt.Errorf("cannot modify BackupTarget since there are existing standby volumes: %v", standbyVolumeNames) + } + } + return nil +} diff --git a/webhook/server/validation.go b/webhook/server/validation.go index a48a52ef5d..92c175d350 100644 --- a/webhook/server/validation.go +++ b/webhook/server/validation.go @@ -11,6 +11,7 @@ import ( "github.com/longhorn/longhorn-manager/webhook/admission" "github.com/longhorn/longhorn-manager/webhook/resources/backingimage" "github.com/longhorn/longhorn-manager/webhook/resources/backup" + "github.com/longhorn/longhorn-manager/webhook/resources/backuptarget" "github.com/longhorn/longhorn-manager/webhook/resources/engine" "github.com/longhorn/longhorn-manager/webhook/resources/node" "github.com/longhorn/longhorn-manager/webhook/resources/orphan" @@ -38,6 +39,7 @@ func Validation(ds *datastore.DataStore) (http.Handler, []admission.Resource, er recurringjob.NewValidator(ds), backingimage.NewValidator(ds), backup.NewValidator(ds), + backuptarget.NewValidator(ds), volume.NewValidator(ds, currentNodeID), orphan.NewValidator(ds), snapshot.NewValidator(ds),