MaxScale webhook validation

This commit is contained in:
Martin Montes
2024-01-22 15:53:11 +01:00
parent 8cf28ffb56
commit 7aa8bc4722
4 changed files with 495 additions and 5 deletions

View File

@ -578,8 +578,8 @@ var _ = Describe("MariaDB webhook", func() {
BeforeAll(func() {
mariadb := MariaDB{
ObjectMeta: metav1.ObjectMeta{
Name: "mariadb-update-webhook",
Namespace: testNamespace,
Name: key.Name,
Namespace: key.Namespace,
},
Spec: MariaDBSpec{
Image: "mariadb:11.3.3",

View File

@ -2,6 +2,7 @@ package v1alpha1
import (
"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"
@ -23,7 +24,7 @@ var _ webhook.Validator = &MaxScale{}
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *MaxScale) ValidateCreate() (admission.Warnings, error) {
maxscaleLogger.V(1).Info("Validate create", "name", r.Name)
return nil, nil
return nil, r.validate()
}
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
@ -33,10 +34,84 @@ func (r *MaxScale) ValidateUpdate(old runtime.Object) (admission.Warnings, error
if err := inmutableWebhook.ValidateUpdate(r, oldMaxScale); err != nil {
return nil, err
}
return nil, nil
return nil, r.validate()
}
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *MaxScale) ValidateDelete() (admission.Warnings, error) {
return nil, nil
}
func (r *MaxScale) validate() error {
validateFns := []func() error{
r.validateServers,
r.validateServices,
r.validatePodDisruptionBudget,
}
for _, fn := range validateFns {
if err := fn(); err != nil {
return err
}
}
return nil
}
func (r *MaxScale) validateServers() error {
idx := r.ServerIndex()
if len(idx) != len(r.Spec.Servers) {
return field.Invalid(
field.NewPath("spec").Child("servers"),
r.Spec.Servers,
"server names must be unique",
)
}
addresses := make(map[string]struct{})
for _, srv := range r.Spec.Servers {
addresses[srv.Address] = struct{}{}
}
if len(addresses) != len(r.Spec.Servers) {
return field.Invalid(
field.NewPath("spec").Child("servers"),
r.Spec.Servers,
"server addresses must be unique",
)
}
return nil
}
func (r *MaxScale) validateServices() error {
idx := r.ServiceIndex()
if len(idx) != len(r.Spec.Services) {
return field.Invalid(
field.NewPath("spec").Child("services"),
r.Spec.Services,
"service names must be unique",
)
}
ports := make(map[int]struct{})
for _, svc := range r.Spec.Services {
ports[int(svc.Listener.Port)] = struct{}{}
}
if len(ports) != len(r.Spec.Services) {
return field.Invalid(
field.NewPath("spec").Child("services"),
r.Spec.Services,
"service listener ports must be unique",
)
}
return nil
}
func (r *MaxScale) validatePodDisruptionBudget() error {
if r.Spec.PodDisruptionBudget == nil {
return nil
}
if err := r.Spec.PodDisruptionBudget.Validate(); err != nil {
return field.Invalid(
field.NewPath("spec").Child("podDisruptionBudget"),
r.Spec.PodDisruptionBudget,
err.Error(),
)
}
return nil
}

View File

@ -0,0 +1,415 @@
package v1alpha1
import (
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var _ = Describe("MaxScale webhook", func() {
Context("When creating a MaxScale", func() {
meta := metav1.ObjectMeta{
Name: "maxscale-create-webhook",
Namespace: testNamespace,
}
DescribeTable(
"Should validate",
func(mxs *MaxScale, wantErr bool) {
_ = k8sClient.Delete(testCtx, mxs)
err := k8sClient.Create(testCtx, mxs)
if wantErr {
Expect(err).To(HaveOccurred())
} else {
Expect(err).ToNot(HaveOccurred())
}
},
Entry(
"Invalid server names",
&MaxScale{
ObjectMeta: meta,
Spec: MaxScaleSpec{
Servers: []MaxScaleServer{
{
Name: "mariadb-0",
Address: "mariadb-repl-0.mariadb-repl-internal.default.svc.cluster.local",
},
{
Name: "mariadb-0",
Address: "mariadb-repl-1.mariadb-repl-internal.default.svc.cluster.local",
},
},
Services: []MaxScaleService{
{
Name: "rw-router",
Router: ServiceRouterReadWriteSplit,
Listener: MaxScaleListener{
Port: 3306,
},
},
},
Monitor: MaxScaleMonitor{
Module: MonitorModuleMariadb,
},
},
},
true,
),
Entry(
"Invalid server addresses",
&MaxScale{
ObjectMeta: meta,
Spec: MaxScaleSpec{
Servers: []MaxScaleServer{
{
Name: "mariadb-0",
Address: "mariadb-repl-0.mariadb-repl-internal.default.svc.cluster.local",
},
{
Name: "mariadb-1",
Address: "mariadb-repl-0.mariadb-repl-internal.default.svc.cluster.local",
},
},
Services: []MaxScaleService{
{
Name: "rw-router",
Router: ServiceRouterReadWriteSplit,
Listener: MaxScaleListener{
Port: 3306,
},
},
},
Monitor: MaxScaleMonitor{
Module: MonitorModuleMariadb,
},
},
},
true,
),
Entry(
"Invalid service names",
&MaxScale{
ObjectMeta: meta,
Spec: MaxScaleSpec{
Servers: []MaxScaleServer{
{
Name: "mariadb-0",
Address: "mariadb-repl-0.mariadb-repl-internal.default.svc.cluster.local",
},
},
Services: []MaxScaleService{
{
Name: "rw-router",
Router: ServiceRouterReadWriteSplit,
Listener: MaxScaleListener{
Port: 3306,
},
},
{
Name: "rw-router",
Router: ServiceRouterReadConnRoute,
Listener: MaxScaleListener{
Port: 3307,
},
},
},
Monitor: MaxScaleMonitor{
Module: MonitorModuleMariadb,
},
},
},
true,
),
Entry(
"Invalid service ports",
&MaxScale{
ObjectMeta: meta,
Spec: MaxScaleSpec{
Servers: []MaxScaleServer{
{
Name: "mariadb-0",
Address: "mariadb-repl-0.mariadb-repl-internal.default.svc.cluster.local",
},
},
Services: []MaxScaleService{
{
Name: "rw-router",
Router: ServiceRouterReadWriteSplit,
Listener: MaxScaleListener{
Port: 3306,
},
},
{
Name: "conn-router",
Router: ServiceRouterReadConnRoute,
Listener: MaxScaleListener{
Port: 3306,
},
},
},
Monitor: MaxScaleMonitor{
Module: MonitorModuleMariadb,
},
},
},
true,
),
Entry(
"Invalid PodDisruptionBudget",
&MaxScale{
ObjectMeta: meta,
Spec: MaxScaleSpec{
Servers: []MaxScaleServer{
{
Name: "mariadb-0",
Address: "mariadb-repl-0.mariadb-repl-internal.default.svc.cluster.local",
},
},
Services: []MaxScaleService{
{
Name: "rw-router",
Router: ServiceRouterReadWriteSplit,
Listener: MaxScaleListener{
Port: 3306,
},
},
},
Monitor: MaxScaleMonitor{
Module: MonitorModuleMariadb,
},
PodDisruptionBudget: &PodDisruptionBudget{
MaxUnavailable: func() *intstr.IntOrString { i := intstr.FromString("50%"); return &i }(),
MinAvailable: func() *intstr.IntOrString { i := intstr.FromString("50%"); return &i }(),
},
},
},
true,
),
Entry(
"Valid",
&MaxScale{
ObjectMeta: meta,
Spec: MaxScaleSpec{
Servers: []MaxScaleServer{
{
Name: "mariadb-0",
Address: "mariadb-repl-0.mariadb-repl-internal.default.svc.cluster.local",
},
},
Services: []MaxScaleService{
{
Name: "rw-router",
Router: ServiceRouterReadWriteSplit,
Listener: MaxScaleListener{
Port: 3306,
},
},
},
Monitor: MaxScaleMonitor{
Module: MonitorModuleMariadb,
},
PodDisruptionBudget: &PodDisruptionBudget{
MaxUnavailable: func() *intstr.IntOrString { i := intstr.FromString("50%"); return &i }(),
},
},
},
false,
),
)
})
Context("When updating a MaxScale", Ordered, func() {
key := types.NamespacedName{
Name: "maxscale-update-webhook",
Namespace: testNamespace,
}
BeforeAll(func() {
mxs := MaxScale{
ObjectMeta: metav1.ObjectMeta{
Name: key.Name,
Namespace: key.Namespace,
},
Spec: MaxScaleSpec{
Servers: []MaxScaleServer{
{
Name: "mariadb-0",
Address: "mariadb-repl-0.mariadb-repl-internal.default.svc.cluster.local",
},
{
Name: "mariadb-1",
Address: "mariadb-repl-1.mariadb-repl-internal.default.svc.cluster.local",
},
{
Name: "mariadb-2",
Address: "mariadb-repl-2.mariadb-repl-internal.default.svc.cluster.local",
},
},
Services: []MaxScaleService{
{
Name: "rw-router",
Router: ServiceRouterReadWriteSplit,
Listener: MaxScaleListener{
Port: 3306,
},
},
{
Name: "rconn-master-router",
Router: ServiceRouterReadConnRoute,
Listener: MaxScaleListener{
Port: 3307,
Params: map[string]string{
"router_options": "master",
},
},
},
{
Name: "rconn-slave-router",
Router: ServiceRouterReadConnRoute,
Listener: MaxScaleListener{
Port: 3308,
Params: map[string]string{
"router_options": "slave",
},
},
},
},
Monitor: MaxScaleMonitor{
Module: MonitorModuleMariadb,
},
Admin: MaxScaleAdmin{
Port: 8989,
},
KubernetesService: &ServiceTemplate{
Type: corev1.ServiceTypeLoadBalancer,
Annotations: map[string]string{
"metallb.universe.tf/loadBalancerIPs": "172.18.0.214",
},
},
},
}
Expect(k8sClient.Create(testCtx, &mxs)).To(Succeed())
})
DescribeTable(
"Should validate",
func(patchFn func(mxs *MaxScale), wantErr bool) {
var mxs MaxScale
Expect(k8sClient.Get(testCtx, key, &mxs)).To(Succeed())
patch := client.MergeFrom(mxs.DeepCopy())
patchFn(&mxs)
err := k8sClient.Patch(testCtx, &mxs, patch)
if wantErr {
Expect(err).To(HaveOccurred())
} else {
Expect(err).ToNot(HaveOccurred())
}
},
Entry(
"Updating Image",
func(mxs *MaxScale) {
mxs.Spec.Image = "mariadb/maxscale:23.07"
},
false,
),
Entry(
"Adding Server",
func(mxs *MaxScale) {
mxs.Spec.Servers = append(mxs.Spec.Servers, MaxScaleServer{
Name: "mariadb-3",
Address: "mariadb-repl-3.mariadb-repl-internal.default.svc.cluster.local",
})
},
false,
),
Entry(
"Updating Server",
func(mxs *MaxScale) {
mxs.Spec.Servers[0].Name = "mariadb-0-test"
},
false,
),
Entry(
"Adding Service",
func(mxs *MaxScale) {
mxs.Spec.Services = append(mxs.Spec.Services, MaxScaleService{
Name: "rconn-router",
Router: ServiceRouterReadConnRoute,
Listener: MaxScaleListener{
Port: 3309,
}},
)
},
false,
),
Entry(
"Updating Service",
func(mxs *MaxScale) {
mxs.Spec.Services[0].Listener.Port = 1111
},
true,
),
Entry(
"Updating Monitor interval",
func(mxs *MaxScale) {
mxs.Spec.Monitor.Interval = metav1.Duration{Duration: 1 * time.Second}
},
false,
),
Entry(
"Updating Monitor module",
func(mxs *MaxScale) {
mxs.Spec.Monitor.Module = MonitorModuleGalera
},
true,
),
Entry(
"Updating Admin",
func(mxs *MaxScale) {
mxs.Spec.Admin.Port = 9090
},
true,
),
Entry(
"Updating Config",
func(mxs *MaxScale) {
mxs.Spec.Config.Params = map[string]string{
"foo": "bar",
}
},
true,
),
Entry(
"Updating Auth",
func(mxs *MaxScale) {
mxs.Spec.Auth.AdminUsername = "foo"
},
true,
),
Entry(
"Updating Replicas",
func(mxs *MaxScale) {
mxs.Spec.Replicas = 3
},
false,
),
Entry(
"Updating Resources",
func(mxs *MaxScale) {
mxs.Spec.Resources = &corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"cpu": resource.MustParse("200m"),
},
}
},
false,
),
)
})
})

View File

@ -26,7 +26,7 @@ release: goreleaser ## Test release locally.
##@ Run
WATCH_NAMESPACE ?= ""
RUN_FLAGS ?= --log-dev --log-maxscale --log-level=info --log-time-encoder=iso8601
RUN_FLAGS ?= --log-dev --log-level=info --log-time-encoder=iso8601
RUN_ENV ?= RELATED_IMAGE_MARIADB=$(RELATED_IMAGE_MARIADB) RELATED_IMAGE_MAXSCALE=$(RELATED_IMAGE_MAXSCALE) RELATED_IMAGE_EXPORTER=$(RELATED_IMAGE_EXPORTER) MARIADB_OPERATOR_IMAGE=$(IMG) \
MARIADB_OPERATOR_NAME=$(MARIADB_OPERATOR_NAME) MARIADB_OPERATOR_NAMESPACE=$(MARIADB_OPERATOR_NAMESPACE) MARIADB_OPERATOR_SA_PATH=$(MARIADB_OPERATOR_SA_PATH) \