diff --git a/api/v1alpha1/lvmcluster_types.go b/api/v1alpha1/lvmcluster_types.go index 9612eeae7..e05a37788 100644 --- a/api/v1alpha1/lvmcluster_types.go +++ b/api/v1alpha1/lvmcluster_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -24,6 +25,11 @@ import ( // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +const ( + // ConditionLVMClusterValid communicates if LVMCluster CR is invalid + ConditionLVMClusterValid conditionsv1.ConditionType = "LVMClusterValid" +) + // LVMClusterSpec defines the desired state of LVMCluster type LVMClusterSpec struct { // Important: Run "make" to regenerate code after modifying this file @@ -90,6 +96,12 @@ type DeviceSelector struct { // MinSize is the minimum size of the device which needs to be included. Defaults to `1Gi` if empty // +optional // MinSize *resource.Quantity `json:"minSize,omitempty"` + + // A list of device paths which would be chosen for creating Volume Group. + // For example "/dev/disk/by-path/pci-0000:04:00.0-nvme-1" + // We discourage using the device names as they can change over node restarts. + // +optional + Paths []string `json:"paths,omitempty"` } // type DeviceClassConfig struct { @@ -108,6 +120,11 @@ type LVMClusterStatus struct { // Ready describes if the LVMCluster is ready. // +optional Ready bool `json:"ready,omitempty"` + + // Conditions describes the state of the resource. + // +optional + Conditions []conditionsv1.Condition `json:"conditions,omitempty"` + // DeviceClassStatuses describes the status of all deviceClasses DeviceClassStatuses []DeviceClassStatus `json:"deviceClassStatuses,omitempty"` } @@ -132,6 +149,8 @@ type NodeStatus struct { Node string `json:"node,omitempty"` // Status is the status of the VG on the node Status VGStatusType `json:"status,omitempty"` + // Reason provides more detail on the VG creation status + Reason string `json:"reason,omitempty"` // Devices is the list of devices used by the deviceclass Devices []string `json:"devices,omitempty"` } diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 2cf05383a..e2d52d216 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ limitations under the License. package v1alpha1 import ( + conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1" "k8s.io/api/core/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -32,7 +33,7 @@ func (in *DeviceClass) DeepCopyInto(out *DeviceClass) { if in.DeviceSelector != nil { in, out := &in.DeviceSelector, &out.DeviceSelector *out = new(DeviceSelector) - **out = **in + (*in).DeepCopyInto(*out) } if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector @@ -81,6 +82,11 @@ func (in *DeviceClassStatus) DeepCopy() *DeviceClassStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeviceSelector) DeepCopyInto(out *DeviceSelector) { *out = *in + if in.Paths != nil { + in, out := &in.Paths, &out.Paths + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeviceSelector. @@ -178,6 +184,13 @@ func (in *LVMClusterSpec) DeepCopy() *LVMClusterSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LVMClusterStatus) DeepCopyInto(out *LVMClusterStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]conditionsv1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.DeviceClassStatuses != nil { in, out := &in.DeviceClassStatuses, &out.DeviceClassStatuses *out = make([]DeviceClassStatus, len(*in)) @@ -358,7 +371,7 @@ func (in *LVMVolumeGroupSpec) DeepCopyInto(out *LVMVolumeGroupSpec) { if in.DeviceSelector != nil { in, out := &in.DeviceSelector, &out.DeviceSelector *out = new(DeviceSelector) - **out = **in + (*in).DeepCopyInto(*out) } if in.NodeSelector != nil { in, out := &in.NodeSelector, &out.NodeSelector diff --git a/bundle/manifests/lvm.topolvm.io_lvmclusters.yaml b/bundle/manifests/lvm.topolvm.io_lvmclusters.yaml index 3122dc030..9ebd5f76d 100644 --- a/bundle/manifests/lvm.topolvm.io_lvmclusters.yaml +++ b/bundle/manifests/lvm.topolvm.io_lvmclusters.yaml @@ -47,6 +47,15 @@ spec: deviceSelector: description: DeviceSelector is a set of rules that should match for a device to be included in the LVMCluster + properties: + paths: + description: A list of device paths which would be chosen + for creating Volume Group. For example "/dev/disk/by-path/pci-0000:04:00.0-nvme-1" + We discourage using the device names as they can change + over node restarts. + items: + type: string + type: array type: object name: description: 'Name of the class, the VG and possibly the @@ -220,6 +229,33 @@ spec: status: description: LVMClusterStatus defines the observed state of LVMCluster properties: + conditions: + description: Conditions describes the state of the resource. + items: + description: Condition represents the state of the operator's reconciliation + functionality. + properties: + lastHeartbeatTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + description: ConditionType is the state of the operator's reconciliation + functionality. + type: string + required: + - status + - type + type: object + type: array deviceClassStatuses: description: DeviceClassStatuses describes the status of all deviceClasses items: @@ -245,6 +281,10 @@ spec: node: description: Node is the name of the node type: string + reason: + description: Reason provides more detail on the VG creation + status + type: string status: description: Status is the status of the VG on the node type: string diff --git a/bundle/manifests/lvm.topolvm.io_lvmvolumegroups.yaml b/bundle/manifests/lvm.topolvm.io_lvmvolumegroups.yaml index 684223dcc..2ca3fda94 100644 --- a/bundle/manifests/lvm.topolvm.io_lvmvolumegroups.yaml +++ b/bundle/manifests/lvm.topolvm.io_lvmvolumegroups.yaml @@ -37,6 +37,15 @@ spec: deviceSelector: description: DeviceSelector is a set of rules that should match for a device to be included in this TopoLVMCluster + properties: + paths: + description: A list of device paths which would be chosen for + creating Volume Group. For example "/dev/disk/by-path/pci-0000:04:00.0-nvme-1" + We discourage using the device names as they can change over + node restarts. + items: + type: string + type: array type: object nodeSelector: description: NodeSelector chooses nodes diff --git a/config/crd/bases/lvm.topolvm.io_lvmclusters.yaml b/config/crd/bases/lvm.topolvm.io_lvmclusters.yaml index 15076fee2..252f025dc 100644 --- a/config/crd/bases/lvm.topolvm.io_lvmclusters.yaml +++ b/config/crd/bases/lvm.topolvm.io_lvmclusters.yaml @@ -49,6 +49,15 @@ spec: deviceSelector: description: DeviceSelector is a set of rules that should match for a device to be included in the LVMCluster + properties: + paths: + description: A list of device paths which would be chosen + for creating Volume Group. For example "/dev/disk/by-path/pci-0000:04:00.0-nvme-1" + We discourage using the device names as they can change + over node restarts. + items: + type: string + type: array type: object name: description: 'Name of the class, the VG and possibly the @@ -222,6 +231,33 @@ spec: status: description: LVMClusterStatus defines the observed state of LVMCluster properties: + conditions: + description: Conditions describes the state of the resource. + items: + description: Condition represents the state of the operator's reconciliation + functionality. + properties: + lastHeartbeatTime: + format: date-time + type: string + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + description: ConditionType is the state of the operator's reconciliation + functionality. + type: string + required: + - status + - type + type: object + type: array deviceClassStatuses: description: DeviceClassStatuses describes the status of all deviceClasses items: @@ -247,6 +283,10 @@ spec: node: description: Node is the name of the node type: string + reason: + description: Reason provides more detail on the VG creation + status + type: string status: description: Status is the status of the VG on the node type: string diff --git a/config/crd/bases/lvm.topolvm.io_lvmvolumegroups.yaml b/config/crd/bases/lvm.topolvm.io_lvmvolumegroups.yaml index 4d037dfad..d48897ae1 100644 --- a/config/crd/bases/lvm.topolvm.io_lvmvolumegroups.yaml +++ b/config/crd/bases/lvm.topolvm.io_lvmvolumegroups.yaml @@ -39,6 +39,15 @@ spec: deviceSelector: description: DeviceSelector is a set of rules that should match for a device to be included in this TopoLVMCluster + properties: + paths: + description: A list of device paths which would be chosen for + creating Volume Group. For example "/dev/disk/by-path/pci-0000:04:00.0-nvme-1" + We discourage using the device names as they can change over + node restarts. + items: + type: string + type: array type: object nodeSelector: description: NodeSelector chooses nodes diff --git a/controllers/conditions.go b/controllers/conditions.go new file mode 100644 index 000000000..ac59d3d29 --- /dev/null +++ b/controllers/conditions.go @@ -0,0 +1,35 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1" + corev1 "k8s.io/api/core/v1" + + lvmv1alpha1 "github.com/red-hat-storage/lvm-operator/api/v1alpha1" +) + +func setLVMClusterCRvalidCondition(conditions *[]conditionsv1.Condition, + status corev1.ConditionStatus, reason string, message string) { + + conditionsv1.SetStatusCondition(conditions, conditionsv1.Condition{ + Type: lvmv1alpha1.ConditionLVMClusterValid, + Status: status, + Reason: reason, + Message: message, + }) +} diff --git a/controllers/lvm_volumegroup.go b/controllers/lvm_volumegroup.go index 4c1c14162..6a6eccbad 100644 --- a/controllers/lvm_volumegroup.go +++ b/controllers/lvm_volumegroup.go @@ -43,8 +43,15 @@ func (c lvmVG) ensureCreated(r *LVMClusterReconciler, ctx context.Context, lvmCl lvmVolumeGroups := c.getLvmVolumeGroups(r, lvmCluster) for _, volumeGroup := range lvmVolumeGroups { - result, err := cutil.CreateOrUpdate(ctx, r.Client, volumeGroup, func() error { - // no need to mutate any field + existingVolumeGroup := &lvmv1alpha1.LVMVolumeGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: volumeGroup.Name, + Namespace: volumeGroup.Namespace, + }, + } + result, err := cutil.CreateOrUpdate(ctx, r.Client, existingVolumeGroup, func() error { + existingVolumeGroup.Finalizers = volumeGroup.Finalizers + existingVolumeGroup.Spec = volumeGroup.Spec return nil }) diff --git a/controllers/lvmcluster_controller.go b/controllers/lvmcluster_controller.go index f77f7c7ee..15abe14cb 100644 --- a/controllers/lvmcluster_controller.go +++ b/controllers/lvmcluster_controller.go @@ -115,6 +115,13 @@ func (r *LVMClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) // Error reading the object - requeue the request. return ctrl.Result{}, err } + + err = r.verifyLvmClusterSpec(ctx, lvmCluster) + if err != nil { + r.Log.Error(err, "failed to verify LVMCluster") + return ctrl.Result{}, err + } + err = r.checkIfOpenshift(ctx) if err != nil { r.Log.Error(err, "failed to check cluster type") @@ -220,6 +227,71 @@ func (r *LVMClusterReconciler) reconcile(ctx context.Context, instance *lvmv1alp return ctrl.Result{}, nil } +func (r *LVMClusterReconciler) verifyLvmClusterSpec(ctx context.Context, instance *lvmv1alpha1.LVMCluster) error { + + err := r.verifyLvmClusterDeviceClasses(ctx, instance) + if err != nil { + // Apply status changes + updateErr := r.Client.Status().Update(ctx, instance) + if updateErr != nil { + r.Log.Error(updateErr, "failed to update LVMCluster status") + return updateErr + } + return err + } + + return nil +} + +func (r *LVMClusterReconciler) verifyLvmClusterDeviceClasses(ctx context.Context, instance *lvmv1alpha1.LVMCluster) error { + + // make sure no device overlap with another VGs + // use map to find the duplicate entries for paths + /* + { + "nodeSelector1": { + "/dev/sda": "vg1", + "/dev/sdb": "vg1" + }, + "nodeSelector2": { + "/dev/sda": "vg1", + "/dev/sdb": "vg1" + } + } + */ + devices := make(map[string]map[string]string) + + for _, deviceClass := range instance.Spec.Storage.DeviceClasses { + if deviceClass.DeviceSelector != nil { + nodeSelector := deviceClass.NodeSelector.String() + for _, path := range deviceClass.DeviceSelector.Paths { + if val, ok := devices[nodeSelector][path]; ok { + var err error + if val != deviceClass.Name { + err = fmt.Errorf("Error: device path %s overlaps in two different deviceClasss %s and %s", path, val, deviceClass.Name) + } else { + err = fmt.Errorf("Error: device path %s is specified at multiple places in deviceClass %s", path, val) + } + r.Log.Error(err, "failed to verify device paths") + + setLVMClusterCRvalidCondition(&instance.Status.Conditions, corev1.ConditionFalse, "CRInvalid", err.Error()) + return err + } + + if devices[nodeSelector] == nil { + devices[nodeSelector] = make(map[string]string) + } + + devices[nodeSelector][path] = deviceClass.Name + } + } + } + + setLVMClusterCRvalidCondition(&instance.Status.Conditions, corev1.ConditionTrue, "CRValid", "LVMCluster CR is validated successfully") + + return nil +} + func (r *LVMClusterReconciler) updateLVMClusterStatus(ctx context.Context, instance *lvmv1alpha1.LVMCluster) error { vgNodeMap := make(map[string][]lvmv1alpha1.NodeStatus) @@ -238,6 +310,7 @@ func (r *LVMClusterReconciler) updateLVMClusterStatus(ctx context.Context, insta vgNodeMap[item.Name] = []lvmv1alpha1.NodeStatus{ { Node: nodeItem.Name, + Reason: item.Reason, Status: item.Status, Devices: item.Devices, }, diff --git a/go.mod b/go.mod index 943fbeb54..17774f207 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/red-hat-storage/lvm-operator go 1.17 require ( + github.com/aws/aws-sdk-go v1.44.10 github.com/go-logr/logr v1.2.3 github.com/google/go-cmp v0.5.8 github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0 @@ -11,6 +12,7 @@ require ( github.com/onsi/gomega v1.19.0 github.com/openshift/api v0.0.0-20211028023115-7224b732cc14 github.com/openshift/client-go v0.0.0-20210831095141-e19a065e79f7 + github.com/openshift/custom-resource-status v1.1.2 github.com/operator-framework/api v0.14.0 github.com/prometheus/client_golang v1.12.1 github.com/stretchr/testify v1.7.0 @@ -24,8 +26,6 @@ require ( sigs.k8s.io/yaml v1.3.0 ) -require github.com/aws/aws-sdk-go v1.44.10 - require ( cloud.google.com/go/compute v1.6.1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect diff --git a/go.sum b/go.sum index f579df471..0c7a020a1 100644 --- a/go.sum +++ b/go.sum @@ -275,6 +275,7 @@ github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1 github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= @@ -578,6 +579,7 @@ github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY= github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= @@ -588,6 +590,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= github.com/onsi/gomega v1.14.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/openshift/api v0.0.0-20210831091943-07e756545ac1/go.mod h1:RsQCVJu4qhUawxxDP7pGlwU3IA4F01wYm3qKEu29Su8= @@ -596,6 +599,8 @@ github.com/openshift/api v0.0.0-20211028023115-7224b732cc14/go.mod h1:RsQCVJu4qh github.com/openshift/build-machinery-go v0.0.0-20210712174854-1bb7fd1518d3/go.mod h1:b1BuldmJlbA/xYtdZvKi+7j5YGB44qJUJDZ9zwiNCfE= github.com/openshift/client-go v0.0.0-20210831095141-e19a065e79f7 h1:iKVU5Tga76kiCWpq9giPi0TfI/gZcFoYb7/x+1SkgwM= github.com/openshift/client-go v0.0.0-20210831095141-e19a065e79f7/go.mod h1:D6P8RkJzwdkBExQdYUnkWcePMLBiTeCCr8eQIQ7y8Dk= +github.com/openshift/custom-resource-status v1.1.2 h1:C3DL44LEbvlbItfd8mT5jWrqPfHnSOQoQf/sypqA6A4= +github.com/openshift/custom-resource-status v1.1.2/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/operator-framework/api v0.14.0 h1:5nk8fQL8l+dDxi11hZi0T7nqhhoIQLn+qL2DhMEGnoE= github.com/operator-framework/api v0.14.0/go.mod h1:r/erkmp9Kc1Al4dnxmRkJYc0uCtD5FohN9VuJ5nTxz0= @@ -843,6 +848,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -892,6 +898,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1010,6 +1017,7 @@ golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1134,6 +1142,7 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10-0.20220218145154-897bd77cd717/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= @@ -1373,6 +1382,7 @@ k8s.io/api v0.19.0/go.mod h1:I1K45XlvTrDjmj5LoM5LuP/KYrhWbjUKT/SoPG0qTjw= k8s.io/api v0.22.1/go.mod h1:bh13rkTp3F1XEaLGykbyRD2QaTTzPm0e/BMd8ptFONY= k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= +k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ= k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= k8s.io/api v0.24.0 h1:J0hann2hfxWr1hinZIDefw7Q96wmCBx6SSB8IY0MdDg= @@ -1386,6 +1396,7 @@ k8s.io/apimachinery v0.19.0/go.mod h1:DnPGDnARWFvYa3pMHgSxtbZb7gpzzAZ1pTfaUNDVlm k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= +k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/apimachinery v0.24.0 h1:ydFCyC/DjCvFCHK5OPMKBlxayQytB8pxy8YQInd5UyQ= @@ -1406,6 +1417,7 @@ k8s.io/code-generator v0.19.0/go.mod h1:moqLn7w0t9cMs4+5CQyxnfA/HV8MF6aAVENF+WZZ k8s.io/code-generator v0.22.1/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= +k8s.io/code-generator v0.23.3/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= k8s.io/code-generator v0.24.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI15w= k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllunGug= @@ -1431,6 +1443,7 @@ k8s.io/klog/v2 v2.60.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk= k8s.io/kube-openapi v0.0.0-20220413171646-5e7f5fdc6da6 h1:nBQrWPlrNIiw0BsX6a6MKr1itkm0ZS0Nl97kNLitFfI= k8s.io/kube-openapi v0.0.0-20220413171646-5e7f5fdc6da6/go.mod h1:daOouuuwd9JXpv1L7Y34iV3yf6nxzipkKMWWlqlvK9M= diff --git a/pkg/internal/block_device.go b/pkg/internal/block_device.go index 8543fec54..504732e1f 100644 --- a/pkg/internal/block_device.go +++ b/pkg/internal/block_device.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "path/filepath" "strings" ) @@ -23,6 +24,9 @@ const ( // mount string to find if a path is part of kubernetes pluginString = "plugins/kubernetes.io" + + DiskByNamePrefix = "/dev" + DiskByPathPrefix = "/dev/disk/by-path" ) // BlockDevice is the a block device as output by lsblk. @@ -39,9 +43,12 @@ type BlockDevice struct { Children []BlockDevice `json:"children,omitempty"` Rotational string `json:"rota"` ReadOnly string `json:"ro,omitempty"` - PathByID string `json:"pathByID,omitempty"` Serial string `json:"serial,omitempty"` PartLabel string `json:"partLabel,omitempty"` + + // DiskByPath is not part of lsblk output + // fetch and set it only if specified in the CR + DiskByPath string } // ListBlockDevices using the lsblk command @@ -49,7 +56,7 @@ func ListBlockDevices(exec Executor) ([]BlockDevice, error) { // var output bytes.Buffer var blockDeviceMap map[string][]BlockDevice columns := "NAME,ROTA,TYPE,SIZE,MODEL,VENDOR,RO,STATE,KNAME,SERIAL,PARTLABEL,FSTYPE" - args := []string{"--json", "-o", columns} + args := []string{"--json", "--paths", "-o", columns} output, err := exec.ExecuteCommandWithOutput("lsblk", args...) if err != nil { @@ -64,6 +71,28 @@ func ListBlockDevices(exec Executor) ([]BlockDevice, error) { return blockDeviceMap["blockdevices"], nil } +func ListDiskByPath(exec Executor) (map[string]string, error) { + + devices := make(map[string]string) + + diskByPathDir := filepath.Join(DiskByPathPrefix, "/*") + + paths, err := filepath.Glob(diskByPathDir) + if err != nil { + return nil, fmt.Errorf("could not list devices in %q: %w", diskByPathDir, err) + } + + for _, path := range paths { + diskName, err := filepath.EvalSymlinks(path) + if err != nil { + return nil, fmt.Errorf("could not eval symLink %q:%w", path, err) + } + devices[diskName] = path + } + + return devices, nil +} + // IsUsableLoopDev returns true if the loop device isn't in use by Kubernetes // by matching the back file path against a standard string used to mount devices // from host into pods @@ -76,7 +105,7 @@ func (b BlockDevice) IsUsableLoopDev(exec Executor) (bool, error) { usable := true - args := []string{fmt.Sprintf("/dev/%s", b.Name), "-O", "BACK-FILE", "--json"} + args := []string{b.Name, "-O", "BACK-FILE", "--json"} output, err := exec.ExecuteCommandWithOutput(losetupPath, args...) if err != nil { @@ -123,7 +152,7 @@ func (b BlockDevice) HasBindMounts() (bool, string, error) { mountInfoList := strings.Split(mountInfo, " ") if len(mountInfoList) >= 10 { // device source is 4th field for bind mounts and 10th for regular mounts - if mountInfoList[3] == fmt.Sprintf("/%s", b.KName) || mountInfoList[9] == fmt.Sprintf("/dev/%s", b.KName) { + if mountInfoList[3] == fmt.Sprintf("/%s", filepath.Base(b.KName)) || mountInfoList[9] == b.KName { return true, mountInfoList[4], nil } } diff --git a/pkg/vgmanager/lvm.go b/pkg/vgmanager/lvm.go index efb7594ca..3b698ab30 100644 --- a/pkg/vgmanager/lvm.go +++ b/pkg/vgmanager/lvm.go @@ -38,6 +38,7 @@ const ( vgRemoveCmd = "/usr/sbin/vgremove" pvRemoveCmd = "/usr/sbin/pvremove" lvCreateCmd = "/usr/sbin/lvcreate" + lvExtendCmd = "/usr/sbin/lvextend" lvRemoveCmd = "/usr/sbin/lvremove" lvChangeCmd = "/usr/sbin/lvchange" ) @@ -46,7 +47,8 @@ const ( type vgsOutput struct { Report []struct { Vg []struct { - Name string `json:"vg_name"` + Name string `json:"vg_name"` + VgSize string `json:"vg_size"` } `json:"vg"` } `json:"report"` } @@ -69,6 +71,7 @@ type lvsOutput struct { VgName string `json:"vg_name"` PoolName string `json:"pool_lv"` LvAttr string `json:"lv_attr"` + LvSize string `json:"lv_size"` } `json:"lv"` } `json:"report"` } @@ -78,6 +81,9 @@ type VolumeGroup struct { // Name is the name of the volume group Name string `json:"vg_name"` + // VgSize is the size of the volume group + VgSize string `json:"vg_size"` + // PVs is the list of physical volumes associated with the volume group PVs []string `json:"pvs"` } @@ -148,7 +154,7 @@ func GetVolumeGroup(exec internal.Executor, name string) (*VolumeGroup, error) { // TODO: Check if `vgs --reportformat json` can be used to filter out the exact volume group name. args := []string{ - "vgs", "--reportformat", "json", + "vgs", "--units", "g", "--reportformat", "json", } if err := execute(exec, res, args...); err != nil { return nil, fmt.Errorf("failed to list volume groups. %v", err) @@ -160,6 +166,7 @@ func GetVolumeGroup(exec internal.Executor, name string) (*VolumeGroup, error) { for _, vg := range report.Vg { if vg.Name == name { volumeGroup.Name = vg.Name + volumeGroup.VgSize = vg.VgSize vgFound = true break } @@ -249,7 +256,7 @@ func ListLogicalVolumes(exec internal.Executor, vgName string) ([]string, error) func GetLVSOutput(exec internal.Executor, vgName string) (*lvsOutput, error) { res := new(lvsOutput) args := []string{ - "lvs", "-S", fmt.Sprintf("vgname=%s", vgName), "--reportformat", "json", + "lvs", "-S", fmt.Sprintf("vgname=%s", vgName), "--units", "g", "--reportformat", "json", } if err := execute(exec, res, args...); err != nil { return nil, err diff --git a/pkg/vgmanager/vgmanager_controller.go b/pkg/vgmanager/vgmanager_controller.go index 2068b16b1..50d16e2d0 100644 --- a/pkg/vgmanager/vgmanager_controller.go +++ b/pkg/vgmanager/vgmanager_controller.go @@ -20,6 +20,8 @@ import ( "context" "fmt" "os" + "path/filepath" + "strconv" "strings" "time" @@ -116,14 +118,6 @@ func (r *VGReconciler) reconcile(ctx context.Context, req ctrl.Request, volumeGr return ctrl.Result{}, err } - //Get the block devices that can be used for this volumegroup - matchingDevices, delayedDevices, err := r.getMatchingDevicesForVG(volumeGroup) - if err != nil { - // Failed to get devices for this vg. Reconcile again. - r.Log.Error(err, "failed to get block devices for volumegroup", "VGName", volumeGroup.Name) - return reconcileAgain, err - } - // Read the lvmd config file lvmdConfig, err := loadLVMDConfig() if err != nil { @@ -148,18 +142,23 @@ func (r *VGReconciler) reconcile(ctx context.Context, req ctrl.Request, volumeGr } status := &lvmv1alpha1.VGStatus{ - Name: req.Name, - Status: lvmv1alpha1.VGStatusReady, - Reason: "", + Name: req.Name, } _, found := deviceClassMap[volumeGroup.Name] - if found { - volGrpHostInfo, err := GetVolumeGroup(r.executor, volumeGroup.Name) - if err != nil { - r.Log.Error(err, "failed to get volume group from the host", "VGName", volumeGroup.Name) - } else { - status.Devices = volGrpHostInfo.PVs + + //Get the block devices that can be used for this volumegroup + matchingDevices, delayedDevices, err := r.getMatchingDevicesForVG(volumeGroup) + if err != nil { + r.Log.Error(err, "failed to get block devices for volumegroup", "name", volumeGroup.Name) + + status.Reason = err.Error() + if statuserr := r.updateStatus(ctx, status); statuserr != nil { + r.Log.Error(statuserr, "failed to update status", "name", volumeGroup.Name) + return reconcileAgain, nil } + + // Failed to get devices for this vg. Reconcile again. + return reconcileAgain, err } if len(matchingDevices) == 0 { @@ -170,7 +169,7 @@ func (r *VGReconciler) reconcile(ctx context.Context, req ctrl.Request, volumeGr if found { // Update the status again just to be safe. - if statuserr := r.updateStatus(ctx); statuserr != nil { + if statuserr := r.updateStatus(ctx, nil); statuserr != nil { r.Log.Error(statuserr, "failed to update status", "name", volumeGroup.Name) return reconcileAgain, nil } @@ -181,9 +180,11 @@ func (r *VGReconciler) reconcile(ctx context.Context, req ctrl.Request, volumeGr // create/extend VG and update lvmd config err = r.addDevicesToVG(volumeGroup.Name, matchingDevices) if err != nil { + status.Reason = err.Error() + r.Log.Error(err, "failed to create/extend volume group", "VGName", volumeGroup.Name) - if statuserr := r.updateStatus(ctx); statuserr != nil { + if statuserr := r.updateStatus(ctx, status); statuserr != nil { r.Log.Error(statuserr, "failed to update status", "VGName", volumeGroup.Name) } return reconcileAgain, err @@ -224,7 +225,7 @@ func (r *VGReconciler) reconcile(ctx context.Context, req ctrl.Request, volumeGr } if err == nil { - if statuserr := r.updateStatus(ctx); statuserr != nil { + if statuserr := r.updateStatus(ctx, nil); statuserr != nil { r.Log.Error(statuserr, "failed to update status", "VGName", volumeGroup.Name) return reconcileAgain, nil } @@ -251,7 +252,11 @@ func (r *VGReconciler) addThinPoolToVG(vgName string, config *lvmv1alpha1.ThinPo if lv.Name == config.Name { if strings.Contains(lv.LvAttr, "t") { r.Log.Info("lvm thinpool already exists", "VGName", vgName, "ThinPool", config.Name) - return nil + err = r.extendThinPool(vgName, lv.LvSize, config) + if err != nil { + r.Log.Error(err, "failed to extend the lvm thinpool", "VGName", vgName, "ThinPool", config.Name) + } + return err } return fmt.Errorf("failed to create thin pool %q. Logical volume with same name already exists", config.Name) @@ -269,6 +274,45 @@ func (r *VGReconciler) addThinPoolToVG(vgName string, config *lvmv1alpha1.ThinPo return nil } +func (r *VGReconciler) extendThinPool(vgName string, lvSize string, config *lvmv1alpha1.ThinPoolConfig) error { + + vg, err := GetVolumeGroup(r.executor, vgName) + if err != nil { + if err != ErrVolumeGroupNotFound { + return fmt.Errorf("failed to get volume group. %q, %v", vgName, err) + } + return nil + } + + thinPoolSize, err := strconv.ParseFloat(lvSize[:len(lvSize)-1], 64) + if err != nil { + return fmt.Errorf("failed to parse lvSize. %v", err) + } + + vgSize, err := strconv.ParseFloat(vg.VgSize[:len(vg.VgSize)-1], 64) + if err != nil { + return fmt.Errorf("failed to parse vgSize. %v", err) + } + + // return if thinPoolSize does not require expansion + if config.SizePercent <= int((thinPoolSize/vgSize)*100) { + return nil + } + + r.Log.Info("extending lvm thinpool ", "VGName", vgName, "ThinPool", config.Name) + + args := []string{"-l", fmt.Sprintf("%d%%Vg", config.SizePercent), fmt.Sprintf("%s/%s", vgName, config.Name)} + + _, err = r.executor.ExecuteCommandWithOutputAsHost(lvExtendCmd, args...) + if err != nil { + return fmt.Errorf("failed to extend thin pool %q in the volume group %q. %v", config.Name, vgName, err) + } + + r.Log.Info("successfully extended the thin pool in the volume group ", "thinpool", config.Name, "vgName", vgName) + + return nil +} + func (r *VGReconciler) processDelete(ctx context.Context, volumeGroup *lvmv1alpha1.LVMVolumeGroup) error { // Read the lvmd config file @@ -346,7 +390,7 @@ func (r *VGReconciler) processDelete(ctx context.Context, volumeGroup *lvmv1alph } } - if statuserr := r.updateStatus(ctx); statuserr != nil { + if statuserr := r.updateStatus(ctx, nil); statuserr != nil { r.Log.Error(statuserr, "failed to update status", "VGName", volumeGroup.Name) return statuserr } @@ -373,7 +417,11 @@ func (r *VGReconciler) addDevicesToVG(vgName string, devices []internal.BlockDev args := []string{vgName} for _, device := range devices { - args = append(args, fmt.Sprintf("/dev/%s", device.KName)) + if device.DiskByPath != "" { + args = append(args, device.DiskByPath) + } else { + args = append(args, device.KName) + } } var cmd string @@ -393,11 +441,83 @@ func (r *VGReconciler) addDevicesToVG(vgName string, devices []internal.BlockDev return nil } -// filterMatchingDevices returns unmatched and matched blockdevices -// TODO: Implement this -func filterMatchingDevices(blockDevices []internal.BlockDevice, volumeGroup *lvmv1alpha1.LVMVolumeGroup) ([]internal.BlockDevice, []internal.BlockDevice, error) { - // currently just match all devices - return []internal.BlockDevice{}, blockDevices, nil +// filterMatchingDevices returns matched blockdevices +func (r *VGReconciler) filterMatchingDevices(blockDevices []internal.BlockDevice, volumeGroup *lvmv1alpha1.LVMVolumeGroup) ([]internal.BlockDevice, error) { + + var filteredBlockDevices []internal.BlockDevice + + if volumeGroup.Spec.DeviceSelector != nil && len(volumeGroup.Spec.DeviceSelector.Paths) > 0 { + vgs, err := ListVolumeGroups(r.executor) + if err != nil { + return []internal.BlockDevice{}, fmt.Errorf("failed to list volume groups. %v", err) + } + + for _, path := range volumeGroup.Spec.DeviceSelector.Paths { + diskName, err := filepath.EvalSymlinks(path) + if err != nil { + err = fmt.Errorf("unable to find symlink for disk path %s: %v", path, err) + return []internal.BlockDevice{}, err + } + + isAlreadyExist := isDeviceAlreadyPartOfVG(vgs, diskName, volumeGroup) + if isAlreadyExist { + continue + } + + blockDevice, ok := hasExactDisk(blockDevices, diskName) + + if filepath.Dir(path) == internal.DiskByPathPrefix { + // handle disk by path here such as /dev/disk/by-path/pci-0000:87:00.0-nvme-1 + if ok { + blockDevice.DiskByPath = path + filteredBlockDevices = append(filteredBlockDevices, blockDevice) + } else { + err = fmt.Errorf("can not find device path %s, device name %s in the available block devices", path, diskName) + return []internal.BlockDevice{}, err + } + } else if filepath.Dir(path) == internal.DiskByNamePrefix { + // handle disk by names here such as /dev/nvme0n1 + if ok { + filteredBlockDevices = append(filteredBlockDevices, blockDevice) + } else { + err := fmt.Errorf("can not find device name %s in the available block devices", path) + return []internal.BlockDevice{}, err + } + } else { + err = fmt.Errorf("unsupported disk path format %s. only '/dev/disk/by-path' and '/dev/' links are currently supported", path) + return []internal.BlockDevice{}, err + } + } + + return filteredBlockDevices, nil + } + + // return all available block devices if none is specified in the CR + return blockDevices, nil +} + +func hasExactDisk(blockDevices []internal.BlockDevice, deviceName string) (internal.BlockDevice, bool) { + for _, blockDevice := range blockDevices { + if blockDevice.KName == deviceName { + return blockDevice, true + } + } + return internal.BlockDevice{}, false +} + +func isDeviceAlreadyPartOfVG(vgs []VolumeGroup, diskName string, volumeGroup *lvmv1alpha1.LVMVolumeGroup) bool { + + for _, vg := range vgs { + if vg.Name == volumeGroup.Name { + for _, pv := range vg.PVs { + if pv == diskName { + return true + } + } + } + } + + return false } func NodeSelectorMatchesNodeLabels(node *corev1.Node, nodeSelector *corev1.NodeSelector) (bool, error) { @@ -469,7 +589,7 @@ func (r *VGReconciler) getMatchingDevicesForVG(volumeGroup *lvmv1alpha1.LVMVolum } var matchingDevices []internal.BlockDevice - _, matchingDevices, err = filterMatchingDevices(remainingValidDevices, volumeGroup) + matchingDevices, err = r.filterMatchingDevices(remainingValidDevices, volumeGroup) if err != nil { r.Log.Error(err, "could not filter matching devices", "VGName", volumeGroup.Name) return nil, nil, err @@ -478,7 +598,8 @@ func (r *VGReconciler) getMatchingDevicesForVG(volumeGroup *lvmv1alpha1.LVMVolum return matchingDevices, delayedDevices, nil } -func (r *VGReconciler) generateVolumeGroupNodeStatus() (*lvmv1alpha1.LVMVolumeGroupNodeStatus, error) { +func (r *VGReconciler) generateVolumeGroupNodeStatus(deviceNameAndPaths map[string]string, + lvmVolumeGroups map[string]*lvmv1alpha1.LVMVolumeGroup, vgStatus *lvmv1alpha1.VGStatus) (*lvmv1alpha1.LVMVolumeGroupNodeStatus, error) { vgs, err := ListVolumeGroups(r.executor) if err != nil { @@ -488,15 +609,53 @@ func (r *VGReconciler) generateVolumeGroupNodeStatus() (*lvmv1alpha1.LVMVolumeGr //lvmvgstatus := vgNodeStatus.Spec.LVMVGStatus var statusarr []lvmv1alpha1.VGStatus + var vgExists bool + for _, vg := range vgs { + // Add pvs as per volumeGroup CR if path is given add path else add name + diskPattern := internal.DiskByNamePrefix + + lvmVolumeGroup, ok := lvmVolumeGroups[vg.Name] + if !ok { + continue + } + + deviceSelector := lvmVolumeGroup.Spec.DeviceSelector + if deviceSelector != nil && len(deviceSelector.Paths) > 0 { + if filepath.Dir(deviceSelector.Paths[0]) == internal.DiskByPathPrefix { + diskPattern = internal.DiskByPathPrefix + } + } + + devices := []string{} + if diskPattern == internal.DiskByPathPrefix { + for _, pv := range vg.PVs { + devices = append(devices, deviceNameAndPaths[pv]) + } + } else { + devices = vg.PVs + } + newStatus := &lvmv1alpha1.VGStatus{ Name: vg.Name, - Reason: "test", - Devices: vg.PVs, + Devices: devices, + Status: lvmv1alpha1.VGStatusReady, } + + if vgStatus != nil && vgStatus.Name == vg.Name { + vgExists = true + newStatus.Status = lvmv1alpha1.VGStatusDegraded + newStatus.Reason = vgStatus.Reason + } + statusarr = append(statusarr, *newStatus) } + if vgStatus != nil && !vgExists { + vgStatus.Status = lvmv1alpha1.VGStatusFailed + statusarr = append(statusarr, *vgStatus) + } + vgNodeStatus := &lvmv1alpha1.LVMVolumeGroupNodeStatus{ ObjectMeta: metav1.ObjectMeta{ Name: r.NodeName, @@ -510,9 +669,19 @@ func (r *VGReconciler) generateVolumeGroupNodeStatus() (*lvmv1alpha1.LVMVolumeGr return vgNodeStatus, nil } -func (r *VGReconciler) updateStatus(ctx context.Context) error { +func (r *VGReconciler) updateStatus(ctx context.Context, vgStatus *lvmv1alpha1.VGStatus) error { - vgNodeStatus, err := r.generateVolumeGroupNodeStatus() + deviceNameAndPaths, err := internal.ListDiskByPath(r.executor) + if err != nil { + return err + } + + lvmVolumeGroups, err := r.getAllLvmVolumeGroups(ctx) + if err != nil { + return err + } + + vgNodeStatus, err := r.generateVolumeGroupNodeStatus(deviceNameAndPaths, lvmVolumeGroups, vgStatus) nodeStatus := &lvmv1alpha1.LVMVolumeGroupNodeStatus{ ObjectMeta: metav1.ObjectMeta{ @@ -541,3 +710,21 @@ func (r *VGReconciler) updateStatus(ctx context.Context) error { } return err } + +func (r *VGReconciler) getAllLvmVolumeGroups(ctx context.Context) (map[string]*lvmv1alpha1.LVMVolumeGroup, error) { + + lvmVolumeGroupsMap := make(map[string]*lvmv1alpha1.LVMVolumeGroup) + + lvmVolumeGroups := &lvmv1alpha1.LVMVolumeGroupList{} + err := r.Client.List(ctx, lvmVolumeGroups, &client.ListOptions{Namespace: r.Namespace}) + if err != nil { + r.Log.Error(err, "failed to list LVMVolumeGroups") + return nil, err + } + + for i := range lvmVolumeGroups.Items { + lvmVolumeGroupsMap[lvmVolumeGroups.Items[i].Name] = &lvmVolumeGroups.Items[i] + } + + return lvmVolumeGroupsMap, nil +} diff --git a/vendor/github.com/openshift/custom-resource-status/LICENSE b/vendor/github.com/openshift/custom-resource-status/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/openshift/custom-resource-status/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/openshift/custom-resource-status/conditions/v1/conditions.go b/vendor/github.com/openshift/custom-resource-status/conditions/v1/conditions.go new file mode 100644 index 000000000..bbeee804a --- /dev/null +++ b/vendor/github.com/openshift/custom-resource-status/conditions/v1/conditions.go @@ -0,0 +1,104 @@ +package v1 + +import ( + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// SetStatusCondition sets the corresponding condition in conditions to newCondition. +func SetStatusCondition(conditions *[]Condition, newCondition Condition) { + if conditions == nil { + conditions = &[]Condition{} + } + existingCondition := FindStatusCondition(*conditions, newCondition.Type) + if existingCondition == nil { + newCondition.LastTransitionTime = metav1.NewTime(time.Now()) + newCondition.LastHeartbeatTime = metav1.NewTime(time.Now()) + *conditions = append(*conditions, newCondition) + return + } + + if existingCondition.Status != newCondition.Status { + existingCondition.Status = newCondition.Status + existingCondition.LastTransitionTime = metav1.NewTime(time.Now()) + } + + existingCondition.Reason = newCondition.Reason + existingCondition.Message = newCondition.Message + existingCondition.LastHeartbeatTime = metav1.NewTime(time.Now()) +} + +// SetStatusConditionNoHearbeat sets the corresponding condition in conditions to newCondition +// without setting lastHeartbeatTime. +func SetStatusConditionNoHeartbeat(conditions *[]Condition, newCondition Condition) { + if conditions == nil { + conditions = &[]Condition{} + } + existingCondition := FindStatusCondition(*conditions, newCondition.Type) + if existingCondition == nil { + newCondition.LastTransitionTime = metav1.NewTime(time.Now()) + *conditions = append(*conditions, newCondition) + return + } + + if existingCondition.Status != newCondition.Status { + existingCondition.Status = newCondition.Status + existingCondition.LastTransitionTime = metav1.NewTime(time.Now()) + } + + existingCondition.Reason = newCondition.Reason + existingCondition.Message = newCondition.Message +} + +// RemoveStatusCondition removes the corresponding conditionType from conditions. +func RemoveStatusCondition(conditions *[]Condition, conditionType ConditionType) { + if conditions == nil { + return + } + newConditions := []Condition{} + for _, condition := range *conditions { + if condition.Type != conditionType { + newConditions = append(newConditions, condition) + } + } + + *conditions = newConditions +} + +// FindStatusCondition finds the conditionType in conditions. +func FindStatusCondition(conditions []Condition, conditionType ConditionType) *Condition { + for i := range conditions { + if conditions[i].Type == conditionType { + return &conditions[i] + } + } + + return nil +} + +// IsStatusConditionTrue returns true when the conditionType is present and set to `corev1.ConditionTrue` +func IsStatusConditionTrue(conditions []Condition, conditionType ConditionType) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, corev1.ConditionTrue) +} + +// IsStatusConditionFalse returns true when the conditionType is present and set to `corev1.ConditionFalse` +func IsStatusConditionFalse(conditions []Condition, conditionType ConditionType) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, corev1.ConditionFalse) +} + +// IsStatusConditionUnknown returns true when the conditionType is present and set to `corev1.ConditionUnknown` +func IsStatusConditionUnknown(conditions []Condition, conditionType ConditionType) bool { + return IsStatusConditionPresentAndEqual(conditions, conditionType, corev1.ConditionUnknown) +} + +// IsStatusConditionPresentAndEqual returns true when conditionType is present and equal to status. +func IsStatusConditionPresentAndEqual(conditions []Condition, conditionType ConditionType, status corev1.ConditionStatus) bool { + for _, condition := range conditions { + if condition.Type == conditionType { + return condition.Status == status + } + } + return false +} diff --git a/vendor/github.com/openshift/custom-resource-status/conditions/v1/doc.go b/vendor/github.com/openshift/custom-resource-status/conditions/v1/doc.go new file mode 100644 index 000000000..b657efeaa --- /dev/null +++ b/vendor/github.com/openshift/custom-resource-status/conditions/v1/doc.go @@ -0,0 +1,9 @@ +// +k8s:deepcopy-gen=package,register +// +k8s:defaulter-gen=TypeMeta +// +k8s:openapi-gen=true + +// Package v1 provides version v1 of the types and functions necessary to +// manage and inspect a slice of conditions. It is opinionated in the +// condition types provided but leaves it to the user to define additional +// types as necessary. +package v1 diff --git a/vendor/github.com/openshift/custom-resource-status/conditions/v1/types.go b/vendor/github.com/openshift/custom-resource-status/conditions/v1/types.go new file mode 100644 index 000000000..950678fb9 --- /dev/null +++ b/vendor/github.com/openshift/custom-resource-status/conditions/v1/types.go @@ -0,0 +1,51 @@ +package v1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Condition represents the state of the operator's +// reconciliation functionality. +// +k8s:deepcopy-gen=true +type Condition struct { + Type ConditionType `json:"type" description:"type of condition ie. Available|Progressing|Degraded."` + + Status corev1.ConditionStatus `json:"status" description:"status of the condition, one of True, False, Unknown"` + + // +optional + Reason string `json:"reason,omitempty" description:"one-word CamelCase reason for the condition's last transition"` + + // +optional + Message string `json:"message,omitempty" description:"human-readable message indicating details about last transition"` + + // +optional + LastHeartbeatTime metav1.Time `json:"lastHeartbeatTime" description:"last time we got an update on a given condition"` + + // +optional + LastTransitionTime metav1.Time `json:"lastTransitionTime" description:"last time the condition transit from one status to another"` +} + +// ConditionType is the state of the operator's reconciliation functionality. +type ConditionType string + +const ( + // ConditionAvailable indicates that the resources maintained by the operator, + // is functional and available in the cluster. + ConditionAvailable ConditionType = "Available" + + // ConditionProgressing indicates that the operator is actively making changes to the resources maintained by the + // operator + ConditionProgressing ConditionType = "Progressing" + + // ConditionDegraded indicates that the resources maintained by the operator are not functioning completely. + // An example of a degraded state would be if not all pods in a deployment were running. + // It may still be available, but it is degraded + ConditionDegraded ConditionType = "Degraded" + + // ConditionUpgradeable indicates whether the resources maintained by the operator are in a state that is safe to upgrade. + // When `False`, the resources maintained by the operator should not be upgraded and the + // message field should contain a human readable description of what the administrator should do to + // allow the operator to successfully update the resources maintained by the operator. + ConditionUpgradeable ConditionType = "Upgradeable" +) diff --git a/vendor/github.com/openshift/custom-resource-status/conditions/v1/zz_generated.deepcopy.go b/vendor/github.com/openshift/custom-resource-status/conditions/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..bbbbf863d --- /dev/null +++ b/vendor/github.com/openshift/custom-resource-status/conditions/v1/zz_generated.deepcopy.go @@ -0,0 +1,23 @@ +// +build !ignore_autogenerated + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastHeartbeatTime.DeepCopyInto(&out.LastHeartbeatTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 237818934..e010eb774 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -265,6 +265,9 @@ github.com/openshift/api/security/v1 ## explicit; go 1.16 github.com/openshift/client-go/security/clientset/versioned/scheme github.com/openshift/client-go/security/clientset/versioned/typed/security/v1 +# github.com/openshift/custom-resource-status v1.1.2 +## explicit; go 1.12 +github.com/openshift/custom-resource-status/conditions/v1 # github.com/operator-framework/api v0.14.0 ## explicit; go 1.17 github.com/operator-framework/api/pkg/lib/version