Skip to content

Commit

Permalink
Merge pull request #72 from rabbitmq/binding-webhook
Browse files Browse the repository at this point in the history
bindings.rabbitmq.com webhook to prevent updates on binding.spec
  • Loading branch information
ChunyiLyu committed Mar 18, 2021
2 parents ee547a3 + 03fb332 commit 199249f
Show file tree
Hide file tree
Showing 19 changed files with 266 additions and 21 deletions.
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
export PATH="$PATH:$GOPATH/bin"
make install-tools
kind create cluster --image kindest/node:"$K8S_VERSION"
make cert-manager
make cluster-operator
DOCKER_REGISTRY_SERVER=local-server OPERATOR_IMAGE=local-operator make deploy-kind
make system-tests
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,9 @@ generate-manifests:
mkdir -p releases
kustomize build config/installation/ > releases/messaging-topology-operator.yaml

CERT_MANAGER_VERSION ?=v1.2.0
cert-manager:
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/$(CERT_MANAGER_VERSION)/cert-manager.yaml

destroy-cert-manager:
kubectl delete -f https://github.com/jetstack/cert-manager/releases/download/$(CERT_MANAGER_VERSION)/cert-manager.yaml
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Before deploying Messaging Topology Operator, you need to have:
1. A Running k8s cluster
2. RabbitMQ [Cluster Operator](https://github.com/rabbitmq/cluster-operator) installed in the k8s cluster
3. A [RabbitMQ cluster](https://github.com/rabbitmq/cluster-operator/tree/main/docs/examples) deployed using the Cluster Operator
4. (Optional) [cert-manager](https://cert-manager.io/docs/installation/kubernetes/) `1.2.0` or above, installed in the k8s cluster

If you have `kubectl` configured to access your running k8s cluster, you can then run the following command to install the Messaging Topology Operator:

Expand All @@ -27,6 +28,10 @@ You can create RabbitMQ resources:
5. [Vhost](./docs/examples/vhosts)
6. [Policy](./docs/examples/policies)

## Install without cert-manager

If you do not have cert-manager in your k8s cluster, you need to generate certificates used by admission webhooks yourself and include them in the operator deployment, crds, and webhooks manifests.

## Contributing

This project follows the typical GitHub pull request model. Before starting any work, please either comment on an [existing issue](https://github.com/rabbitmq/messaging-topology-operator/issues), or file a new one.
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/binding_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// BindingSpec defines the desired state of Binding
Expand Down Expand Up @@ -66,6 +67,13 @@ type BindingList struct {
Items []Binding `json:"items"`
}

func (b *Binding) GroupResource() schema.GroupResource {
return schema.GroupResource{
Group: b.GroupVersionKind().Group,
Resource: b.GroupVersionKind().Kind,
}
}

func init() {
SchemeBuilder.Register(&Binding{}, &BindingList{})
}
49 changes: 49 additions & 0 deletions api/v1alpha1/binding_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package v1alpha1

import (
"fmt"
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"
)

var logger = logf.Log.WithName("binding-webhook")

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

// +kubebuilder:webhook:verbs=create;update,path=/validate-rabbitmq-com-v1alpha1-binding,mutating=false,failurePolicy=fail,groups=rabbitmq.com,resources=bindings,versions=v1alpha1,name=vbinding.kb.io,sideEffects=none,admissionReviewVersions=v1

var _ webhook.Validator = &Binding{}

// no validation logic on create
func (b *Binding) ValidateCreate() error {
return nil
}

// updates on bindings.rabbitmq.com is forbidden
func (b *Binding) ValidateUpdate(old runtime.Object) error {
oldBinding, ok := old.(*Binding)
if !ok {
return apierrors.NewBadRequest(fmt.Sprintf("expected a binding but got a %T", old))
}

if oldBinding.Spec != b.Spec {
return apierrors.NewForbidden(
b.GroupResource(),
b.Name,
field.Forbidden(field.NewPath("spec"), "binding.spec is immutable"))
}
return nil
}

// no validation logic on delete
func (b *Binding) ValidateDelete() error {
return nil
}
73 changes: 73 additions & 0 deletions api/v1alpha1/binding_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package v1alpha1

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

var _ = Describe("Binding webhook", func() {

var oldBinding = Binding{
ObjectMeta: metav1.ObjectMeta{
Name: "update-binding",
},
Spec: BindingSpec{
Vhost: "/test",
Source: "test",
Destination: "test",
DestinationType: "queue",
RabbitmqClusterReference: RabbitmqClusterReference{
Name: "some-cluster",
Namespace: "default",
},
},
}

It("does not allow updates on vhost", func() {
newBinding := oldBinding.DeepCopy()
newBinding.Spec.Vhost = "/new-vhost"
Expect(apierrors.IsForbidden(newBinding.ValidateUpdate(&oldBinding))).To(BeTrue())
})

It("does not allow updates on source", func() {
newBinding := oldBinding.DeepCopy()
newBinding.Spec.Source = "updated-source"
Expect(apierrors.IsForbidden(newBinding.ValidateUpdate(&oldBinding))).To(BeTrue())
})

It("does not allow updates on destination", func() {
newBinding := oldBinding.DeepCopy()
newBinding.Spec.Destination = "updated-des"
Expect(apierrors.IsForbidden(newBinding.ValidateUpdate(&oldBinding))).To(BeTrue())
})

It("does not allow updates on destination type", func() {
newBinding := oldBinding.DeepCopy()
newBinding.Spec.DestinationType = "exchange"
Expect(apierrors.IsForbidden(newBinding.ValidateUpdate(&oldBinding))).To(BeTrue())
})

It("does not allow updates on routing key", func() {
newBinding := oldBinding.DeepCopy()
newBinding.Spec.RoutingKey = "not-allowed"
Expect(apierrors.IsForbidden(newBinding.ValidateUpdate(&oldBinding))).To(BeTrue())
})

It("does not allow updates on binding arguments", func() {
newBinding := oldBinding.DeepCopy()
newBinding.Spec.Arguments = &runtime.RawExtension{Raw: []byte(`{"new":"new-value"}`)}
Expect(apierrors.IsForbidden(newBinding.ValidateUpdate(&oldBinding))).To(BeTrue())
})

It("does not allow updates on RabbitmqClusterReference", func() {
newBinding := oldBinding.DeepCopy()
newBinding.Spec.RabbitmqClusterReference = RabbitmqClusterReference{
Name: "new-cluster",
Namespace: "default",
}
Expect(apierrors.IsForbidden(newBinding.ValidateUpdate(&oldBinding))).To(BeTrue())
})
})
8 changes: 2 additions & 6 deletions config/certmanager/certificate.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
# The following manifests contain a self-signed issuer CR and a certificate CR.
# More document can be found at https://docs.cert-manager.io
# WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for
# breaking changes
apiVersion: cert-manager.io/v1alpha2
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: selfsigned-issuer
namespace: system
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1alpha2
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml
Expand Down
7 changes: 7 additions & 0 deletions config/crd/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,12 @@ resources:
- bases/rabbitmq.com_policies.yaml
# +kubebuilder:scaffold:crdkustomizeresource

patchesStrategicMerge:
- patches/webhook_in_bindings.yaml
# +kubebuilder:scaffold:crdkustomizewebhookpatch

- patches/cainjection_in_bindings.yaml
# +kubebuilder:scaffold:crdkustomizecainjectionpatch

configurations:
- kustomizeconfig.yaml
2 changes: 1 addition & 1 deletion config/crd/patches/cainjection_in_bindings.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# The following patch adds a directive for certmanager to inject CA into the CRD
# CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1alpha1
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
Expand Down
18 changes: 9 additions & 9 deletions config/crd/patches/webhook_in_bindings.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# The following patch enables conversion webhook for CRD
# CRD conversion requires k8s 1.13 or later.
apiVersion: apiextensions.k8s.io/v1alpha1
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: bindings.rabbitmq.com
spec:
conversion:
strategy: Webhook
webhookClientConfig:
# this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
# but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
caBundle: Cg==
service:
namespace: system
name: webhook-service
path: /convert
webhook:
conversionReviewVersions: ["v1", "v1beta1"]
clientConfig:
caBundle: Cg==
service:
namespace: system
name: webhook-service
path: /convert
34 changes: 34 additions & 0 deletions config/default/base/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ namespace: rabbitmq-system
resources:
- ../../crd
- ../../manager
- ../../webhook
- ../../certmanager

patches:
- manager_webhook_patch.yaml
- webhookcainjection_patch.yaml

vars:
- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
fieldref:
fieldpath: metadata.namespace
- name: CERTIFICATE_NAME
objref:
kind: Certificate
group: cert-manager.io
version: v1
name: serving-cert # this name should match the one in certificate.yaml
- name: SERVICE_NAMESPACE # namespace of the service
objref:
kind: Service
version: v1
name: webhook-service
fieldref:
fieldpath: metadata.namespace
- name: SERVICE_NAME
objref:
kind: Service
version: v1
name: webhook-service

images:
- name: controller
Expand Down
23 changes: 23 additions & 0 deletions config/default/base/manager_webhook_patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: operator
namespace: system
spec:
template:
spec:
containers:
- name: manager
ports:
- containerPort: 9443
name: webhook-server
protocol: TCP
volumeMounts:
- mountPath: /tmp/k8s-webhook-server/serving-certs
name: cert
readOnly: true
volumes:
- name: cert
secret:
defaultMode: 420
secretName: webhook-server-cert
7 changes: 7 additions & 0 deletions config/default/base/webhookcainjection_patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook-configuration
annotations:
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
2 changes: 0 additions & 2 deletions config/webhook/kustomizeconfig.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# the following config is for teaching kustomize where to look at when substituting vars.
# It requires kustomize v2.1.0 or newer to work properly.
nameReference:
- kind: Service
version: v1
Expand Down
28 changes: 28 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
creationTimestamp: null
name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-rabbitmq-com-v1alpha1-binding
failurePolicy: Fail
name: vbinding.kb.io
rules:
- apiGroups:
- rabbitmq.com
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- bindings
sideEffects: None
3 changes: 1 addition & 2 deletions config/webhook/service.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

apiVersion: v1
kind: Service
metadata:
Expand All @@ -9,4 +8,4 @@ spec:
- port: 443
targetPort: 9443
selector:
control-plane: controller-manager
app.kubernetes.io/name: messaging-topology-operator
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

rabbitmqv1beta1 "github.com/rabbitmq/cluster-operator/api/v1beta1"

rabbitmqcomv1alpha1 "github.com/rabbitmq/messaging-topology-operator/api/v1alpha1"
topologyv1alpha1 "github.com/rabbitmq/messaging-topology-operator/api/v1alpha1"
"github.com/rabbitmq/messaging-topology-operator/controllers"
// +kubebuilder:scaffold:imports
Expand Down Expand Up @@ -118,6 +119,10 @@ func main() {
log.Error(err, "unable to create controller", "controller", policyControllerName)
os.Exit(1)
}
if err = (&rabbitmqcomv1alpha1.Binding{}).SetupWebhookWithManager(mgr); err != nil {
log.Error(err, "unable to create webhook", "webhook", "Binding")
os.Exit(1)
}
// +kubebuilder:scaffold:builder

log.Info("starting manager")
Expand Down
6 changes: 6 additions & 0 deletions system_tests/binding_system_tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,11 @@ var _ = Describe("Binding", func() {

By("setting status.observedGeneration")
Expect(updatedBinding.Status.ObservedGeneration).To(Equal(updatedBinding.GetGeneration()))

By("not allowing updates on binding.spec")
updateBinding := topologyv1alpha1.Binding{}
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: binding.Name, Namespace: binding.Namespace}, &updateBinding)).To(Succeed())
updatedBinding.Spec.RoutingKey = "new-key"
Expect(k8sClient.Update(ctx, &updatedBinding).Error()).To(ContainSubstring("spec: Forbidden: binding.spec is immutable"))
})
})

0 comments on commit 199249f

Please sign in to comment.