Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions api/v1alpha1/pattern_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
Copyright 2022.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"context"
"fmt"

"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

var patternlog = logf.Log.WithName("pattern-resource")

// +kubebuilder:object:generate=false
// +k8s:deepcopy-gen=false
// +k8s:openapi-gen=false
// PatternValidator validates Pattern resources to enforce singleton semantics.
type PatternValidator struct {
Client client.Client
}

//nolint:lll
// +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

var _ webhook.CustomValidator = &PatternValidator{}

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (r *PatternValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
r.Client = mgr.GetClient()
return ctrl.NewWebhookManagedBy(mgr).
For(&Pattern{}).
WithValidator(r).
Complete()
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
func (r *PatternValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
p, err := convertToPattern(obj)
if err != nil {
return nil, err
}
patternlog.Info("validate create", "name", p.Name)

var patterns PatternList
if err = r.Client.List(ctx, &patterns); err != nil {
return nil, fmt.Errorf("failed to list Pattern resources: %v", err)
}
if len(patterns.Items) > 0 {
return nil, fmt.Errorf("only one Pattern resource is allowed")
}

return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
func (r *PatternValidator) ValidateUpdate(_ context.Context, _, newObj runtime.Object) (admission.Warnings, error) {
p, err := convertToPattern(newObj)
if err != nil {
return nil, err
}
patternlog.Info("validate update", "name", p.Name)
return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
func (r *PatternValidator) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
p, err := convertToPattern(obj)
if err != nil {
return nil, err
}
patternlog.Info("validate delete", "name", p.Name)
return nil, nil
}

func convertToPattern(obj runtime.Object) (*Pattern, error) {
p, ok := obj.(*Pattern)
if !ok {
return nil, fmt.Errorf("expected a Pattern object but got %T", obj)
}
return p, nil
}
168 changes: 168 additions & 0 deletions api/v1alpha1/pattern_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
Copyright 2022.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha1

import (
"context"
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestValidateCreate_AllowsFirstPattern(t *testing.T) {
scheme := runtime.NewScheme()
if err := AddToScheme(scheme); err != nil {
t.Fatalf("failed to add scheme: %v", err)
}

fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
validator := &PatternValidator{Client: fakeClient}

p := &Pattern{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pattern",
Namespace: "default",
},
Spec: PatternSpec{
ClusterGroupName: "hub",
GitConfig: GitConfig{
TargetRepo: "https://github.com/example/repo",
TargetRevision: "main",
},
},
}

warnings, err := validator.ValidateCreate(context.Background(), p)
if err != nil {
t.Errorf("expected no error for first pattern, got: %v", err)
}
if warnings != nil {
t.Errorf("expected no warnings, got: %v", warnings)
}
}

func TestValidateCreate_DeniesSecondPattern(t *testing.T) {
scheme := runtime.NewScheme()
if err := AddToScheme(scheme); err != nil {
t.Fatalf("failed to add scheme: %v", err)
}

existing := &Pattern{
ObjectMeta: metav1.ObjectMeta{
Name: "existing-pattern",
Namespace: "default",
},
Spec: PatternSpec{
ClusterGroupName: "hub",
GitConfig: GitConfig{
TargetRepo: "https://github.com/example/repo",
TargetRevision: "main",
},
},
}

fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(existing).Build()
validator := &PatternValidator{Client: fakeClient}

p := &Pattern{
ObjectMeta: metav1.ObjectMeta{
Name: "second-pattern",
Namespace: "default",
},
Spec: PatternSpec{
ClusterGroupName: "hub",
GitConfig: GitConfig{
TargetRepo: "https://github.com/example/repo2",
TargetRevision: "main",
},
},
}

_, err := validator.ValidateCreate(context.Background(), p)
if err == nil {
t.Error("expected error when creating second pattern, got nil")
}
}

func TestValidateCreate_RejectsNonPatternObject(t *testing.T) {
scheme := runtime.NewScheme()
if err := AddToScheme(scheme); err != nil {
t.Fatalf("failed to add scheme: %v", err)
}

fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
validator := &PatternValidator{Client: fakeClient}

notAPattern := &PatternList{}

_, err := validator.ValidateCreate(context.Background(), notAPattern)
if err == nil {
t.Error("expected error for non-Pattern object, got nil")
}
}

func TestValidateUpdate_Allows(t *testing.T) {
scheme := runtime.NewScheme()
if err := AddToScheme(scheme); err != nil {
t.Fatalf("failed to add scheme: %v", err)
}

fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
validator := &PatternValidator{Client: fakeClient}

p := &Pattern{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pattern",
Namespace: "default",
},
}

warnings, err := validator.ValidateUpdate(context.Background(), p, p)
if err != nil {
t.Errorf("expected no error on update, got: %v", err)
}
if warnings != nil {
t.Errorf("expected no warnings, got: %v", warnings)
}
}

func TestValidateDelete_Allows(t *testing.T) {
scheme := runtime.NewScheme()
if err := AddToScheme(scheme); err != nil {
t.Fatalf("failed to add scheme: %v", err)
}

fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
validator := &PatternValidator{Client: fakeClient}

p := &Pattern{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pattern",
Namespace: "default",
},
}

warnings, err := validator.ValidateDelete(context.Background(), p)
if err != nil {
t.Errorf("expected no error on delete, got: %v", err)
}
if warnings != nil {
t.Errorf("expected no warnings, got: %v", warnings)
}
}
2 changes: 1 addition & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions bundle/manifests/patterns-operator-webhook-service_v1_service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app.kubernetes.io/managed-by: kustomize
app.kubernetes.io/name: patterns-operator
name: patterns-operator-webhook-service
spec:
ports:
- port: 443
protocol: TCP
targetPort: 9443
selector:
control-plane: controller-manager
status:
loadBalancer: {}
6 changes: 6 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "Pattern")
os.Exit(1)
}
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err = (&gitopsv1alpha1.PatternValidator{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Pattern")
os.Exit(1)
}
}
//+kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions config/default/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ bases:
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
#- ../webhook
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
#- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
Expand All @@ -36,7 +36,7 @@ patchesStrategicMerge:

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

# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'.
# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks.
Expand Down
26 changes: 26 additions & 0 deletions config/default/manager_webhook_patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
namespace: system
labels:
app.kubernetes.io/name: patterns-operator
app.kubernetes.io/managed-by: kustomize
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
6 changes: 6 additions & 0 deletions config/webhook/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resources:
- manifests.yaml
- service.yaml

configurations:
- kustomizeconfig.yaml
22 changes: 22 additions & 0 deletions config/webhook/kustomizeconfig.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# the following config is for teaching kustomize where to look at when substituting nameReference.
# It requires kustomize v2.1.0 or newer to work properly.
nameReference:
- kind: Service
version: v1
fieldSpecs:
- kind: MutatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/name
- kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/name

namespace:
- kind: MutatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/namespace
create: true
- kind: ValidatingWebhookConfiguration
group: admissionregistration.k8s.io
path: webhooks/clientConfig/service/namespace
create: true
Loading