diff --git a/types/types.go b/types/types.go index 3e7769560d..6e81d865d0 100644 --- a/types/types.go +++ b/types/types.go @@ -160,6 +160,8 @@ const ( LonghornLabelManagedBy = "managed-by" LonghornLabelSnapshotForCloningVolume = "for-cloning-volume" LonghornLabelBackingImageDataSource = "backing-image-data-source" + LonghornLabelDataEngineUpgradeManager = "data-engine-upgrade-manager" + LonghornLabelNodeDataEngineUpgrade = "node-data-engine-upgrade" LonghornLabelBackupVolume = "backup-volume" LonghornLabelRecurringJob = "job" LonghornLabelRecurringJobGroup = "job-group" @@ -1265,3 +1267,25 @@ func MergeStringMaps(baseMap, overwriteMap map[string]string) map[string]string } return result } + +func GenerateNodeDataEngineUpgradeName(prefix, nodeID string) string { + return prefix + "-" + nodeID + "-" + util.RandomID() +} + +func GetDataEngineUpgradeManagerLabels() map[string]string { + labels := GetBaseLabelsForSystemManagedComponent() + labels[GetLonghornLabelComponentKey()] = LonghornLabelDataEngineUpgradeManager + return labels +} + +func GetNodeDataEngineUpgradeLabels(upgradeManagerID, nodeID string) map[string]string { + labels := GetBaseLabelsForSystemManagedComponent() + labels[GetLonghornLabelComponentKey()] = LonghornLabelNodeDataEngineUpgrade + if upgradeManagerID != "" { + labels[GetLonghornLabelKey(LonghornLabelDataEngineUpgradeManager)] = upgradeManagerID + } + if nodeID != "" { + labels[GetLonghornLabelKey(LonghornLabelNode)] = nodeID + } + return labels +} diff --git a/webhook/resources/dataengineupgrademanager/mutator.go b/webhook/resources/dataengineupgrademanager/mutator.go new file mode 100644 index 0000000000..1729ab48c7 --- /dev/null +++ b/webhook/resources/dataengineupgrademanager/mutator.go @@ -0,0 +1,74 @@ +package dataengineupgrademanager + +import ( + "fmt" + + "github.com/pkg/errors" + + "k8s.io/apimachinery/pkg/runtime" + + admissionregv1 "k8s.io/api/admissionregistration/v1" + + "github.com/longhorn/longhorn-manager/datastore" + "github.com/longhorn/longhorn-manager/types" + "github.com/longhorn/longhorn-manager/webhook/admission" + "github.com/longhorn/longhorn-manager/webhook/common" + + longhorn "github.com/longhorn/longhorn-manager/k8s/pkg/apis/longhorn/v1beta2" + werror "github.com/longhorn/longhorn-manager/webhook/error" +) + +type dataEngineUpgradeManagerMutator struct { + admission.DefaultMutator + ds *datastore.DataStore +} + +func NewMutator(ds *datastore.DataStore) admission.Mutator { + return &dataEngineUpgradeManagerMutator{ds: ds} +} + +func (u *dataEngineUpgradeManagerMutator) Resource() admission.Resource { + return admission.Resource{ + Name: "dataengineupgrademanagers", + Scope: admissionregv1.NamespacedScope, + APIGroup: longhorn.SchemeGroupVersion.Group, + APIVersion: longhorn.SchemeGroupVersion.Version, + ObjectType: &longhorn.DataEngineUpgradeManager{}, + OperationTypes: []admissionregv1.OperationType{ + admissionregv1.Create, + }, + } +} + +func (u *dataEngineUpgradeManagerMutator) Create(request *admission.Request, newObj runtime.Object) (admission.PatchOps, error) { + return mutate(newObj) +} + +func mutate(newObj runtime.Object) (admission.PatchOps, error) { + upgradeManager, ok := newObj.(*longhorn.DataEngineUpgradeManager) + if !ok { + return nil, werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.DataEngineUpgradeManager", newObj), "") + } + var patchOps admission.PatchOps + + longhornLabels := types.GetDataEngineUpgradeManagerLabels() + patchOp, err := common.GetLonghornLabelsPatchOp(upgradeManager, longhornLabels, nil) + if err != nil { + err := errors.Wrapf(err, "failed to get label patch for upgradeManager %v", upgradeManager.Name) + return nil, werror.NewInvalidError(err.Error(), "") + } + if patchOp != "" { + patchOps = append(patchOps, patchOp) + } + + patchOp, err = common.GetLonghornFinalizerPatchOpIfNeeded(upgradeManager) + if err != nil { + err := errors.Wrapf(err, "failed to get finalizer patch for dataEngineUpgradeManager %v", upgradeManager.Name) + return nil, werror.NewInvalidError(err.Error(), "") + } + if patchOp != "" { + patchOps = append(patchOps, patchOp) + } + + return patchOps, nil +} diff --git a/webhook/resources/dataengineupgrademanager/validator.go b/webhook/resources/dataengineupgrademanager/validator.go new file mode 100644 index 0000000000..b0f5d1e710 --- /dev/null +++ b/webhook/resources/dataengineupgrademanager/validator.go @@ -0,0 +1,79 @@ +package dataengineupgrademanager + +import ( + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/runtime" + + admissionregv1 "k8s.io/api/admissionregistration/v1" + + "github.com/longhorn/longhorn-manager/datastore" + "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 dataEngineUpgradeManagerValidator struct { + admission.DefaultValidator + ds *datastore.DataStore +} + +func NewValidator(ds *datastore.DataStore) admission.Validator { + return &dataEngineUpgradeManagerValidator{ds: ds} +} + +func (u *dataEngineUpgradeManagerValidator) Resource() admission.Resource { + return admission.Resource{ + Name: "dataengineupgrademanagers", + Scope: admissionregv1.NamespacedScope, + APIGroup: longhorn.SchemeGroupVersion.Group, + APIVersion: longhorn.SchemeGroupVersion.Version, + ObjectType: &longhorn.DataEngineUpgradeManager{}, + OperationTypes: []admissionregv1.OperationType{ + admissionregv1.Create, + admissionregv1.Update, + }, + } +} + +func (u *dataEngineUpgradeManagerValidator) Create(request *admission.Request, newObj runtime.Object) error { + upgradeManager, ok := newObj.(*longhorn.DataEngineUpgradeManager) + if !ok { + return werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.DataEngineUpgradeManager", newObj), "") + } + + if upgradeManager.Spec.DataEngine != longhorn.DataEngineTypeV2 { + err := fmt.Errorf("data engine %v is not supported", upgradeManager.Spec.DataEngine) + return werror.NewInvalidError(err.Error(), "spec.dataEngine") + } + + return nil +} + +func (u *dataEngineUpgradeManagerValidator) Update(request *admission.Request, oldObj runtime.Object, newObj runtime.Object) error { + oldUpgradeManager, ok := oldObj.(*longhorn.DataEngineUpgradeManager) + if !ok { + return werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.DataEngineUpgradeManager", oldObj), "") + } + newUpgradeManager, ok := newObj.(*longhorn.DataEngineUpgradeManager) + if !ok { + return werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.DataEngineUpgradeManager", newObj), "") + } + + if newUpgradeManager.Spec.DataEngine != longhorn.DataEngineTypeV2 { + err := fmt.Errorf("data engine %v is not supported", newUpgradeManager.Spec.DataEngine) + return werror.NewInvalidError(err.Error(), "spec.dataEngine") + } + + if oldUpgradeManager.Spec.DataEngine != newUpgradeManager.Spec.DataEngine { + return werror.NewInvalidError("spec.dataEngine field is immutable", "spec.dataEngine") + } + + if !reflect.DeepEqual(oldUpgradeManager.Spec.Nodes, newUpgradeManager.Spec.Nodes) { + return werror.NewInvalidError("nodes field is immutable", "spec.nodes") + } + + return nil +} diff --git a/webhook/resources/nodedataengineupgrade/mutator.go b/webhook/resources/nodedataengineupgrade/mutator.go new file mode 100644 index 0000000000..af7d7d66f1 --- /dev/null +++ b/webhook/resources/nodedataengineupgrade/mutator.go @@ -0,0 +1,74 @@ +package nodedataengineupgrade + +import ( + "fmt" + + "github.com/pkg/errors" + + "k8s.io/apimachinery/pkg/runtime" + + admissionregv1 "k8s.io/api/admissionregistration/v1" + + "github.com/longhorn/longhorn-manager/datastore" + "github.com/longhorn/longhorn-manager/types" + "github.com/longhorn/longhorn-manager/webhook/admission" + "github.com/longhorn/longhorn-manager/webhook/common" + + longhorn "github.com/longhorn/longhorn-manager/k8s/pkg/apis/longhorn/v1beta2" + werror "github.com/longhorn/longhorn-manager/webhook/error" +) + +type nodeDataEngineUpgradeMutator struct { + admission.DefaultMutator + ds *datastore.DataStore +} + +func NewMutator(ds *datastore.DataStore) admission.Mutator { + return &nodeDataEngineUpgradeMutator{ds: ds} +} + +func (u *nodeDataEngineUpgradeMutator) Resource() admission.Resource { + return admission.Resource{ + Name: "nodedataengineupgrades", + Scope: admissionregv1.NamespacedScope, + APIGroup: longhorn.SchemeGroupVersion.Group, + APIVersion: longhorn.SchemeGroupVersion.Version, + ObjectType: &longhorn.NodeDataEngineUpgrade{}, + OperationTypes: []admissionregv1.OperationType{ + admissionregv1.Create, + }, + } +} + +func (u *nodeDataEngineUpgradeMutator) Create(request *admission.Request, newObj runtime.Object) (admission.PatchOps, error) { + return mutate(newObj) +} + +func mutate(newObj runtime.Object) (admission.PatchOps, error) { + nodeUpgrade, ok := newObj.(*longhorn.NodeDataEngineUpgrade) + if !ok { + return nil, werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.NodeDataEngineUpgrade", newObj), "") + } + var patchOps admission.PatchOps + + longhornLabels := types.GetNodeDataEngineUpgradeLabels(nodeUpgrade.Spec.DataEngineUpgradeManager, nodeUpgrade.Spec.NodeID) + patchOp, err := common.GetLonghornLabelsPatchOp(nodeUpgrade, longhornLabels, nil) + if err != nil { + err := errors.Wrapf(err, "failed to get label patch for nodeUpgrade %v", nodeUpgrade.Name) + return nil, werror.NewInvalidError(err.Error(), "") + } + if patchOp != "" { + patchOps = append(patchOps, patchOp) + } + + patchOp, err = common.GetLonghornFinalizerPatchOpIfNeeded(nodeUpgrade) + if err != nil { + err := errors.Wrapf(err, "failed to get finalizer patch for nodeDataEngineUpgrade %v", nodeUpgrade.Name) + return nil, werror.NewInvalidError(err.Error(), "") + } + if patchOp != "" { + patchOps = append(patchOps, patchOp) + } + + return patchOps, nil +} diff --git a/webhook/resources/nodedataengineupgrade/validator.go b/webhook/resources/nodedataengineupgrade/validator.go new file mode 100644 index 0000000000..adc37058fe --- /dev/null +++ b/webhook/resources/nodedataengineupgrade/validator.go @@ -0,0 +1,95 @@ +package nodedataengineupgrade + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + + admissionregv1 "k8s.io/api/admissionregistration/v1" + + "github.com/longhorn/longhorn-manager/datastore" + "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 nodeDataEngineUpgradeValidator struct { + admission.DefaultValidator + ds *datastore.DataStore +} + +func NewValidator(ds *datastore.DataStore) admission.Validator { + return &nodeDataEngineUpgradeValidator{ds: ds} +} + +func (u *nodeDataEngineUpgradeValidator) Resource() admission.Resource { + return admission.Resource{ + Name: "nodedataengineupgrades", + Scope: admissionregv1.NamespacedScope, + APIGroup: longhorn.SchemeGroupVersion.Group, + APIVersion: longhorn.SchemeGroupVersion.Version, + ObjectType: &longhorn.NodeDataEngineUpgrade{}, + OperationTypes: []admissionregv1.OperationType{ + admissionregv1.Create, + admissionregv1.Update, + }, + } +} + +func (u *nodeDataEngineUpgradeValidator) Create(request *admission.Request, newObj runtime.Object) error { + nodeUpgrade, ok := newObj.(*longhorn.NodeDataEngineUpgrade) + if !ok { + return werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.NodeDataEngineUpgrade", newObj), "") + } + + if nodeUpgrade.Spec.NodeID == "" { + return werror.NewInvalidError("nodeID is required", "spec.nodeID") + } + + if nodeUpgrade.Spec.DataEngine != longhorn.DataEngineTypeV2 { + err := fmt.Errorf("data engine %v is not supported", nodeUpgrade.Spec.DataEngine) + return werror.NewInvalidError(err.Error(), "spec.dataEngine") + } + + if nodeUpgrade.Spec.InstanceManagerImage == "" { + err := fmt.Errorf("instanceManagerImage is required") + return werror.NewInvalidError(err.Error(), "spec.instanceManagerImage") + } + + if nodeUpgrade.Spec.DataEngineUpgradeManager == "" { + err := fmt.Errorf("dataEngineUpgradeManager is required") + return werror.NewInvalidError(err.Error(), "spec.dataEngineUpgradeManager") + } + + return nil +} + +func (u *nodeDataEngineUpgradeValidator) Update(request *admission.Request, oldObj runtime.Object, newObj runtime.Object) error { + oldNodeUpgrade, ok := oldObj.(*longhorn.NodeDataEngineUpgrade) + if !ok { + return werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.NodeDataEngineUpgrade", oldObj), "") + } + newNodeUpgrade, ok := newObj.(*longhorn.NodeDataEngineUpgrade) + if !ok { + return werror.NewInvalidError(fmt.Sprintf("%v is not a *longhorn.NodeDataEngineUpgrade", newObj), "") + } + + if oldNodeUpgrade.Spec.NodeID != newNodeUpgrade.Spec.NodeID { + return werror.NewInvalidError("nodeID field is immutable", "spec.nodeID") + } + + if oldNodeUpgrade.Spec.DataEngine != newNodeUpgrade.Spec.DataEngine { + return werror.NewInvalidError("dataEngine field is immutable", "spec.dataEngine") + } + + if oldNodeUpgrade.Spec.InstanceManagerImage != newNodeUpgrade.Spec.InstanceManagerImage { + return werror.NewInvalidError("instanceManagerImage field is immutable", "spec.instanceManagerImage") + } + + if oldNodeUpgrade.Spec.DataEngineUpgradeManager != newNodeUpgrade.Spec.DataEngineUpgradeManager { + return werror.NewInvalidError("dataEngineUpgradeManager field is immutable", "spec.dataEngineUpgradeManager") + } + + return nil +} diff --git a/webhook/resources/volume/validator.go b/webhook/resources/volume/validator.go index 9e20ec96b6..00c7da753a 100644 --- a/webhook/resources/volume/validator.go +++ b/webhook/resources/volume/validator.go @@ -101,7 +101,7 @@ func (v *volumeValidator) Create(request *admission.Request, newObj runtime.Obje } if volume.Spec.Image == "" { - return werror.NewInvalidError("BUG: Invalid empty Setting.EngineImage", "") + return werror.NewInvalidError("BUG: Invalid empty Setting.EngineImage", "spec.image") } if !volume.Spec.Standby { @@ -128,8 +128,7 @@ func (v *volumeValidator) Create(request *admission.Request, newObj runtime.Obje return werror.NewInvalidError(err.Error(), "") } - err := wcommon.ValidateRequiredDataEngineEnabled(v.ds, volume.Spec.DataEngine) - if err != nil { + if err := wcommon.ValidateRequiredDataEngineEnabled(v.ds, volume.Spec.DataEngine); err != nil { return err } @@ -142,19 +141,40 @@ func (v *volumeValidator) Create(request *admission.Request, newObj runtime.Obje } if err := v.ds.CheckDataEngineImageCompatiblityByImage(volume.Spec.Image, volume.Spec.DataEngine); err != nil { - return werror.NewInvalidError(err.Error(), "volume.spec.image") + return werror.NewInvalidError(err.Error(), "spec.image") + } + + if volume.Spec.TargetNodeID != "" { + return werror.NewInvalidError("spec.targetNodeID should be empty for a new volume", "spec.targetNodeID") } - // TODO: remove this check when we support the following features for SPDK volumes if types.IsDataEngineV2(volume.Spec.DataEngine) { + // TODO: remove this check when we support the following features for SPDK volumes if volume.Spec.Encrypted { - return werror.NewInvalidError("encrypted volume is not supported for data engine v2", "") + return werror.NewInvalidError("encrypted volume is not supported for data engine v2", "spec.encrypted") } + // TODO: remove this check when we support the following features for SPDK volumes if volume.Spec.BackingImage != "" { - return werror.NewInvalidError("backing image is not supported for data engine v2", "") + return werror.NewInvalidError("backing image is not supported for data engine v2", "spec.backingImage") } + // TODO: remove this check when we support the following features for SPDK volumes if types.IsDataFromVolume(volume.Spec.DataSource) { - return werror.NewInvalidError("clone is not supported for data engine v2", "") + return werror.NewInvalidError("clone is not supported for data engine v2", "spec.dataSource") + } + + if volume.Spec.NodeID != "" { + node, err := v.ds.GetNodeRO(volume.Spec.NodeID) + if err != nil { + err = errors.Wrapf(err, "failed to get node %v", volume.Spec.NodeID) + return werror.NewInternalError(err.Error()) + } + + if node.Spec.DataEngineUpgradeRequested { + if volume.Spec.NodeID != "" { + return werror.NewInvalidError(fmt.Sprintf("volume %v is not allowed to attach to node %v during v2 data engine upgrade", + volume.Name, volume.Spec.NodeID), "spec.nodeID") + } + } } } @@ -243,6 +263,30 @@ func (v *volumeValidator) Update(request *admission.Request, oldObj runtime.Obje } } + // prevent the changing v.Spec.MigrationNodeID to different node when the volume is doing live migration (when v.Status.CurrentMigrationNodeID != "") + if newVolume.Status.CurrentMigrationNodeID != "" && + newVolume.Spec.MigrationNodeID != oldVolume.Spec.MigrationNodeID && + newVolume.Spec.MigrationNodeID != newVolume.Status.CurrentMigrationNodeID && + newVolume.Spec.MigrationNodeID != "" { + err := fmt.Errorf("cannot change v.Spec.MigrationNodeID to node %v when the volume is doing live migration to node %v ", newVolume.Spec.MigrationNodeID, newVolume.Status.CurrentMigrationNodeID) + return werror.NewInvalidError(err.Error(), "") + } + + if err := validateSnapshotMaxCount(newVolume.Spec.SnapshotMaxCount); err != nil { + return werror.NewInvalidError(err.Error(), "spec.snapshotMaxCount") + } + + if err := validateSnapshotMaxSize(newVolume.Spec.Size, newVolume.Spec.SnapshotMaxSize); err != nil { + return werror.NewInvalidError(err.Error(), "spec.snapshotMaxSize") + } + + if (oldVolume.Spec.SnapshotMaxCount != newVolume.Spec.SnapshotMaxCount) || + (oldVolume.Spec.SnapshotMaxSize != newVolume.Spec.SnapshotMaxSize) { + if err := v.validateUpdatingSnapshotMaxCountAndSize(oldVolume, newVolume); err != nil { + return err + } + } + if types.IsDataEngineV2(newVolume.Spec.DataEngine) { // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.Size != newVolume.Spec.Size { @@ -251,48 +295,56 @@ func (v *volumeValidator) Update(request *admission.Request, oldObj runtime.Obje return werror.NewInvalidError(err.Error(), "") } + // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.BackingImage != newVolume.Spec.BackingImage { err := fmt.Errorf("changing backing image for volume %v is not supported for data engine %v", newVolume.Name, newVolume.Spec.DataEngine) return werror.NewInvalidError(err.Error(), "") } + // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.Encrypted != newVolume.Spec.Encrypted { err := fmt.Errorf("changing encryption for volume %v is not supported for data engine %v", newVolume.Name, newVolume.Spec.DataEngine) return werror.NewInvalidError(err.Error(), "") } + // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.SnapshotDataIntegrity != newVolume.Spec.SnapshotDataIntegrity { err := fmt.Errorf("changing snapshot data integrity for volume %v is not supported for data engine %v", newVolume.Name, newVolume.Spec.DataEngine) return werror.NewInvalidError(err.Error(), "") } + // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.ReplicaAutoBalance != newVolume.Spec.ReplicaAutoBalance { err := fmt.Errorf("changing replica auto balance for volume %v is not supported for data engine %v", newVolume.Name, newVolume.Spec.DataEngine) return werror.NewInvalidError(err.Error(), "") } + // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.RestoreVolumeRecurringJob != newVolume.Spec.RestoreVolumeRecurringJob { err := fmt.Errorf("changing restore volume recurring job for volume %v is not supported for data engine %v", newVolume.Name, newVolume.Spec.DataEngine) return werror.NewInvalidError(err.Error(), "") } + // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.ReplicaSoftAntiAffinity != newVolume.Spec.ReplicaSoftAntiAffinity { err := fmt.Errorf("changing replica soft anti-affinity for volume %v is not supported for data engine %v", newVolume.Name, newVolume.Spec.DataEngine) return werror.NewInvalidError(err.Error(), "") } + // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.ReplicaZoneSoftAntiAffinity != newVolume.Spec.ReplicaZoneSoftAntiAffinity { err := fmt.Errorf("changing replica zone soft anti-affinity for volume %v is not supported for data engine %v", newVolume.Name, newVolume.Spec.DataEngine) return werror.NewInvalidError(err.Error(), "") } + // TODO: remove this check when we support the following features for SPDK volumes if oldVolume.Spec.ReplicaDiskSoftAntiAffinity != newVolume.Spec.ReplicaDiskSoftAntiAffinity { if oldVolume.Spec.ReplicaDiskSoftAntiAffinity != "" && newVolume.Spec.ReplicaDiskSoftAntiAffinity != longhorn.ReplicaDiskSoftAntiAffinityDefault { err := fmt.Errorf("changing replica disk soft anti-affinity for volume %v is not supported for data engine %v", @@ -300,31 +352,63 @@ func (v *volumeValidator) Update(request *admission.Request, oldObj runtime.Obje return werror.NewInvalidError(err.Error(), "") } } - } - // prevent the changing v.Spec.MigrationNodeID to different node when the volume is doing live migration (when v.Status.CurrentMigrationNodeID != "") - if newVolume.Status.CurrentMigrationNodeID != "" && - newVolume.Spec.MigrationNodeID != oldVolume.Spec.MigrationNodeID && - newVolume.Spec.MigrationNodeID != newVolume.Status.CurrentMigrationNodeID && - newVolume.Spec.MigrationNodeID != "" { - err := fmt.Errorf("cannot change v.Spec.MigrationNodeID to node %v when the volume is doing live migration to node %v ", newVolume.Spec.MigrationNodeID, newVolume.Status.CurrentMigrationNodeID) - return werror.NewInvalidError(err.Error(), "") - } + // v2 data engine upgrade + node, err := v.ds.GetNodeRO(oldVolume.Status.OwnerID) + if err != nil { + return werror.NewInternalError(fmt.Sprintf("failed to get node %v, err %v", oldVolume.Status.OwnerID, err)) + } - if err := validateSnapshotMaxCount(newVolume.Spec.SnapshotMaxCount); err != nil { - return werror.NewInvalidError(err.Error(), "spec.snapshotMaxCount") - } + if node.Spec.DataEngineUpgradeRequested { + if oldVolume.Spec.NodeID == "" && newVolume.Spec.NodeID != "" { + return werror.NewInvalidError(fmt.Sprintf("volume %v is not allowed to attach to node %v during v2 data engine upgrade", + newVolume.Name, newVolume.Spec.NodeID), "spec.nodeID") + } - if err := validateSnapshotMaxSize(newVolume.Spec.Size, newVolume.Spec.SnapshotMaxSize); err != nil { - return werror.NewInvalidError(err.Error(), "spec.snapshotMaxSize") - } + if oldVolume.Spec.TargetNodeID == "" && oldVolume.Spec.TargetNodeID != newVolume.Spec.TargetNodeID { + if oldVolume.Status.Robustness != longhorn.VolumeRobustnessHealthy { + return werror.NewInvalidError(fmt.Sprintf("unable to change targetNodeID for volume %v when the volume is not healthy during v2 data engine upgrade", newVolume.Name), "spec.targetNodeID") + } + + if newVolume.Spec.NumberOfReplicas <= 1 { + return werror.NewInvalidError(fmt.Sprintf("unable to change targetNodeID for volume %v when the volume has only one replica during v2 data engine upgrade", newVolume.Name), "spec.targetNodeID") + } + + // Setting targetNodeID to the same node is not meaningless and is not allowed + if newVolume.Spec.TargetNodeID == oldVolume.Spec.NodeID { + return werror.NewInvalidError(fmt.Sprintf("unable to change targetNodeID for volume %v to the same node %v during v2 data engine upgrade", newVolume.Name, newVolume.Spec.TargetNodeID), "spec.targetNodeID") + } + + instanceManagerImage, err := v.ds.GetSettingValueExisted(types.SettingNameDefaultInstanceManagerImage) + if err != nil { + return werror.NewInternalError(fmt.Sprintf("failed to get setting %v, err %v", types.SettingNameDefaultInstanceManagerImage, err)) + } + + // Only allow to upgrade default instance manager image + if oldVolume.Spec.Image == instanceManagerImage { + return werror.NewInvalidError(fmt.Sprintf("volume %v is already using instance manager image %v", newVolume.Name, instanceManagerImage), "") + } + if newVolume.Spec.Image != instanceManagerImage { + return werror.NewInvalidError(fmt.Sprintf("volume %v should use instance manager image %v", newVolume.Name, instanceManagerImage), "") + } + } + } else { + if oldVolume.Spec.TargetNodeID != newVolume.Spec.TargetNodeID { + return werror.NewInvalidError(fmt.Errorf("unable to change targetNodeID for volume %v when the node has not requested v2 data engine upgrade", newVolume.Name).Error(), "spec.targetNodeID") + } - if (oldVolume.Spec.SnapshotMaxCount != newVolume.Spec.SnapshotMaxCount) || - (oldVolume.Spec.SnapshotMaxSize != newVolume.Spec.SnapshotMaxSize) { - if err := v.validateUpdatingSnapshotMaxCountAndSize(oldVolume, newVolume); err != nil { - return err + if oldVolume.Spec.NodeID != "" { + if oldVolume.Spec.Image != newVolume.Spec.Image { + return werror.NewInvalidError(fmt.Sprintf("unable to change image for attached volume %v when the node has not requested v2 data engine upgrade", newVolume.Name), "spec.image") + } + } + } + } else { + if newVolume.Spec.TargetNodeID != "" { + return werror.NewInvalidError("unable to set targetNodeID for volume when the volume is not using data engine v2", "spec.targetNodeID") } } + return nil } diff --git a/webhook/server/mutation.go b/webhook/server/mutation.go index d242ffdea2..a463e6a85b 100644 --- a/webhook/server/mutation.go +++ b/webhook/server/mutation.go @@ -13,10 +13,12 @@ import ( "github.com/longhorn/longhorn-manager/webhook/resources/backup" "github.com/longhorn/longhorn-manager/webhook/resources/backupbackingimage" "github.com/longhorn/longhorn-manager/webhook/resources/backupvolume" + "github.com/longhorn/longhorn-manager/webhook/resources/dataengineupgrademanager" "github.com/longhorn/longhorn-manager/webhook/resources/engine" "github.com/longhorn/longhorn-manager/webhook/resources/engineimage" "github.com/longhorn/longhorn-manager/webhook/resources/instancemanager" "github.com/longhorn/longhorn-manager/webhook/resources/node" + "github.com/longhorn/longhorn-manager/webhook/resources/nodedataengineupgrade" "github.com/longhorn/longhorn-manager/webhook/resources/orphan" "github.com/longhorn/longhorn-manager/webhook/resources/recurringjob" "github.com/longhorn/longhorn-manager/webhook/resources/replica" @@ -50,6 +52,8 @@ func Mutation(ds *datastore.DataStore) (http.Handler, []admission.Resource, erro volumeattachment.NewMutator(ds), instancemanager.NewMutator(ds), backupbackingimage.NewMutator(ds), + dataengineupgrademanager.NewMutator(ds), + nodedataengineupgrade.NewMutator(ds), } router := webhook.NewRouter() diff --git a/webhook/server/validation.go b/webhook/server/validation.go index a664004eaf..c24091508a 100644 --- a/webhook/server/validation.go +++ b/webhook/server/validation.go @@ -10,9 +10,11 @@ import ( "github.com/longhorn/longhorn-manager/util" "github.com/longhorn/longhorn-manager/webhook/admission" "github.com/longhorn/longhorn-manager/webhook/resources/backingimage" + "github.com/longhorn/longhorn-manager/webhook/resources/dataengineupgrademanager" "github.com/longhorn/longhorn-manager/webhook/resources/engine" "github.com/longhorn/longhorn-manager/webhook/resources/instancemanager" "github.com/longhorn/longhorn-manager/webhook/resources/node" + "github.com/longhorn/longhorn-manager/webhook/resources/nodedataengineupgrade" "github.com/longhorn/longhorn-manager/webhook/resources/orphan" "github.com/longhorn/longhorn-manager/webhook/resources/recurringjob" "github.com/longhorn/longhorn-manager/webhook/resources/replica" @@ -47,6 +49,8 @@ func Validation(ds *datastore.DataStore) (http.Handler, []admission.Resource, er engine.NewValidator(ds), replica.NewValidator(ds), instancemanager.NewValidator(ds), + dataengineupgrademanager.NewValidator(ds), + nodedataengineupgrade.NewValidator(ds), } router := webhook.NewRouter()