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

Dissallow volume updates #243

Merged
merged 6 commits into from
Dec 14, 2021
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
3 changes: 3 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ resources:
kind: Volume
path: github.com/onmetal/onmetal-api/apis/storage/v1alpha1
version: v1alpha1
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
Expand Down
7 changes: 7 additions & 0 deletions apis/storage/v1alpha1/volume_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// VolumeGK is a helper to easily access the GroupKind information of an Volume
var VolumeGK = schema.GroupKind{
Group: GroupVersion.Group,
Kind: "Volume",
}

// VolumeSpec defines the desired state of Volume
type VolumeSpec struct {
// StorageClass is the storage class of a volume
Expand Down
82 changes: 82 additions & 0 deletions apis/storage/v1alpha1/volume_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
adracus marked this conversation as resolved.
Show resolved Hide resolved
* Copyright (c) 2021 by the OnMetal authors.
*
* 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 (
"reflect"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

const (
fieldImmutable = "field is immutable"
)

// log is for logging in this package.
var volumelog = logf.Log.WithName("volume-resource")

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

//+kubebuilder:webhook:path=/validate-storage-onmetal-de-v1alpha1-volume,mutating=false,failurePolicy=fail,sideEffects=None,groups=storage.onmetal.de,resources=volumes,verbs=create;update,versions=v1alpha1,name=vvolume.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &Volume{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Volume) ValidateCreate() error {
volumelog.Info("validate create", "name", r.Name)
return nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *Volume) ValidateUpdate(old runtime.Object) error {
volumelog.Info("validate update", "name", r.Name)
oldRange := old.(*Volume)
path := field.NewPath("spec")

var allErrs field.ErrorList
if !reflect.DeepEqual(r.Spec.StorageClass, oldRange.Spec.StorageClass) {
allErrs = append(allErrs, field.Invalid(path.Child("storageClass"), r.Spec.StorageClass, fieldImmutable))
}

if oldRange.Spec.StoragePool.Name != "" && !reflect.DeepEqual(r.Spec.StoragePool, oldRange.Spec.StoragePool) {
allErrs = append(allErrs, field.Invalid(path.Child("storagePool"), r.Spec.StoragePool, fieldImmutable))
}

if !reflect.DeepEqual(r.Spec.StoragePoolSelector, oldRange.Spec.StoragePoolSelector) {
allErrs = append(allErrs, field.Invalid(path.Child("storagePoolSelector"), r.Spec.StoragePoolSelector, fieldImmutable))
}

if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(VolumeGK, r.Name, allErrs)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *Volume) ValidateDelete() error {
volumelog.Info("validate delete", "name", r.Name)
return nil
}
86 changes: 86 additions & 0 deletions apis/storage/v1alpha1/volume_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2021 by the OnMetal authors.
*
* 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 (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var _ = Describe("volume validation webhook", func() {
ns := SetupTest()
Context("upon volume update", func() {
It("signals error if storageclass is changed", func() {
By("creating a storage pool")
storagePool := &StoragePool{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "test-pool-",
},
}
Expect(k8sClient.Create(ctx, storagePool)).To(Succeed(), "failed to create storage pool")

By("patching the storage pool status to contain a storage class")
storagePoolBase := storagePool.DeepCopy()
storagePool.Status.AvailableStorageClasses = []corev1.LocalObjectReference{{Name: "my-volumeclass"}}
Expect(k8sClient.Status().Patch(ctx, storagePool, client.MergeFrom(storagePoolBase))).
To(Succeed(), "failed to patch storage pool status")

By("creating a volume w/ the requested storage class")
volume := &Volume{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
GenerateName: "test-volume-",
},
Spec: VolumeSpec{
StorageClass: corev1.LocalObjectReference{
Name: "my-volumeclass",
},
},
}
Expect(k8sClient.Create(ctx, volume)).To(Succeed(), "failed to create volume")
newStorageClass := v1.LocalObjectReference{Name: "newclass"}
volume.Spec.StorageClass = newStorageClass
err := k8sClient.Update(ctx, volume)
Expect(err).To(HaveOccurred())
path := field.NewPath("spec")
fieldErr := field.Invalid(path.Child("storageClass"), newStorageClass, "field is immutable")
fieldErrList := field.ErrorList{fieldErr}
Expect(err.Error()).To(ContainSubstring(fieldErrList.ToAggregate().Error()))
})

It("keeps silent when update storagepool once by scheduler", func() {
volume := &Volume{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns.Name,
GenerateName: "volume-",
},
}
Expect(k8sClient.Create(ctx, volume)).To(Succeed())

newStoragePoolName := "new_storagepool"
volume.Spec.StoragePool.Name = newStoragePoolName
err := k8sClient.Update(ctx, volume)
Expect(err).NotTo(HaveOccurred())
})

})
})
158 changes: 158 additions & 0 deletions apis/storage/v1alpha1/webhook_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* Copyright (c) 2021 by the OnMetal authors.
*
* 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 (
"context"
"crypto/tls"
"fmt"
"net"
"path/filepath"
"testing"
"time"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
admissionv1beta1 "k8s.io/api/admission/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
)

