diff --git a/images/webhooks/src/main.go b/images/webhooks/src/main.go index ff616a2b..943d9e1f 100644 --- a/images/webhooks/src/main.go +++ b/images/webhooks/src/main.go @@ -23,9 +23,11 @@ import ( kwhhttp "github.com/slok/kubewebhook/v2/pkg/http" kwhlogrus "github.com/slok/kubewebhook/v2/pkg/log/logrus" kwhmutating "github.com/slok/kubewebhook/v2/pkg/webhook/mutating" + kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating" corev1 "k8s.io/api/core/v1" "net/http" "os" + "webhooks/v1alpha1" "webhooks/validators" ) @@ -61,29 +63,51 @@ func main() { cfg := initFlags() - mt := kwhmutating.MutatorFunc(validators.PodSchedulerMutation) + pscmMF := kwhmutating.MutatorFunc(validators.PodSchedulerMutation) - mcfg := kwhmutating.WebhookConfig{ + pscmMCfg := kwhmutating.WebhookConfig{ ID: "PodSchedulerMutation", Obj: &corev1.Pod{}, - Mutator: mt, + Mutator: pscmMF, Logger: logger, } - wh, err := kwhmutating.NewWebhook(mcfg) + pscmWh, err := kwhmutating.NewWebhook(pscmMCfg) if err != nil { fmt.Fprintf(os.Stderr, "error creating webhook: %s", err) os.Exit(1) } // Get the handler for our webhook. - whHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: wh, Logger: logger}) + pscmWhHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: pscmWh, Logger: logger}) + if err != nil { + fmt.Fprintf(os.Stderr, "error creating webhook handler: %s", err) + os.Exit(1) + } + + scuMF := kwhvalidating.ValidatorFunc(validators.StorageClassUpdate) + + scuMCfg := kwhvalidating.WebhookConfig{ + ID: "StorageClassUpdate", + Obj: &v1alpha1.LocalStorageClass{}, + Validator: scuMF, + Logger: logger, + } + scuWh, err := kwhvalidating.NewWebhook(scuMCfg) + if err != nil { + fmt.Fprintf(os.Stderr, "error creating webhook: %s", err) + os.Exit(1) + } + + // Get the handler for our webhook. + scuWhHandler, err := kwhhttp.HandlerFor(kwhhttp.HandlerConfig{Webhook: scuWh, Logger: logger}) if err != nil { fmt.Fprintf(os.Stderr, "error creating webhook handler: %s", err) os.Exit(1) } mux := http.NewServeMux() - mux.Handle("/pod-scheduler-mutation", whHandler) + mux.Handle("/pod-scheduler-mutation", pscmWhHandler) + mux.Handle("/storage-class-update", scuWhHandler) mux.HandleFunc("/healthz", httpHandlerHealthz) logger.Infof("Listening on %s", port) diff --git a/images/webhooks/src/v1alpha1/local_storage_class.go b/images/webhooks/src/v1alpha1/local_storage_class.go new file mode 100644 index 00000000..18bbee34 --- /dev/null +++ b/images/webhooks/src/v1alpha1/local_storage_class.go @@ -0,0 +1,59 @@ +/* +Copyright 2024 Flant JSC + +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 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type LocalStorageClass struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec LocalStorageClassSpec `json:"spec"` + Status *LocalStorageClassStatus `json:"status,omitempty"` +} + +// LocalStorageClassList contains a list of empty block device +type LocalStorageClassList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + Items []LocalStorageClass `json:"items"` +} + +type LocalStorageClassSpec struct { + IsDefault bool `json:"isDefault"` + ReclaimPolicy string `json:"reclaimPolicy"` + VolumeBindingMode string `json:"volumeBindingMode"` + LVM *LocalStorageClassLVM `json:"lvm,omitempty"` +} + +type LocalStorageClassLVM struct { + Type string `json:"type"` + LVMVolumeGroups []LocalStorageClassLVG `json:"lvmVolumeGroups"` +} + +type LocalStorageClassStatus struct { + Phase string `json:"phase,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type LocalStorageClassLVG struct { + Name string `json:"name"` + Thin *LocalStorageClassThinPool `json:"thin,omitempty"` +} + +type LocalStorageClassThinPool struct { + PoolName string `json:"poolName"` +} diff --git a/images/webhooks/src/v1alpha1/lvm_volume_group.go b/images/webhooks/src/v1alpha1/lvm_volume_group.go new file mode 100644 index 00000000..52123b9d --- /dev/null +++ b/images/webhooks/src/v1alpha1/lvm_volume_group.go @@ -0,0 +1,75 @@ +/* +Copyright 2024 Flant JSC +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 ( + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type LvmVolumeGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + Items []LvmVolumeGroup `json:"items"` +} + +type LvmVolumeGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec LvmVolumeGroupSpec `json:"spec"` + Status LvmVolumeGroupStatus `json:"status,omitempty"` +} + +type SpecThinPool struct { + Name string `json:"name"` + Size resource.Quantity `json:"size"` +} + +type LvmVolumeGroupSpec struct { + ActualVGNameOnTheNode string `json:"actualVGNameOnTheNode"` + BlockDeviceNames []string `json:"blockDeviceNames"` + ThinPools []SpecThinPool `json:"thinPools"` + Type string `json:"type"` +} + +type LvmVolumeGroupDevice struct { + BlockDevice string `json:"blockDevice"` + DevSize resource.Quantity `json:"devSize"` + PVSize string `json:"pvSize"` + PVUuid string `json:"pvUUID"` + Path string `json:"path"` +} + +type LvmVolumeGroupNode struct { + Devices []LvmVolumeGroupDevice `json:"devices"` + Name string `json:"name"` +} + +type StatusThinPool struct { + Name string `json:"name"` + ActualSize resource.Quantity `json:"actualSize"` + UsedSize string `json:"usedSize"` +} + +type LvmVolumeGroupStatus struct { + AllocatedSize string `json:"allocatedSize"` + Health string `json:"health"` + Message string `json:"message"` + Nodes []LvmVolumeGroupNode `json:"nodes"` + ThinPools []StatusThinPool `json:"thinPools"` + VGSize string `json:"vgSize"` + VGUuid string `json:"vgUUID"` +} diff --git a/images/webhooks/src/v1alpha1/register.go b/images/webhooks/src/v1alpha1/register.go new file mode 100644 index 00000000..e7a5d0dc --- /dev/null +++ b/images/webhooks/src/v1alpha1/register.go @@ -0,0 +1,51 @@ +/* +Copyright 2024 Flant JSC + +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 ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + LocalStorageClassKind = "LocalStorageClass" + APIGroup = "storage.deckhouse.io" + APIVersion = "v1alpha1" +) + +// SchemeGroupVersion is group version used to register these objects +var ( + SchemeGroupVersion = schema.GroupVersion{ + Group: APIGroup, + Version: APIVersion, + } + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &LocalStorageClass{}, + &LocalStorageClassList{}, + &LvmVolumeGroup{}, + &LvmVolumeGroupList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil +} diff --git a/images/webhooks/src/v1alpha1/zz_generated.deepcopy.go b/images/webhooks/src/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..abf5c426 --- /dev/null +++ b/images/webhooks/src/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,135 @@ +/* +Copyright 2024 Flant JSC + +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 "k8s.io/apimachinery/pkg/runtime" + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalStorageClass) DeepCopyInto(out *LocalStorageClass) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmptyBlockDevice. +func (in *LocalStorageClass) DeepCopy() *LocalStorageClass { + if in == nil { + return nil + } + out := new(LocalStorageClass) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LocalStorageClass) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalStorageClassList) DeepCopyInto(out *LocalStorageClassList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LocalStorageClass, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuestbookList. +func (in *LocalStorageClassList) DeepCopy() *LocalStorageClassList { + if in == nil { + return nil + } + out := new(LocalStorageClassList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LocalStorageClassList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LvmVolumeGroup) DeepCopyInto(out *LvmVolumeGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmptyBlockDevice. +func (in *LvmVolumeGroup) DeepCopy() *LvmVolumeGroup { + if in == nil { + return nil + } + out := new(LvmVolumeGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LvmVolumeGroup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LvmVolumeGroupList) DeepCopyInto(out *LvmVolumeGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]LvmVolumeGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GuestbookList. +func (in *LvmVolumeGroupList) DeepCopy() *LvmVolumeGroupList { + if in == nil { + return nil + } + out := new(LvmVolumeGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LvmVolumeGroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} diff --git a/images/webhooks/src/validators/storageClassUpdate.go b/images/webhooks/src/validators/storageClassUpdate.go new file mode 100644 index 00000000..cdf2a88d --- /dev/null +++ b/images/webhooks/src/validators/storageClassUpdate.go @@ -0,0 +1,62 @@ +/* +Copyright 2024 Flant JSC + +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 validators + +import ( + "context" + "webhooks/v1alpha1" + + "github.com/slok/kubewebhook/v2/pkg/model" + kwhvalidating "github.com/slok/kubewebhook/v2/pkg/webhook/validating" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func StorageClassUpdate(ctx context.Context, _ *model.AdmissionReview, obj metav1.Object) (*kwhvalidating.ValidatorResult, error) { + sc, ok := obj.(*v1alpha1.LocalStorageClass) + if !ok { + // If not a storage class just continue the validation chain(if there is one) and do nothing. + return &kwhvalidating.ValidatorResult{}, nil + } + + thickExists := false + thinExists := false + for _, lvmGroup := range sc.Spec.LVM.LVMVolumeGroups { + if lvmGroup.Thin == nil { + thickExists = true + } else { + thinExists = true + } + } + + if thinExists && sc.Spec.LVM.Type == "Thick" { + return &kwhvalidating.ValidatorResult{Valid: false, Message: "there must be only thick pools with Thick LVM type"}, + nil + } + + if thickExists && sc.Spec.LVM.Type == "Thin" { + return &kwhvalidating.ValidatorResult{Valid: false, Message: "there must be only thin pools with Thin LVM type"}, + nil + } + + if thickExists == true && thinExists == true { + return &kwhvalidating.ValidatorResult{Valid: false, Message: "there must be only thin or thick pools simultaneously"}, + nil + } else { + return &kwhvalidating.ValidatorResult{Valid: true}, + nil + } +} diff --git a/templates/webhooks/webhook.yaml b/templates/webhooks/webhook.yaml index 4738dbed..17e7e1d9 100644 --- a/templates/webhooks/webhook.yaml +++ b/templates/webhooks/webhook.yaml @@ -29,3 +29,29 @@ webhooks: admissionReviewVersions: ["v1", "v1beta1"] sideEffects: None timeoutSeconds: 5 + +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: "d8-{{ .Chart.Name }}-storage-class-update" +webhooks: + - name: "d8-{{ .Chart.Name }}-storage-class-update.deckhouse.io" + failurePolicy: Fail + rules: + - apiGroups: ["storage.deckhouse.io"] + apiVersions: ["v1alpha1"] + operations: ["CREATE", "UPDATE"] + resources: ["localstorageclasses"] + scope: "Cluster" + clientConfig: + service: + namespace: "d8-{{ .Chart.Name }}" + name: "webhooks" + path: "/storage-class-update" + caBundle: | + {{ .Values.sdsLocalVolume.internal.customWebhookCert.ca }} + + admissionReviewVersions: ["v1", "v1beta1"] + sideEffects: None + timeoutSeconds: 5