Skip to content

Commit 9f90cd0

Browse files
committed
chore: Add logic and test for validation webhook
Signed-off-by: Teddy Andrieux <teddy.andrieux@scality.com>
1 parent b62d5fe commit 9f90cd0

File tree

4 files changed

+192
-19
lines changed

4 files changed

+192
-19
lines changed

config/manager/kustomization.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
resources:
22
- manager.yaml
3+
apiVersion: kustomize.config.k8s.io/v1beta1
4+
kind: Kustomization
5+
images:
6+
- name: controller
7+
newName: example.com/crl-operator
8+
newTag: v0.0.1

internal/webhook/v1alpha1/managedcrl_webhook.go

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@ import (
2020
"context"
2121
"fmt"
2222

23+
cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
2324
"k8s.io/apimachinery/pkg/runtime"
2425
ctrl "sigs.k8s.io/controller-runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
2527
logf "sigs.k8s.io/controller-runtime/pkg/log"
2628
"sigs.k8s.io/controller-runtime/pkg/webhook"
2729
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
2830

31+
"github.com/go-logr/logr"
2932
crloperatorv1alpha1 "github.com/scality/crl-operator/api/v1alpha1"
3033
)
3134

@@ -36,7 +39,9 @@ var managedcrllog = logf.Log.WithName("managedcrl-resource")
3639
// SetupManagedCRLWebhookWithManager registers the webhook for ManagedCRL in the manager.
3740
func SetupManagedCRLWebhookWithManager(mgr ctrl.Manager) error {
3841
return ctrl.NewWebhookManagedBy(mgr).For(&crloperatorv1alpha1.ManagedCRL{}).
39-
WithValidator(&ManagedCRLCustomValidator{}).
42+
WithValidator(&ManagedCRLCustomValidator{
43+
client: mgr.GetClient(),
44+
}).
4045
Complete()
4146
}
4247

