Skip to content

Commit a4bc5ee

Browse files
Merge pull request #514 from mbaldessari/singleton
Make the pattern CR a singleton
2 parents 090114f + 7d7ce35 commit a4bc5ee

11 files changed

Lines changed: 388 additions & 3 deletions

api/v1alpha1/pattern_webhook.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
Copyright 2022.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
"k8s.io/apimachinery/pkg/runtime"
24+
ctrl "sigs.k8s.io/controller-runtime"
25+
"sigs.k8s.io/controller-runtime/pkg/client"
26+
logf "sigs.k8s.io/controller-runtime/pkg/log"
27+
"sigs.k8s.io/controller-runtime/pkg/webhook"
28+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
29+
)
30+
31+
var patternlog = logf.Log.WithName("pattern-resource")
32+
33+
// +kubebuilder:object:generate=false
34+
// +k8s:deepcopy-gen=false
35+
// +k8s:openapi-gen=false
36+
// PatternValidator validates Pattern resources to enforce singleton semantics.
37+
type PatternValidator struct {
38+
Client client.Client
39+
}
40+
41+
//nolint:lll
42+
// +kubebuilder:webhook:verbs=create,path=/validate-gitops-hybrid-cloud-patterns-io-v1alpha1-pattern,mutating=false,failurePolicy=fail,groups=gitops.hybrid-cloud-patterns.io,resources=patterns,versions=v1alpha1,name=vpattern.gitops.hybrid-cloud-patterns.io,admissionReviewVersions=v1,sideEffects=none
43+
44+
var _ webhook.CustomValidator = &PatternValidator{}
45+
46+
// SetupWebhookWithManager will setup the manager to manage the webhooks
47+
func (r *PatternValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
48+
r.Client = mgr.GetClient()
49+
return ctrl.NewWebhookManagedBy(mgr).
50+
For(&Pattern{}).
51+
WithValidator(r).
52+
Complete()
53+
}
54+
55+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
56+
func (r *PatternValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
57+
p, err := convertToPattern(obj)
58+
if err != nil {
59+
return nil, err
60+
}
61+
patternlog.Info("validate create", "name", p.Name)
62+
63+
var patterns PatternList
64+
if err = r.Client.List(ctx, &patterns); err != nil {
65+
return nil, fmt.Errorf("failed to list Pattern resources: %v", err)
66+
}
67+
if len(patterns.Items) > 0 {
68+
return nil, fmt.Errorf("only one Pattern resource is allowed")
69+
}
70+
71+
return nil, nil
72+
}
73+
74+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
75+
func (r *PatternValidator) ValidateUpdate(_ context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
76+
p, err := convertToPattern(newObj)
77+
if err != nil {
78+
return nil, err
79+
}
80+
patternlog.Info("validate update", "name", p.Name)
81+
return nil, nil
82+
}
83+
84+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
85+
func (r *PatternValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
86+
p, err := convertToPattern(obj)
87+
if err != nil {
88+
return nil, err
89+
}
90+
patternlog.Info("validate delete", "name", p.Name)
91+
return nil, nil
92+
}
93+
94+
func convertToPattern(obj runtime.Object) (*Pattern, error) {
95+
p, ok := obj.(*Pattern)
96+
if !ok {
97+
return nil, fmt.Errorf("expected a Pattern object but got %T", obj)
98+
}
99+
return p, nil
100+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
Copyright 2022.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/runtime"
25+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
26+
)
27+
28+
func TestValidateCreate_AllowsFirstPattern(t *testing.T) {
29+
scheme := runtime.NewScheme()
30+
if err := AddToScheme(scheme); err != nil {
31+
t.Fatalf("failed to add scheme: %v", err)
32+
}
33+
34+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
35+
validator := &PatternValidator{Client: fakeClient}
36+
37+
p := &Pattern{
38+
ObjectMeta: metav1.ObjectMeta{
39+
Name: "test-pattern",
40+
Namespace: "default",
41+
},
42+
Spec: PatternSpec{
43+
ClusterGroupName: "hub",
44+
GitConfig: GitConfig{
45+
TargetRepo: "https://github.com/example/repo",
46+
TargetRevision: "main",
47+
},
48+
},
49+
}
50+
51+
warnings, err := validator.ValidateCreate(context.Background(), p)
52+
if err != nil {
53+
t.Errorf("expected no error for first pattern, got: %v", err)
54+
}
55+
if warnings != nil {
56+
t.Errorf("expected no warnings, got: %v", warnings)
57+
}
58+
}
59+
60+
func TestValidateCreate_DeniesSecondPattern(t *testing.T) {
61+
scheme := runtime.NewScheme()
62+
if err := AddToScheme(scheme); err != nil {
63+
t.Fatalf("failed to add scheme: %v", err)
64+
}
65+
66+
existing := &Pattern{
67+
ObjectMeta: metav1.ObjectMeta{
68+
Name: "existing-pattern",
69+
Namespace: "default",
70+
},
71+
Spec: PatternSpec{
72+
ClusterGroupName: "hub",
73+
GitConfig: GitConfig{
74+
TargetRepo: "https://github.com/example/repo",
75+
TargetRevision: "main",
76+
},
77+
},
78+
}
79+
80+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existing).Build()
81+
validator := &PatternValidator{Client: fakeClient}
82+
83+
p := &Pattern{
84+
ObjectMeta: metav1.ObjectMeta{
85+
Name: "second-pattern",
86+
Namespace: "default",
87+
},
88+
Spec: PatternSpec{
89+
ClusterGroupName: "hub",
90+
GitConfig: GitConfig{
91+
TargetRepo: "https://github.com/example/repo2",
92+
TargetRevision: "main",
93+
},
94+
},
95+
}
96+
97+
_, err := validator.ValidateCreate(context.Background(), p)
98+
if err == nil {
99+
t.Error("expected error when creating second pattern, got nil")
100+
}
101+
}
102+
103+
func TestValidateCreate_RejectsNonPatternObject(t *testing.T) {
104+
scheme := runtime.NewScheme()
105+
if err := AddToScheme(scheme); err != nil {
106+
t.Fatalf("failed to add scheme: %v", err)
107+
}
108+
109+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
110+
validator := &PatternValidator{Client: fakeClient}
111+
112+
notAPattern := &PatternList{}
113+
114+
_, err := validator.ValidateCreate(context.Background(), notAPattern)
115+
if err == nil {
116+
t.Error("expected error for non-Pattern object, got nil")
117+
}
118+
}
119+
120+
func TestValidateUpdate_Allows(t *testing.T) {
121+
scheme := runtime.NewScheme()
122+
if err := AddToScheme(scheme); err != nil {
123+
t.Fatalf("failed to add scheme: %v", err)
124+
}
125+
126+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
127+
validator := &PatternValidator{Client: fakeClient}
128+
129+
p := &Pattern{
130+
ObjectMeta: metav1.ObjectMeta{
131+
Name: "test-pattern",
132+
Namespace: "default",
133+
},
134+
}
135+
136+
warnings, err := validator.ValidateUpdate(context.Background(), p, p)
137+
if err != nil {
138+
t.Errorf("expected no error on update, got: %v", err)
139+
}
140+
if warnings != nil {
141+
t.Errorf("expected no warnings, got: %v", warnings)
142+
}
143+
}
144+
145+
func TestValidateDelete_Allows(t *testing.T) {
146+
scheme := runtime.NewScheme()
147+
if err := AddToScheme(scheme); err != nil {
148+
t.Fatalf("failed to add scheme: %v", err)
149+
}
150+
151+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
152+
validator := &PatternValidator{Client: fakeClient}
153+
154+
p := &Pattern{
155+
ObjectMeta: metav1.ObjectMeta{
156+
Name: "test-pattern",
157+
Namespace: "default",
158+
},
159+
}
160+
161+
warnings, err := validator.ValidateDelete(context.Background(), p)
162+
if err != nil {
163+
t.Errorf("expected no error on delete, got: %v", err)
164+
}
165+
if warnings != nil {
166+
t.Errorf("expected no warnings, got: %v", warnings)
167+
}
168+
}

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
creationTimestamp: null
5+
labels:
6+
app.kubernetes.io/managed-by: kustomize
7+
app.kubernetes.io/name: patterns-operator
8+
name: patterns-operator-webhook-service
9+
spec:
10+
ports:
11+
- port: 443
12+
protocol: TCP
13+
targetPort: 9443
14+
selector:
15+
control-plane: controller-manager
16+
status:
17+
loadBalancer: {}