var (
cfg *rest.Config
ctx = ctrl.SetupSignalHandler()
k8sClient client.Client
webhookScheme *runtime.Scheme
testEnv *envtest.Environment
)

var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: false,
WebhookInstallOptions: envtest.WebhookInstallOptions{
Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")},
},
}

var err error
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())

webhookScheme = runtime.NewScheme()
err = AddToScheme(webhookScheme)
Expect(err).NotTo(HaveOccurred())

err = admissionv1beta1.AddToScheme(webhookScheme)
Expect(err).NotTo(HaveOccurred())

err = corev1.AddToScheme(webhookScheme)
Expect(err).NotTo(HaveOccurred())

//+kubebuilder:scaffold:scheme

k8sClient, err = client.New(cfg, client.Options{Scheme: webhookScheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())

}, 60)

var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})

func SetupTest() *corev1.Namespace {
var (
cancel context.CancelFunc
ctxForSetup context.Context
)

ns := &corev1.Namespace{}

BeforeEach(func() {
// start webhook server using Manager
ctxForSetup, cancel = context.WithCancel(ctx)
webhookInstallOptions := &testEnv.WebhookInstallOptions
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: webhookScheme,
Host: webhookInstallOptions.LocalServingHost,
Port: webhookInstallOptions.LocalServingPort,
CertDir: webhookInstallOptions.LocalServingCertDir,
LeaderElection: false,
MetricsBindAddress: "0",
})
Expect(err).NotTo(HaveOccurred())

err = (&Volume{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

//+kubebuilder:scaffold:webhook

go func() {
defer GinkgoRecover()

err = mgr.Start(ctxForSetup)
if err != nil {
Expect(err).NotTo(HaveOccurred())
}
}()

// wait for the webhook server to get ready
dialer := &net.Dialer{Timeout: time.Second}
addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
Eventually(func() error {
conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
if err != nil {
return err
}
conn.Close()
return nil
}).Should(Succeed())

*ns = corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{GenerateName: "ns-"},
}
Expect(k8sClient.Create(ctxForSetup, ns)).NotTo(HaveOccurred(), "failed to create test namespace")
})

AfterEach(func() {
Expect(k8sClient.Delete(ctxForSetup, ns)).NotTo(HaveOccurred(), "failed to delete test namespace")

defer cancel()
})

return ns
}

func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)

RunSpecs(t, "Webhook Suite")
}
2 changes: 1 addition & 1 deletion apis/storage/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,24 @@ webhooks:
resources:
- ipamranges
sideEffects: None
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-storage-onmetal-de-v1alpha1-volume
failurePolicy: Fail
name: vvolume.kb.io
rules:
- apiGroups:
- storage.onmetal.de
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- volumes
sideEffects: None
Loading