@@ -53,35 +58,34 @@ func SetupManagedCRLWebhookWithManager(mgr ctrl.Manager) error {
5358
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
5459
// as this struct is used only for temporary operations and does not need to be deeply copied.
5560
type ManagedCRLCustomValidator struct {
56-
// TODO(user): Add more fields as needed for validation
61+
client client.Client
5762
}
5863

5964
var _ webhook.CustomValidator = &ManagedCRLCustomValidator{}
6065

6166
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type ManagedCRL.
62-
func (v *ManagedCRLCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
67+
func (v *ManagedCRLCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
6368
managedcrl, ok := obj.(*crloperatorv1alpha1.ManagedCRL)
6469
if !ok {
6570
return nil, fmt.Errorf("expected a ManagedCRL object but got %T", obj)
6671
}
67-
managedcrllog.Info("Validation for ManagedCRL upon creation", "name", managedcrl.GetName())
72+
logger := managedcrllog.WithValues("name", managedcrl.GetName()).WithValues("namespace", managedcrl.GetNamespace())
6873

69-
// TODO(user): fill in your validation logic upon object creation.
74+
logger.Info("Validation for ManagedCRL upon creation")
7075

71-
return nil, nil
76+
return nil, validationManagedCRL(logger, ctx, v.client, managedcrl)
7277
}
7378

7479
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type ManagedCRL.
75-
func (v *ManagedCRLCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
80+
func (v *ManagedCRLCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
7681
managedcrl, ok := newObj.(*crloperatorv1alpha1.ManagedCRL)
7782
if !ok {
7883
return nil, fmt.Errorf("expected a ManagedCRL object for the newObj but got %T", newObj)
7984
}
80-
managedcrllog.Info("Validation for ManagedCRL upon update", "name", managedcrl.GetName())
81-
82-
// TODO(user): fill in your validation logic upon object update.
85+
logger := managedcrllog.WithValues("name", managedcrl.GetName()).WithValues("namespace", managedcrl.GetNamespace())
86+
logger.Info("Validation for ManagedCRL upon update")
8387

84-
return nil, nil
88+
return nil, validationManagedCRL(logger, ctx, v.client, managedcrl)
8589
}
8690

8791
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type ManagedCRL.
@@ -96,3 +100,49 @@ func (v *ManagedCRLCustomValidator) ValidateDelete(ctx context.Context, obj runt
96100

97101
return nil, nil
98102
}
103+
104+
// validationManagedCRL validates the ManagedCRL fields.
105+
func validationManagedCRL(logger logr.Logger, ctx context.Context, c client.Client, managedcrl *crloperatorv1alpha1.ManagedCRL) error {
106+
managedcrl.WithDefaults()
107+
if err := managedcrl.Validate(); err != nil {
108+
logger.Error(err, "Validation failed")
109+
return err
110+
}
111+
112+
// Ensure the specified Issuer or ClusterIssuer exists
113+
issuerLogger := logger.WithValues("issuer_name", managedcrl.Spec.IssuerRef.Name, "issuer_kind", managedcrl.Spec.IssuerRef.Kind)
114+
issuerRef := managedcrl.Spec.IssuerRef
115+
switch issuerRef.Kind {
116+
case "Issuer":
117+
var issuer cmv1.Issuer
118+
err := c.Get(ctx, client.ObjectKey{Namespace: managedcrl.Namespace, Name: issuerRef.Name}, &issuer)
119+
if err != nil {
120+
issuerLogger.Error(err, "Issuer not found")
121+
return fmt.Errorf("issuer %s not found in namespace %s", issuerRef.Name, managedcrl.Namespace)
122+
}
123+
if issuer.Spec.CA == nil || issuer.Spec.CA.SecretName == "" {
124+
err := fmt.Errorf("issuer %s in namespace %s is not a CA issuer", issuerRef.Name, managedcrl.Namespace)
125+
issuerLogger.Error(err, "Issuer is not a CA issuer")
126+
return err
127+
}
128+
case "ClusterIssuer":
129+
var issuer cmv1.ClusterIssuer
130+
err := c.Get(ctx, client.ObjectKey{Name: issuerRef.Name}, &issuer)
131+
if err != nil {
132+
issuerLogger.Error(err, "ClusterIssuer not found")
133+
return fmt.Errorf("clusterissuer %s not found", issuerRef.Name)
134+
}
135+
if issuer.Spec.CA == nil || issuer.Spec.CA.SecretName == "" {
136+
err := fmt.Errorf("clusterissuer %s is not a CA issuer", issuerRef.Name)
137+
issuerLogger.Error(err, "Issuer is not a CA issuer")
138+
return err
139+
}
140+
default:
141+
err := fmt.Errorf("invalid IssuerRef kind: %s", issuerRef.Kind)
142+
issuerLogger.Error(err, "IssuerRef kind must be either 'Issuer' or 'ClusterIssuer'")
143+
return err
144+
}
145+
146+
logger.Info("Validation successful")
147+
return nil
148+
}

test/integration/managedcrl_controller_test.go

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"crypto/x509"
2222
"fmt"
2323
"math/big"
24+
"strings"
2425
"time"
2526

2627
. "github.com/onsi/ginkgo/v2"
@@ -32,6 +33,7 @@ import (
3233
"sigs.k8s.io/controller-runtime/pkg/client"
3334

3435
cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
36+
cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
3537
corev1 "k8s.io/api/core/v1"
3638
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3739

@@ -42,6 +44,7 @@ type mcrlTestCase struct {
4244
name string
4345
spec crloperatorv1alpha1.ManagedCRLSpec
4446
shouldError bool
47+
errorMessage string
4548
shouldExposePod bool
4649
shouldExposeIngress bool
4750
shouldConfigureIssuer bool
@@ -177,6 +180,61 @@ var (
177180
shouldExposePod: true,
178181
shouldExposeIngress: true,
179182
shouldConfigureIssuer: true,
183+
}, {
184+
name: "error-invalid-issuer-kind",
185+
spec: crloperatorv1alpha1.ManagedCRLSpec{
186+
IssuerRef: cmmetav1.IssuerReference{
187+
Kind: "InvalidKind",
188+
},
189+
},
190+
shouldError: true,
191+
errorMessage: "issuerRef kind must be either 'Issuer' or 'ClusterIssuer', got 'InvalidKind'",
192+
}, {
193+
name: "error-non-existent-issuer",
194+
spec: crloperatorv1alpha1.ManagedCRLSpec{
195+
IssuerRef: cmmetav1.IssuerReference{
196+
Name: "non-existent-issuer",
197+
},
198+
},
199+
shouldError: true,
200+
errorMessage: "issuer non-existent-issuer not found",
201+
}, {
202+
name: "error-non-ca-issuer",
203+
spec: crloperatorv1alpha1.ManagedCRLSpec{
204+
IssuerRef: cmmetav1.IssuerReference{
205+
Name: "test-issuer-non-ca",
206+
},
207+
},
208+
shouldError: true,
209+
errorMessage: "issuer test-issuer-non-ca .*is not a CA issuer",
210+
}, {
211+
name: "error-too-small-duration",
212+
spec: crloperatorv1alpha1.ManagedCRLSpec{
213+
Duration: &metav1.Duration{Duration: time.Hour},
214+
},
215+
shouldError: true,
216+
errorMessage: "duration must be at least 24h",
217+
}, {
218+
name: "error-invalid-serial-number",
219+
spec: crloperatorv1alpha1.ManagedCRLSpec{
220+
Revocations: []crloperatorv1alpha1.RevocationSpec{
221+
{
222+
SerialNumber: "invalid-serial",
223+
},
224+
},
225+
},
226+
shouldError: true,
227+
errorMessage: "invalid serial number: invalid-serial",
228+
}, {
229+
name: "error-empty-ingress",
230+
spec: crloperatorv1alpha1.ManagedCRLSpec{
231+
Expose: &crloperatorv1alpha1.CRLExposeSpec{
232+
Enabled: true,
233+
Ingress: &crloperatorv1alpha1.IngressSpec{},
234+
},
235+
},
236+
shouldError: true,
237+
errorMessage: "invalid ingress configuration: either hostname or ipAddresses must be specified",
180238
},
181239
}
182240
)
@@ -186,15 +244,19 @@ func toTableEntry(tcs []mcrlTestCase) []TableEntry {
186244
for _, tc := range tcs {
187245
// Always add one entry for Issuer and one for ClusterIssuer
188246
name := tc.name
189-
tc.spec.IssuerRef.Name = "test-issuer"
190-
191-
tc.name = fmt.Sprintf("%s-issuer", name)
192-
tc.spec.IssuerRef.Kind = "Issuer"
193-
entries = append(entries, Entry(fmt.Sprintf("ManagedCRL %s", tc.name), tc))
247+
if tc.spec.IssuerRef.Name == "" {
248+
tc.spec.IssuerRef.Name = "test-issuer"
249+
}
250+
issuerKindToTest := []string{"Issuer", "ClusterIssuer"}
251+
if tc.spec.IssuerRef.Kind != "" {
252+
issuerKindToTest = []string{tc.spec.IssuerRef.Kind}
253+
}
194254

195-
tc.name = fmt.Sprintf("%s-clusterissuer", name)
196-
tc.spec.IssuerRef.Kind = "ClusterIssuer"
197-
entries = append(entries, Entry(fmt.Sprintf("ManagedCRL %s", tc.name), tc))
255+
for _, kind := range issuerKindToTest {
256+
tc.name = fmt.Sprintf("%s-%s", name, strings.ToLower(kind))
257+
tc.spec.IssuerRef.Kind = kind
258+
entries = append(entries, Entry(fmt.Sprintf("ManagedCRL %s", tc.name), tc))
259+
}
198260
}
199261
return entries
200262
}
@@ -231,6 +293,19 @@ var _ = Describe("ManagedCRL Controller", func() {
231293
},
232294
},
233295
)).To(Succeed())
296+
Expect(k8sClient.Create(
297+
ctx,
298+
&cmv1.ClusterIssuer{
299+
ObjectMeta: metav1.ObjectMeta{
300+
Name: "test-issuer-non-ca",
301+
},
302+
Spec: cmv1.IssuerSpec{
303+
IssuerConfig: cmv1.IssuerConfig{
304+
SelfSigned: &cmv1.SelfSignedIssuer{},
305+
},
306+
},
307+
},
308+
)).To(Succeed())
234309
Expect(k8sClient.Create(
235310
ctx,
236311
&corev1.Secret{
@@ -261,6 +336,20 @@ var _ = Describe("ManagedCRL Controller", func() {
261336
},
262337
},
263338
)).To(Succeed())
339+
Expect(k8sClient.Create(
340+
ctx,
341+
&cmv1.Issuer{
342+
ObjectMeta: metav1.ObjectMeta{
343+
Name: "test-issuer-non-ca",
344+
Namespace: testNamespace,
345+
},
346+
Spec: cmv1.IssuerSpec{
347+
IssuerConfig: cmv1.IssuerConfig{
348+
SelfSigned: &cmv1.SelfSignedIssuer{},
349+
},
350+
},
351+
},
352+
)).To(Succeed())
264353
})
265354

