ceph: implement controller-runtime for admission controllers#5761
ceph: implement controller-runtime for admission controllers#5761travisn merged 1 commit intorook:masterfrom
Conversation
pkg/daemon/admission/server.go
Outdated
| tlsCertFile = "tls.crt" | ||
| tlsKeyFile = "tls.key" | ||
| serverPort int32 = 8079 | ||
| port = 8079 |
There was a problem hiding this comment.
What is the difference between serverPort and port? Can't we just re-use serverPort?
There was a problem hiding this comment.
Removed ! Moved the dir and port to cmd/ceph/admission.go
| logger.Infof("starting the webhook for backend %q", a.providerName) | ||
| certPath := filepath.Join(tlsDir, tlsCertFile) | ||
| keyPath := filepath.Join(tlsDir, tlsKeyFile) | ||
| keyPair, err := NewTlsKeypairReloader(certPath, keyPath) |
There was a problem hiding this comment.
Are tlsutil.go and controller.go used anymore? Can we remove them? If so, then perhaps this package is so simple we can delete the whole package and move the remaining code under the ceph provider. For example, similar to what was done for the nfs operator in pkg/operator/nfs/webhook.go.
There was a problem hiding this comment.
This is a draft PR. There are still some issues i am working on. I'll let you know once the updates are done
There was a problem hiding this comment.
I've removed the tlsutil.go and controller.go
There was a problem hiding this comment.
@travisn the code from the package (server.go) is meant to be a generic server code that can work for all providers. I think it would make sense to move it under pkg/operator/admission/server.go. May add another file later in this package for the unit tests. WDYT ?
There was a problem hiding this comment.
The generic code is so short now that I wonder if it makes sense to have any common code. The controller runtime packages already have the common code.
There was a problem hiding this comment.
Ok, i've moved the server code under pkg/operator/ceph/server.go
425b073 to
0e05481
Compare
| logger.Infof("starting the webhook for backend %q", a.providerName) | ||
| certPath := filepath.Join(tlsDir, tlsCertFile) | ||
| keyPath := filepath.Join(tlsDir, tlsKeyFile) | ||
| keyPair, err := NewTlsKeypairReloader(certPath, keyPath) |
There was a problem hiding this comment.
The generic code is so short now that I wonder if it makes sense to have any common code. The controller runtime packages already have the common code.
cmd/rook/ceph/admission.go
Outdated
| if err != nil { | ||
| logger.Errorf("failed to add to scheme %v. ", err) | ||
| } | ||
| a := admission.New("ceph", port, certDir, &cephv1.CephCluster{}, scheme) |
There was a problem hiding this comment.
If we want to implement the admission webhook for multiple CRDs, we would need to start them all here (in separate goroutines, right? I guess they would just each listen on a separate port.
There was a problem hiding this comment.
Though i havent tested it yet but i think that may not be the right approach as adding new ports means adding more services + validatingwebhookconfigs+ adding each port on the deployment for each of the CRDs to expose the port.
There was a problem hiding this comment.
I have updated the code to add CephBlockPool validation. I've tried to make the code as generic and easy to change to add more CRDs. Here are the steps i did for cephblockpool which can be used for other ceph CRDs.
- Implement
webhook.Validatorinterface inpkg/apis/ceph.rook.io/v1/webhook.gofor the CRD which needs to be validated - Add a
ValidatingWebhookConfiguration(copy-paste work) inwebhook-config.yamlchanging the relevant fields ( name, path, resources) and make sure to note down thepathgiven here. - In
cmd/ceph/admission.go, Add the abovepathand map it to the given CRD inpathResourceMap. - Done ! Test it out.
There was a problem hiding this comment.
Similiar approach can be followed for other providers also.
76dc7b3 to
7010343
Compare
cmd/rook/ceph/admission.go
Outdated
| } | ||
| scheme = runtime.NewScheme() | ||
| pathResourceMap = map[string]webhook.Validator{ | ||
| "/validate-cephclusters": &cephv1.CephCluster{}, |
There was a problem hiding this comment.
Let's define this pathResourceMap inside the pkg/operator/ceph/server.go file. I don't see a reason to have this in the cmd package.
There was a problem hiding this comment.
Yes, i'll move it here as it is part of ceph package. We may need to move it out once other providers implement their validations.
cmd/rook/ceph/admission.go
Outdated
| a := admission.New(context, "ceph", operator.ValidateCephResource) | ||
| err := cephv1.AddToScheme(scheme) | ||
| if err != nil { | ||
| logger.Errorf("failed to add to scheme %v. ", err) |
There was a problem hiding this comment.
If there is a failure we need to exit since the admission controller would fail to start later anyway. See other places where we call
rook.TerminateFatal(err)
pkg/apis/ceph.rook.io/v1/webhook.go
Outdated
| @@ -0,0 +1,89 @@ | |||
| package v1 | |||
There was a problem hiding this comment.
How about separate source files for different CRDs? For example, validate_cluster.go and validate_pool.go
pkg/apis/ceph.rook.io/v1/webhook.go
Outdated
| // will be registered for the validating webhook. | ||
| var _ webhook.Validator = &CephCluster{} | ||
|
|
||
| func (in *CephCluster) ValidateCreate() error { |
There was a problem hiding this comment.
Instead of in, it's more common to use the first letter of the type.
| func (in *CephCluster) ValidateCreate() error { | |
| func (c *CephCluster) ValidateCreate() error { |
pkg/apis/ceph.rook.io/v1/webhook.go
Outdated
|
|
||
| var _ webhook.Validator = &CephBlockPool{} | ||
|
|
||
| func (in *CephBlockPool) ValidateCreate() error { |
There was a problem hiding this comment.
p for pool
| func (in *CephBlockPool) ValidateCreate() error { | |
| func (p *CephBlockPool) ValidateCreate() error { |
pkg/apis/ceph.rook.io/v1/webhook.go
Outdated
| if in.Spec.Replicated.Size > 0 || in.Spec.Replicated.TargetSizeRatio > 0 { | ||
| return errors.New("invalid create: both erasurecoded and replicated fields cannot be set at the same time") | ||
| } | ||
| } |
There was a problem hiding this comment.
A couple other checks:
- If either codingChunks or dataChunks are set, then dataChunks must be at least 2 and codingChunks must be at least 1.
- If all of these values are 0, it's an error. You must set either replicated or erasure coded settings.
pkg/operator/ceph/server.go
Outdated
| } | ||
|
|
||
| // StartServer will start the server | ||
| func (a *AdmissionController) StartServer() { |
There was a problem hiding this comment.
StartServer() should return an err so that the cmd package can call TerminateFatal() if there is some issue starting the webhook.
| func (a *AdmissionController) StartServer() { | |
| func (a *AdmissionController) StartServer() error { |
pkg/operator/ceph/server.go
Outdated
| } | ||
| mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), opts) | ||
| if err != nil { | ||
| logger.Errorf("failed to create manager %v. ", err) |
pkg/operator/ceph/server.go
Outdated
| logger.Info("starting webhook server") | ||
| err = mgr.Start(ctrl.SetupSignalHandler()) | ||
| if err != nil { | ||
| logger.Errorf("failed to start server %v. ", err) |
| - apiGroups: ["ceph.rook.io"] | ||
| apiVersions: ["v1"] | ||
| operations: ["CREATE","UPDATE","DELETE"] | ||
| resources: ["cephblockpools"] |
There was a problem hiding this comment.
Defining a separate ValidatingWebhookConfiguration for every CRD is really painful. Can you point to other examples of where this is done in other projects? I would really like to confirm if we can have one definition and watch all CRDs in the apiGroup ceph.rook.io
There was a problem hiding this comment.
I've updated the code. We can now make do with just one ValidatingWebhookConfiguration but we have to add a webhook in the array for each CRD and make sure that the path is written with the same template
There was a problem hiding this comment.
Looks good since it is in the same webhook config and we can use a single endpoint.
42fd480 to
6beab17
Compare
| - apiGroups: ["ceph.rook.io"] | ||
| apiVersions: ["v1"] | ||
| operations: ["CREATE","UPDATE","DELETE"] | ||
| resources: ["cephblockpools"] |
There was a problem hiding this comment.
Looks good since it is in the same webhook config and we can use a single endpoint.
cmd/rook/ceph/admission.go
Outdated
| if err != nil { | ||
| rook.TerminateFatal(err) | ||
| } | ||
| a := operator.NewAdmissionController("ceph", port, certDir, scheme) |
There was a problem hiding this comment.
Do we even need to pass any of these parameters? What if we just define them directly inside StartServer()? It looks like they aren't configurable, so we don't need to expose them here.
| func (a *AdmissionController) StartServer() error { | ||
| logger.Infof("starting the webhook for backend %q", a.providerName) | ||
|
|
||
| opts := ctrl.Options{ |
There was a problem hiding this comment.
For example, related to my other comment, can we just initialize the scheme, port, and certDir here?
There was a problem hiding this comment.
I've moved the inits to server.go
| ) | ||
|
|
||
| type AdmissionController struct { | ||
| providerName string |
There was a problem hiding this comment.
This code is in the ceph namespace now, so no need to make it generic.
6beab17 to
94930b3
Compare
cmd/rook/ceph/admission.go
Outdated
|
|
||
| a := admission.New(context, "ceph", operator.ValidateCephResource) | ||
| a.StartServer() | ||
| err := operator.StartServer() |
There was a problem hiding this comment.
Since we don't have an admission controller struct anymore, let's rename the method.
| err := operator.StartServer() | |
| err := operator.StartAdmissionController() |
94930b3 to
2d8856f
Compare
|
|
||
| func ValidatePoolSpecs(ps PoolSpec) error { | ||
| // Checks if either ErasureCoded or Replicated fields are set | ||
| if ps.ErasureCoded.CodingChunks <= 0 && ps.ErasureCoded.DataChunks <= 0 && ps.Replicated.TargetSizeRatio <= 0 && ps.Replicated.Size <= 0 { |
There was a problem hiding this comment.
We really should have unit tests for all of these validations. It should be fairly simple to create them, so would be great to have them with this PR.
There was a problem hiding this comment.
Updated with unit tests. please review
b2b8ade to
38f106a
Compare
| err := yaml.Unmarshal(cephClusterYaml, c) | ||
| assert.Nil(t, err) | ||
| err = c.ValidateCreate() | ||
| assert.Nil(t, err) |
There was a problem hiding this comment.
Instead of assert.Nil(), it's preferred to use assert.NoError()
| assert.Nil(t, err) | |
| assert.NoError(t, err) |
| c.Spec.External.Enable = true | ||
| c.Spec.Monitoring = MonitoringSpec{} | ||
| err = c.ValidateCreate() | ||
| assert.NotNil(t, err) |
There was a problem hiding this comment.
| assert.NotNil(t, err) | |
| assert.Error(t, err) |
| # key: value | ||
| `) | ||
| } | ||
| func getCephClusterCRD() []byte { |
There was a problem hiding this comment.
Instead of creating yaml and unmarshalling it in the test, let's just use the golang types directly. For example, you should be able to directly declare any of the structs in types.go, change the properties, and then validate them.
38f106a to
3c74f26
Compare
| } | ||
| } | ||
| func getCephClusterCRD() *CephCluster { | ||
| return &CephCluster{ |
There was a problem hiding this comment.
Much better instead of yaml, thanks. One more point is that there is no need for the test to set all the fields. Just set what is needed for an individual test instead of all the optional fields. With that approach, you likely don't even need a helper to create the struct and you can just create a small struct in each individual test.
There was a problem hiding this comment.
Well i was thinking about not adding any duplicates in code but I've removed the helper funcs.
3c74f26 to
e88dfe4
Compare
| c := &CephCluster{ | ||
| ObjectMeta: metav1.ObjectMeta{ | ||
| Name: "rook-ceph", | ||
| Namespace: "rook-ceph", |
There was a problem hiding this comment.
You likely don't even need to set the Namespace for the test.
There was a problem hiding this comment.
removed. I've just kept the name and datadirhostpath
| assert.NoError(t, err) | ||
| c.Spec.External.Enable = true | ||
| c.Spec.Monitoring = MonitoringSpec{ | ||
| Enabled: false, |
There was a problem hiding this comment.
false is the default, no need to set it
There was a problem hiding this comment.
Oops, it was supposed to be true. I've fixed it
| Namespace: "rook-ceph", | ||
| }, | ||
| Spec: PoolSpec{ | ||
| FailureDomain: "osd", |
There was a problem hiding this comment.
Validation doesn't do anything with the FailureDomain, right? No need to set it for the test.
e88dfe4 to
8e2c88d
Compare
moves the admission-controller server to use controller-runtime library for better support, maintainibility of code. Signed-off-by: Vineet Badrinath <vbadrina@redhat.com>
8e2c88d to
98874cc
Compare
travisn
left a comment
There was a problem hiding this comment.
The test changes look good thanks!
moves the admission-controller server to use controller-runtime library for better support, maintainibility of code.
Signed-off-by: Vineet Badrinath vbadrina@redhat.com
Description of your changes:
This is a follow-up PR for #5045
It moves the server logic to use controller-runtime library for webhooks.
This help in better support, maintenance and performance of the code.