cmd/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ func main() {
108108
setupLog.Error(err, "unable to create controller", "controller", "Pattern")
109109
os.Exit(1)
110110
}
111+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
112+
if err = (&gitopsv1alpha1.PatternValidator{}).SetupWebhookWithManager(mgr); err != nil {
113+
setupLog.Error(err, "unable to create webhook", "webhook", "Pattern")
114+
os.Exit(1)
115+
}
116+
}
111117
//+kubebuilder:scaffold:builder
112118

113119
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {

config/default/kustomization.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ bases:
1818
- ../manager
1919
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
2020
# crd/kustomization.yaml
21-
#- ../webhook
21+
- ../webhook
2222
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
2323
#- ../certmanager
2424
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
@@ -36,7 +36,7 @@ patchesStrategicMerge:
3636

3737
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
3838
# crd/kustomization.yaml
39-
#- manager_webhook_patch.yaml
39+
- manager_webhook_patch.yaml
4040

4141
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
4242
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: controller-manager
5+
namespace: system
6+
labels:
7+
app.kubernetes.io/name: patterns-operator
8+
app.kubernetes.io/managed-by: kustomize
9+
spec:
10+
template:
11+
spec:
12+
containers:
13+
- name: manager
14+
ports:
15+
- containerPort: 9443
16+
name: webhook-server
17+
protocol: TCP
18+
volumeMounts:
19+
- mountPath: /tmp/k8s-webhook-server/serving-certs
20+
name: cert
21+
readOnly: true
22+
volumes:
23+
- name: cert
24+
secret:
25+
defaultMode: 420
26+
secretName: webhook-server-cert

config/webhook/kustomization.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
resources:
2+
- manifests.yaml
3+
- service.yaml
4+
5+
configurations:
6+
- kustomizeconfig.yaml
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# the following config is for teaching kustomize where to look at when substituting nameReference.
2+
# It requires kustomize v2.1.0 or newer to work properly.
3+
nameReference:
4+
- kind: Service
5+
version: v1
6+
fieldSpecs:
7+
- kind: MutatingWebhookConfiguration
8+
group: admissionregistration.k8s.io
9+
path: webhooks/clientConfig/service/name
10+
- kind: ValidatingWebhookConfiguration
11+
group: admissionregistration.k8s.io
12+
path: webhooks/clientConfig/service/name
13+
14+
namespace:
15+
- kind: MutatingWebhookConfiguration
16+
group: admissionregistration.k8s.io
17+
path: webhooks/clientConfig/service/namespace
18+
create: true
19+
- kind: ValidatingWebhookConfiguration
20+
group: admissionregistration.k8s.io
21+
path: webhooks/clientConfig/service/namespace
22+
create: true

0 commit comments

Comments
 (0)