266355
AfterEach(func() {
@@ -275,6 +364,11 @@ var _ = Describe("ManagedCRL Controller", func() {
275364
Name: "test-issuer",
276365
},
277366
})).To(Succeed())
367+
Expect(k8sClient.Delete(ctx, &cmv1.ClusterIssuer{
368+
ObjectMeta: metav1.ObjectMeta{
369+
Name: "test-issuer-non-ca",
370+
},
371+
})).To(Succeed())
278372
})
279373

280374
DescribeTableSubtree("should reconcile various ManagedCRL resources as expected", func(tc mcrlTestCase) {
@@ -295,6 +389,7 @@ var _ = Describe("ManagedCRL Controller", func() {
295389
err := k8sClient.Create(ctx, managedcrl)
296390
if tc.shouldError {
297391
Expect(err).To(HaveOccurred())
392+
Expect(err).To(MatchError(MatchRegexp(tc.errorMessage)))
298393
return
299394
}
300395
Expect(err).ToNot(HaveOccurred())

test/integration/suite_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"os"
2525
"path/filepath"
2626
"testing"
27+
"time"
2728

2829
. "github.com/onsi/ginkgo/v2"
2930
. "github.com/onsi/gomega"
@@ -38,9 +39,11 @@ import (
3839
"sigs.k8s.io/controller-runtime/pkg/envtest"
3940
logf "sigs.k8s.io/controller-runtime/pkg/log"
4041
"sigs.k8s.io/controller-runtime/pkg/log/zap"
42+
"sigs.k8s.io/controller-runtime/pkg/webhook"
4143

4244
crloperatorv1alpha1 "github.com/scality/crl-operator/api/v1alpha1"
4345
"github.com/scality/crl-operator/internal/controller"
46+
webhookv1alpha1 "github.com/scality/crl-operator/internal/webhook/v1alpha1"
4447
// +kubebuilder:scaffold:imports
4548
)
4649

@@ -137,6 +140,9 @@ var _ = BeforeSuite(func() {
137140
filepath.Join("..", "..", "testdata", "crds"),
138141
},
139142
ErrorIfCRDPathMissing: true,
143+
WebhookInstallOptions: envtest.WebhookInstallOptions{
144+
Paths: []string{filepath.Join("..", "..", "config", "webhook")},
145+
},
140146
}
141147

142148
// Retrieve the first found binary directory to allow running tests from IDEs
@@ -155,8 +161,14 @@ var _ = BeforeSuite(func() {
155161

156162
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
157163
Scheme: scheme.Scheme,
164+
WebhookServer: webhook.NewServer(webhook.Options{
165+
Host: testEnv.WebhookInstallOptions.LocalServingHost,
166+
Port: testEnv.WebhookInstallOptions.LocalServingPort,
167+
CertDir: testEnv.WebhookInstallOptions.LocalServingCertDir,
168+
}),
158169
})
159170
Expect(err).ToNot(HaveOccurred())
171+
Expect(webhookv1alpha1.SetupManagedCRLWebhookWithManager(k8sManager)).To(Succeed())
160172

161173
// Create default cert-manager namespace
162174
Expect(k8sClient.Create(
@@ -234,6 +246,16 @@ var _ = BeforeSuite(func() {
234246
err = k8sManager.Start(ctx)
235247
Expect(err).ToNot(HaveOccurred())
236248
}()
249+
250+
By("waiting for webhook server to be ready")
251+
Eventually(func() error {
252+
webhookServer := k8sManager.GetWebhookServer()
253+
err := webhookServer.StartedChecker()(nil)
254+
if err != nil {
255+
return fmt.Errorf("webhook server not started: %w", err)
256+
}
257+
return nil
258+
}, 10*time.Second, time.Second).Should(Succeed())
237259
})
238260

239261
var _ = AfterSuite(func() {

0 commit comments

Comments
 (0)