Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create validation webhook for lvmcluster #255

Merged
merged 5 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified
deploy: update-mgr-env manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} && $(KUSTOMIZE) edit set nameprefix ${MANAGER_NAME_PREFIX}
cd config/default && $(KUSTOMIZE) edit set image rbac-proxy=$(RBAC_PROXY_IMG)
cd config/webhook && $(KUSTOMIZE) edit set nameprefix ${MANAGER_NAME_PREFIX}
$(KUSTOMIZE) build config/default | kubectl apply -f -

undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config.
Expand Down Expand Up @@ -232,6 +233,7 @@ bundle: update-mgr-env manifests kustomize operator-sdk rename-csv build-prometh
rm -rf bundle
# $(OPERATOR_SDK) generate kustomize manifests --package $(BUNDLE_PACKAGE) -q
cd config/default && $(KUSTOMIZE) edit set namespace $(OPERATOR_NAMESPACE)
cd config/webhook && $(KUSTOMIZE) edit set nameprefix ${MANAGER_NAME_PREFIX}
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} && $(KUSTOMIZE) edit set nameprefix ${MANAGER_NAME_PREFIX}
cd config/default && $(KUSTOMIZE) edit set image rbac-proxy=$(RBAC_PROXY_IMG)
$(KUSTOMIZE) build config/manifests | $(OPERATOR_SDK) generate bundle -q --package $(BUNDLE_PACKAGE) --version $(VERSION) $(BUNDLE_METADATA_OPTS)
Expand Down
3 changes: 3 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ resources:
kind: LVMCluster
path: github.com/red-hat-storage/lvm-operator/api/v1alpha1
version: v1alpha1
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
Expand Down
266 changes: 266 additions & 0 deletions api/v1alpha1/lvmcluster_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
/*
Copyright 2022 Red Hat Openshift Data Foundation.

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 v1alpha1

import (
"fmt"
"strings"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

// log is for logging in this package.
var lvmclusterlog = logf.Log.WithName("lvmcluster-webhook")

var _ webhook.Validator = &LVMCluster{}

var (
ErrDeviceClassNotFound = fmt.Errorf("DeviceClass not found in the LVMCluster")
ErrThinPoolConfigNotSet = fmt.Errorf("ThinPoolConfig is not set for the DeviceClass")
)

//+kubebuilder:webhook:path=/validate-lvm-topolvm-io-v1alpha1-lvmcluster,mutating=false,failurePolicy=fail,sideEffects=None,groups=lvm.topolvm.io,resources=lvmclusters,verbs=create;update,versions=v1alpha1,name=vlvmcluster.kb.io,admissionReviewVersions=v1

func (l *LVMCluster) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(l).
Complete()
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (l *LVMCluster) ValidateCreate() error {
lvmclusterlog.Info("validate create", "name", l.Name)

if len(l.Spec.Storage.DeviceClasses) != 1 {
return fmt.Errorf("Exactly one deviceClass is allowed")
}

err := l.verifyPathsAreNotEmpty()
if err != nil {
return err
}

err = l.verifyAbsolutePath()
if err != nil {
return err
}

err = l.verifyNoDeviceOverlap()
if err != nil {
return err
}

return nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (l *LVMCluster) ValidateUpdate(old runtime.Object) error {
lvmclusterlog.Info("validate update", "name", l.Name)

if len(l.Spec.Storage.DeviceClasses) != 1 {
return fmt.Errorf("Exactly one deviceClass is allowed")
}

err := l.verifyPathsAreNotEmpty()
if err != nil {
return err
}

err = l.verifyAbsolutePath()
if err != nil {
return err
}

err = l.verifyNoDeviceOverlap()
if err != nil {
return err
}

oldLVMCluster, ok := old.(*LVMCluster)
if !ok {
return fmt.Errorf("Failed to parse LVMCluster.")
}

for _, deviceClass := range l.Spec.Storage.DeviceClasses {
var newThinPoolConfig, oldThinPoolConfig *ThinPoolConfig
var newDevices, oldDevices []string

newThinPoolConfig = deviceClass.ThinPoolConfig
oldThinPoolConfig, err = oldLVMCluster.getThinPoolsConfigOfDeviceClass(deviceClass.Name)

if (newThinPoolConfig != nil && oldThinPoolConfig == nil && err != ErrDeviceClassNotFound) ||
(newThinPoolConfig == nil && oldThinPoolConfig != nil) {
return fmt.Errorf("ThinPoolConfig can not be changed")
}

if newThinPoolConfig != nil && oldThinPoolConfig != nil {
if newThinPoolConfig.Name != oldThinPoolConfig.Name {
return fmt.Errorf("ThinPoolConfig.Name can not be changed")
} else if newThinPoolConfig.SizePercent != oldThinPoolConfig.SizePercent {
return fmt.Errorf("ThinPoolConfig.SizePercent can not be changed")
} else if newThinPoolConfig.OverprovisionRatio != oldThinPoolConfig.OverprovisionRatio {
return fmt.Errorf("ThinPoolConfig.OverprovisionRatio can not be changed")
}
}

if deviceClass.DeviceSelector != nil {
newDevices = deviceClass.DeviceSelector.Paths
}

oldDevices, err = oldLVMCluster.getPathsOfDeviceClass(deviceClass.Name)

// if devices are removed now
if len(oldDevices) > len(newDevices) {
return fmt.Errorf("Invalid:devices can not be removed from the LVMCluster once added.")
}

// if devices are added now
if len(oldDevices) == 0 && len(newDevices) > 0 && err != ErrDeviceClassNotFound {
return fmt.Errorf("Invalid:devices can not be added in the LVMCluster once created without devices.")
}

deviceMap := make(map[string]bool)

for _, device := range oldDevices {
deviceMap[device] = true
}

for _, device := range newDevices {
delete(deviceMap, device)
}

// if any old device is removed now
if len(deviceMap) != 0 {
return fmt.Errorf("Invalid:some of devices are deleted from the LVMCluster. "+
"Device can not be removed from the LVMCluster once added. "+
"oldDevices:%s, newDevices:%s", oldDevices, newDevices)
}
}

return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (l *LVMCluster) ValidateDelete() error {
lvmclusterlog.Info("validate delete", "name", l.Name)

return nil
}

func (l *LVMCluster) verifyPathsAreNotEmpty() error {

for _, deviceClass := range l.Spec.Storage.DeviceClasses {
if deviceClass.DeviceSelector != nil {
if len(deviceClass.DeviceSelector.Paths) == 0 {
return fmt.Errorf("Path list should not be empty when DeviceSelector is specified.")
}
}
}

return nil
}

func (l *LVMCluster) verifyAbsolutePath() error {

for _, deviceClass := range l.Spec.Storage.DeviceClasses {
if deviceClass.DeviceSelector != nil {
for _, path := range deviceClass.DeviceSelector.Paths {
if !strings.HasPrefix(path, "/dev/") {
return fmt.Errorf("Given path %s is not an absolute path. "+
"Please provide the absolute path to the device", path)
}
}
}
}

return nil
}

func (l *LVMCluster) verifyNoDeviceOverlap() 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 l.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)
Copy link
Contributor

@nbalacha nbalacha Sep 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this is no required seeing that we are now restricting it to one deviceClass.

} else {
err = fmt.Errorf("Error: device path %s is specified at multiple places in deviceClass %s", path, val)
}
return err
}

if devices[nodeSelector] == nil {
devices[nodeSelector] = make(map[string]string)
}

devices[nodeSelector][path] = deviceClass.Name
}
}
}

return nil
}

func (l *LVMCluster) getPathsOfDeviceClass(deviceClassName string) ([]string, error) {

for _, deviceClass := range l.Spec.Storage.DeviceClasses {
if deviceClass.Name == deviceClassName {
if deviceClass.DeviceSelector != nil {
return deviceClass.DeviceSelector.Paths, nil
}
return []string{}, nil
}
}

return []string{}, ErrDeviceClassNotFound
iamniting marked this conversation as resolved.
Show resolved Hide resolved
}

func (l *LVMCluster) getThinPoolsConfigOfDeviceClass(deviceClassName string) (*ThinPoolConfig, error) {

for _, deviceClass := range l.Spec.Storage.DeviceClasses {
if deviceClass.Name == deviceClassName {
if deviceClass.ThinPoolConfig != nil {
return deviceClass.ThinPoolConfig, nil
}
return nil, ErrThinPoolConfigNotSet
}
}

return nil, ErrDeviceClassNotFound
}
Loading