Skip to content

Commit 1d0c71d

Browse files
Validate CRD Spec fields (#83)
* Validate CRD Spec fields This configures the CRD to require that we have either a CAPI Cluster ref, or a Secret ref. GitopsClusters will be rejected if they have neither, or have both. Co-authored-by: Yiannis Triantafyllopoulos <8741709+yiannistri@users.noreply.github.com>
1 parent 829b7bc commit 1d0c71d

3 files changed

Lines changed: 100 additions & 0 deletions

File tree

api/v1alpha1/gitopscluster_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
const GitOpsClusterNoSecretFinalizerAnnotation = "clusters.gitops.weave.works/no-secret-finalizer"
2929

3030
// GitopsClusterSpec defines the desired state of GitopsCluster
31+
// +kubebuilder:validation:XValidation:rule="(has(self.secretRef) || has(self.capiClusterRef))",message="must provide a secretRef or capiClusterRef"
32+
// +kubebuilder:validation:XValidation:rule="!(has(self.secretRef) && has(self.capiClusterRef))",message="cannot provide both capiClusterRef and secretRef"
3133
type GitopsClusterSpec struct {
3234
// SecretRef specifies the Secret containing the kubeconfig for a cluster.
3335
// +optional
@@ -62,6 +64,7 @@ func (in *GitopsCluster) SetConditions(conditions []metav1.Condition) {
6264
// +kubebuilder:printcolumn:name="ClusterConnectivity",type="string",JSONPath=".status.conditions[?(@.type==\"ClusterConnectivity\")].status",description=""
6365

6466
// GitopsCluster is the Schema for the gitopsclusters API
67+
// +kubebuilder:validation:XValidation:rule="has(self.spec)",message="must confgure spec"
6568
type GitopsCluster struct {
6669
metav1.TypeMeta `json:",inline"`
6770
metav1.ObjectMeta `json:"metadata,omitempty"`

config/crd/bases/gitops.weave.works_gitopsclusters.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ spec:
6767
- name
6868
type: object
6969
type: object
70+
x-kubernetes-validations:
71+
- message: must provide a secretRef or capiClusterRef
72+
rule: (has(self.secretRef) || has(self.capiClusterRef))
73+
- message: cannot provide both capiClusterRef and secretRef
74+
rule: '!(has(self.secretRef) && has(self.capiClusterRef))'
7075
status:
7176
description: GitopsClusterStatus defines the observed state of GitopsCluster
7277
properties:
@@ -141,6 +146,9 @@ spec:
141146
type: array
142147
type: object
143148
type: object
149+
x-kubernetes-validations:
150+
- message: must configure spec
151+
rule: has(self.spec)
144152
served: true
145153
storage: true
146154
subresources:

controllers/gitopscluster_controller_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package controllers_test
22

33
import (
44
"context"
5+
"path/filepath"
56
"regexp"
67
"testing"
78
"time"
@@ -22,6 +23,7 @@ import (
2223
"sigs.k8s.io/controller-runtime/pkg/client"
2324
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2425
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
26+
"sigs.k8s.io/controller-runtime/pkg/envtest"
2527
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2628
)
2729

@@ -514,6 +516,86 @@ func TestFinalizers(t *testing.T) {
514516
}
515517
}
516518

519+
func TestGitopsClusterValidation(t *testing.T) {
520+
testEnv := &envtest.Environment{
521+
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
522+
}
523+
testCfg, err := testEnv.Start()
524+
if err != nil {
525+
t.Fatal(err)
526+
}
527+
defer func() {
528+
if err := testEnv.Stop(); err != nil {
529+
t.Fatalf("failed to shutdown testEnv: %s", err)
530+
}
531+
}()
532+
533+
s := runtime.NewScheme()
534+
if err := gitopsv1alpha1.AddToScheme(s); err != nil {
535+
t.Fatal(err)
536+
}
537+
538+
cl, err := client.New(testCfg, client.Options{Scheme: s})
539+
if err != nil {
540+
t.Fatal(err)
541+
}
542+
543+
t.Run("when neither the secret nor capi cluster are configured", func(t *testing.T) {
544+
testCluster := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
545+
c.ObjectMeta.Name = "no-config"
546+
c.ObjectMeta.Namespace = "default"
547+
c.Spec.SecretRef = nil
548+
c.Spec.CAPIClusterRef = nil
549+
})
550+
551+
err := cl.Create(context.TODO(), testCluster)
552+
assertErrorMatch(t, "must provide a secretRef or capiClusterRef", err)
553+
assertErrorDoesNotMatch(t, "cannot provide both capiClusterRef and secretRef", err)
554+
})
555+
556+
t.Run("when both the secret and capi cluster are configured", func(t *testing.T) {
557+
testCluster := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
558+
c.ObjectMeta.Name = "both-configs"
559+
c.ObjectMeta.Namespace = "default"
560+
c.Spec.SecretRef = &meta.LocalObjectReference{
561+
Name: "test-secret",
562+
}
563+
c.Spec.CAPIClusterRef = &meta.LocalObjectReference{
564+
Name: "test-cluster",
565+
}
566+
})
567+
568+
err := cl.Create(context.TODO(), testCluster)
569+
assertErrorMatch(t, "cannot provide both capiClusterRef and secretRef", err)
570+
assertErrorDoesNotMatch(t, "must provide a secretRef or capiClusterRef", err)
571+
})
572+
573+
t.Run("when the secret is configured", func(t *testing.T) {
574+
testCluster := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
575+
c.ObjectMeta.Name = "only-secret-configured"
576+
c.ObjectMeta.Namespace = "default"
577+
c.Spec.SecretRef = &meta.LocalObjectReference{
578+
Name: "test-secret",
579+
}
580+
})
581+
582+
assertNoError(t, cl.Create(context.TODO(), testCluster))
583+
})
584+
585+
t.Run("when the capiClusterRef is configured", func(t *testing.T) {
586+
testCluster := makeTestCluster(func(c *gitopsv1alpha1.GitopsCluster) {
587+
c.ObjectMeta.Name = "only-capi-cluster-configured"
588+
c.ObjectMeta.Namespace = "default"
589+
c.Spec.CAPIClusterRef = &meta.LocalObjectReference{
590+
Name: "test-cluster",
591+
}
592+
})
593+
594+
assertNoError(t, cl.Create(context.TODO(), testCluster))
595+
})
596+
597+
}
598+
517599
func makeTestReconciler(t *testing.T, opts controllers.Options, objs ...runtime.Object) *controllers.GitopsClusterReconciler {
518600
s, tc := makeTestClientAndScheme(t, opts, objs...)
519601
return controllers.NewGitopsClusterReconciler(tc, s, opts)
@@ -592,6 +674,13 @@ func assertErrorMatch(t *testing.T, s string, e error) {
592674
}
593675
}
594676

677+
func assertErrorDoesNotMatch(t *testing.T, s string, e error) {
678+
t.Helper()
679+
if matchErrorString(t, s, e) {
680+
t.Fatalf("error did match, got %s, should not contain %s", e, s)
681+
}
682+
}
683+
595684
func matchErrorString(t *testing.T, s string, e error) bool {
596685
t.Helper()
597686
if s == "" && e == nil {

0 commit comments

Comments
 (0)