diff --git a/Dockerfile b/Dockerfile index 57931d5a..34c9b8a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,2 +1,4 @@ -FROM scratch -COPY bin/firewall-controller /firewall-controller +FROM alpine:3.18 +COPY bin/firewall-controller-webhook . +USER 65534 +ENTRYPOINT ["/firewall-controller-webhook"] diff --git a/Makefile b/Makefile index 20b41be2..b0aef11a 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ LOCALBIN ?= $(shell pwd)/bin CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest -all: firewall-controller +all: firewall-controller firewall-controller-webhook # Build firewall-controller binary firewall-controller: generate fmt vet @@ -24,6 +24,19 @@ firewall-controller: generate fmt vet strip bin/firewall-controller sha256sum bin/firewall-controller > bin/firewall-controller.sha256 +firewall-controller-webhook: generate fmt vet + CGO_ENABLED=0 go build \ + -tags netgo \ + -trimpath \ + -ldflags \ + "-X 'github.com/metal-stack/v.Version=$(VERSION)' \ + -X 'github.com/metal-stack/v.Revision=$(GITVERSION)' \ + -X 'github.com/metal-stack/v.GitSHA1=$(SHA)' \ + -X 'github.com/metal-stack/v.BuildDate=$(BUILDDATE)'" \ + -o bin/firewall-controller-webhook cmd/webhook/cmd.go + strip bin/firewall-controller-webhook + sha256sum bin/firewall-controller-webhook > bin/firewall-controller-webhook.sha256 + $(LOCALBIN): mkdir -p $(LOCALBIN) @@ -52,7 +65,8 @@ deploy: manifests # Generate manifests e.g. CRD, RBAC etc. manifests: controller-gen - $(CONTROLLER_GEN) crd rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) crd rbac:roleName=manager-role paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) +webhook paths="./..." +output:dir=config/webhooks # Run go fmt against code fmt: diff --git a/api/v1/clusterwidenetworkpolicy_types.go b/api/v1/clusterwidenetworkpolicy_types.go index 421855da..d2590927 100644 --- a/api/v1/clusterwidenetworkpolicy_types.go +++ b/api/v1/clusterwidenetworkpolicy_types.go @@ -1,16 +1,11 @@ package v1 import ( - "errors" - "fmt" - "net" "strings" dnsgo "github.com/miekg/dns" - corev1 "k8s.io/api/core/v1" networking "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" ) type IPVersion string @@ -186,72 +181,3 @@ func (s FQDNSelector) GetRegex() string { // Anchor the match to require the whole string to match this expression return "^" + pattern + "$" } - -// Validate validates the spec of a ClusterwideNetworkPolicy -func (p *PolicySpec) Validate() error { - var errs []error - for _, e := range p.Egress { - errs = append(errs, validatePorts(e.Ports), validateIPBlocks(e.To)) - } - for _, i := range p.Ingress { - errs = append(errs, validatePorts(i.Ports), validateIPBlocks(i.From)) - } - - return errors.Join(errs...) -} - -func validatePorts(ports []networking.NetworkPolicyPort) error { - var errs []error - for _, p := range ports { - if p.Port != nil && p.Port.Type != intstr.Int { - errs = append(errs, fmt.Errorf("only int ports are supported, but %v given", p.Port)) - } - - if p.Port != nil && (p.Port.IntValue() > 65535 || p.Port.IntValue() <= 0) { - errs = append(errs, fmt.Errorf("only ports between 0 and 65535 are allowed, but %v given", p.Port)) - } - - if p.Protocol != nil { - proto := *p.Protocol - if proto != corev1.ProtocolUDP && proto != corev1.ProtocolTCP { - errs = append(errs, fmt.Errorf("only TCP and UDP are supported as protocol, but %v given", proto)) - } - } - } - return errors.Join(errs...) -} - -func validateIPBlocks(blocks []networking.IPBlock) error { - var errs []error - for _, b := range blocks { - _, blockNet, err := net.ParseCIDR(b.CIDR) - if err != nil { - errs = append(errs, fmt.Errorf("%v is not a valid IP CIDR", b.CIDR)) - continue - } - - for _, e := range b.Except { - exceptIP, exceptNet, err := net.ParseCIDR(b.CIDR) - if err != nil { - errs = append(errs, fmt.Errorf("%v is not a valid IP CIDR", e)) - continue - } - - if !blockNet.Contains(exceptIP) { - errs = append(errs, fmt.Errorf("%v is not contained in the IP CIDR %v", exceptIP, blockNet)) - continue - } - - blockSize, _ := blockNet.Mask.Size() - exceptSize, _ := exceptNet.Mask.Size() - if exceptSize > blockSize { - errs = append(errs, fmt.Errorf("netmask size of network to be excluded must be smaller than netmask of the block CIDR")) - } - } - } - return errors.Join(errs...) -} - -func init() { - SchemeBuilder.Register(&ClusterwideNetworkPolicy{}, &ClusterwideNetworkPolicyList{}) -} diff --git a/api/v1/clusterwidenetworkpolicy_types_test.go b/api/v1/clusterwidenetworkpolicy_types_test.go deleted file mode 100644 index 04dfb015..00000000 --- a/api/v1/clusterwidenetworkpolicy_types_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package v1 - -import ( - "testing" - - corev1 "k8s.io/api/core/v1" - networking "k8s.io/api/networking/v1" - "k8s.io/apimachinery/pkg/util/intstr" -) - -func TestPolicySpec_Validate(t *testing.T) { - tcp := corev1.ProtocolTCP - udp := corev1.ProtocolUDP - port1 := intstr.FromInt(8080) - port2 := intstr.FromInt(8081) - invalid := intstr.FromString("invalid") - invalidPort := intstr.FromInt(99999) - tests := []struct { - name string - Ingress []IngressRule - Egress []EgressRule - wantErr bool - }{ - { - name: "simple test", - Ingress: []IngressRule{ - { - From: []networking.IPBlock{ - { - CIDR: "1.1.0.0/16", - Except: []string{"1.1.1.0/24"}, - }, - { - CIDR: "192.168.0.1/32", - Except: []string{"192.168.0.1/32"}, - }, - }, - Ports: []networking.NetworkPolicyPort{ - { - Protocol: nil, - Port: &port1, - }, - { - Protocol: &tcp, - Port: &port2, - }, - { - Protocol: &udp, - Port: &port2, - }, - }, - }, - }, - }, - { - name: "invalid test", - Ingress: []IngressRule{ - { - From: []networking.IPBlock{ - { - CIDR: "1.1.0.0/24", - Except: []string{"1.1.1.0/16"}, - }, - { - CIDR: "192.168.0.1", - Except: []string{"192.168.0.2"}, - }, - }, - Ports: []networking.NetworkPolicyPort{ - { - Protocol: nil, - Port: &invalid, - }, - }, - }, - }, - wantErr: true, - }, - { - name: "invalid port", - Ingress: []IngressRule{ - { - From: []networking.IPBlock{ - { - CIDR: "1.1.0.0/24", - }, - }, - Ports: []networking.NetworkPolicyPort{ - { - Protocol: &tcp, - Port: &invalidPort, - }, - }, - }, - }, - wantErr: true, - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - p := &PolicySpec{ - Ingress: tt.Ingress, - Egress: tt.Egress, - } - if err := p.Validate(); (err != nil) != tt.wantErr { - t.Errorf("PolicySpec.Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestFQDNSelector_GetRegex(t *testing.T) { - tests := []struct { - name string - selector FQDNSelector - expectedRegex string - }{ - { - name: "match all cases", - selector: FQDNSelector{ - MatchPattern: "*", - }, - expectedRegex: "(^(" + allowedDNSCharsREGroup + "+[.])+$)|(^[.]$)", - }, - { - name: "selector with match-all and literal", - selector: FQDNSelector{ - MatchPattern: "*.com", - }, - expectedRegex: "^" + allowedDNSCharsREGroup + "*[.]com[.]$", - }, - { - name: "selector with match-all at the end", - selector: FQDNSelector{ - MatchPattern: "example.*", - }, - expectedRegex: "^example[.]" + allowedDNSCharsREGroup + "*[.]$", - }, - { - name: "selector with static value", - selector: FQDNSelector{ - MatchPattern: "example.com", - }, - expectedRegex: "^example[.]com[.]$", - }, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - if r := tt.selector.GetRegex(); tt.expectedRegex != r { - t.Errorf("FQDNSelector.GetRegex returned %s, expected %s", r, tt.expectedRegex) - } - }) - } -} diff --git a/api/v1/defaults/defaults.go b/api/v1/defaults/defaults.go new file mode 100644 index 00000000..340dd92e --- /dev/null +++ b/api/v1/defaults/defaults.go @@ -0,0 +1,34 @@ +package defaults + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + v1 "github.com/metal-stack/firewall-controller/api/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +type defaulter struct { + log logr.Logger +} + +func NewDefaulter(log logr.Logger) admission.CustomDefaulter { + return &defaulter{ + log: log, + } +} + +func (d *defaulter) Default(ctx context.Context, obj runtime.Object) error { + f, ok := obj.(*v1.ClusterwideNetworkPolicy) + if !ok { + return fmt.Errorf("mutator received unexpected type: %T", obj) + } + + d.log.Info("defaulting resource", "name", f.GetName(), "namespace", f.GetNamespace()) + + // TODO: Implement + + return nil +} diff --git a/api/v1/groupversion_info.go b/api/v1/groupversion_info.go index c1dd2632..df7bb37f 100644 --- a/api/v1/groupversion_info.go +++ b/api/v1/groupversion_info.go @@ -1,6 +1,10 @@ // Package v1 contains API Schema definitions for the firewall v1 API group // +kubebuilder:object:generate=true // +groupName=metal-stack.io +// +// +kubebuilder:webhook:path=/validate-metal-stack-io-v1-clusterwide-network-policy,mutating=false,failurePolicy=fail,groups=metal-stack.io,resources=clusterwidenetworkpolicy,verbs=create;update,versions=v1,name=metal-stack.io,sideEffects=None,admissionReviewVersions=v1 +// +// +kubebuilder:webhook:path=/mutate-metal-stack-io-v1-clusterwide-network-policy,mutating=true,failurePolicy=fail,groups=metal-stack.io,resources=clusterwidenetworkpolicy,verbs=create,versions=v1,name=metal-stack.io,sideEffects=None,admissionReviewVersions=v1 package v1 import ( diff --git a/api/v1/validation/validation.go b/api/v1/validation/validation.go new file mode 100644 index 00000000..45e0e35e --- /dev/null +++ b/api/v1/validation/validation.go @@ -0,0 +1,215 @@ +package validation + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + v1 "github.com/metal-stack/firewall-controller/api/v1" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + apivalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type Validator struct { + log logr.Logger +} + +func NewValidator(log logr.Logger) *Validator { + return &Validator{ + log: log, + } +} + +func (v *Validator) ValidateCreate(ctx context.Context, obj runtime.Object) error { + var ( + o, ok = obj.(*v1.ClusterwideNetworkPolicy) + allErrs field.ErrorList + ) + + if !ok { + return apierrors.NewBadRequest(fmt.Sprintf("validator received unexpected type: %T", obj)) + } + + accessor, err := meta.Accessor(obj) + if err != nil { + return apierrors.NewBadRequest(fmt.Sprintf("failed to get accessor for object: %s", err)) + } + + v.log.Info("validating resource creation", "name", accessor.GetName(), "namespace", accessor.GetNamespace()) + + allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessor(accessor, true, apivalidation.NameIsDNSSubdomain, field.NewPath("metadata"))...) + allErrs = append(allErrs, validateCreate(o)...) + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid( + obj.GetObjectKind().GroupVersionKind().GroupKind(), + accessor.GetName(), + allErrs, + ) +} + +func validateCreate(cwnp *v1.ClusterwideNetworkPolicy) field.ErrorList { + var allErrs field.ErrorList + + // TODO: implement + + return allErrs +} + +func (v *Validator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error { + var ( + oldO, oldOk = oldObj.(*v1.ClusterwideNetworkPolicy) + newO, newOk = newObj.(*v1.ClusterwideNetworkPolicy) + allErrs field.ErrorList + ) + + if !oldOk { + return apierrors.NewBadRequest(fmt.Sprintf("validator received unexpected type: %T", oldO)) + } + if !newOk { + return apierrors.NewBadRequest(fmt.Sprintf("validator received unexpected type: %T", newO)) + } + + oldAccessor, err := meta.Accessor(oldO) + if err != nil { + return apierrors.NewBadRequest(fmt.Sprintf("failed to get accessor for object: %s", err)) + } + newAccessor, err := meta.Accessor(newO) + if err != nil { + return apierrors.NewBadRequest(fmt.Sprintf("failed to get accessor for object: %s", err)) + } + + v.log.Info("validating resource update", "name", newAccessor.GetName(), "namespace", newAccessor.GetNamespace()) + + allErrs = append(allErrs, apivalidation.ValidateObjectMetaAccessorUpdate(newAccessor, oldAccessor, field.NewPath("metadata"))...) + allErrs = append(allErrs, validateUpdate(oldO, newO)...) + + if len(allErrs) == 0 { + return nil + } + + return apierrors.NewInvalid( + newO.GetObjectKind().GroupVersionKind().GroupKind(), + newAccessor.GetName(), + allErrs, + ) +} + +func validateUpdate(old, new *v1.ClusterwideNetworkPolicy) field.ErrorList { + var allErrs field.ErrorList + + // TODO: implement + + return allErrs +} + +// Validates ClusterwideNetworkPolicy object +// +kubebuilder:rbac:groups=metal-stack.io,resources=clusterwidenetworkpolicies,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=metal-stack.io,resources=clusterwidenetworkpolicies/status,verbs=get;update;patch +// func (r *ClusterwideNetworkPolicyValidationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +// var clusterNP firewallv1.ClusterwideNetworkPolicy +// if err := r.ShootClient.Get(ctx, req.NamespacedName, &clusterNP); err != nil { +// return ctrl.Result{}, client.IgnoreNotFound(err) +// } + +// // if network policy does not belong to the namespace where clusterwide network policies are stored: +// // update status with error message +// if req.Namespace != firewallv1.ClusterwideNetworkPolicyNamespace { +// r.Recorder.Event( +// &clusterNP, +// corev1.EventTypeWarning, +// "Unapplicable", +// fmt.Sprintf("cluster wide network policies must be defined in namespace %s otherwise they won't take effect", firewallv1.ClusterwideNetworkPolicyNamespace), +// ) +// return ctrl.Result{}, nil +// } + +// err := clusterNP.Spec.Validate() +// if err != nil { +// r.Recorder.Event( +// &clusterNP, +// corev1.EventTypeWarning, +// "Unapplicable", +// fmt.Sprintf("cluster wide network policy is not valid: %v", err), +// ) +// return ctrl.Result{}, nil +// } + +// return ctrl.Result{}, nil +// } + +// // Validate validates the spec of a ClusterwideNetworkPolicy +// func (p *PolicySpec) Validate() error { +// var errs []error +// for _, e := range p.Egress { +// errs = append(errs, validatePorts(e.Ports), validateIPBlocks(e.To)) +// } +// for _, i := range p.Ingress { +// errs = append(errs, validatePorts(i.Ports), validateIPBlocks(i.From)) +// } + +// return errors.Join(errs...) +// } + +// func validatePorts(ports []networking.NetworkPolicyPort) error { +// var errs []error +// for _, p := range ports { +// if p.Port != nil && p.Port.Type != intstr.Int { +// errs = append(errs, fmt.Errorf("only int ports are supported, but %v given", p.Port)) +// } + +// if p.Port != nil && (p.Port.IntValue() > 65535 || p.Port.IntValue() <= 0) { +// errs = append(errs, fmt.Errorf("only ports between 0 and 65535 are allowed, but %v given", p.Port)) +// } + +// if p.Protocol != nil { +// proto := *p.Protocol +// if proto != corev1.ProtocolUDP && proto != corev1.ProtocolTCP { +// errs = append(errs, fmt.Errorf("only TCP and UDP are supported as protocol, but %v given", proto)) +// } +// } +// } +// return errors.Join(errs...) +// } + +// func validateIPBlocks(blocks []networking.IPBlock) error { +// var errs []error +// for _, b := range blocks { +// _, blockNet, err := net.ParseCIDR(b.CIDR) +// if err != nil { +// errs = append(errs, fmt.Errorf("%v is not a valid IP CIDR", b.CIDR)) +// continue +// } + +// for _, e := range b.Except { +// exceptIP, exceptNet, err := net.ParseCIDR(b.CIDR) +// if err != nil { +// errs = append(errs, fmt.Errorf("%v is not a valid IP CIDR", e)) +// continue +// } + +// if !blockNet.Contains(exceptIP) { +// errs = append(errs, fmt.Errorf("%v is not contained in the IP CIDR %v", exceptIP, blockNet)) +// continue +// } + +// blockSize, _ := blockNet.Mask.Size() +// exceptSize, _ := exceptNet.Mask.Size() +// if exceptSize > blockSize { +// errs = append(errs, fmt.Errorf("netmask size of network to be excluded must be smaller than netmask of the block CIDR")) +// } +// } +// } +// return errors.Join(errs...) +// } + +func (_ *Validator) ValidateDelete(ctx context.Context, obj runtime.Object) error { + return nil +} diff --git a/api/v1/validation/validation_test.go b/api/v1/validation/validation_test.go new file mode 100644 index 00000000..13717b8e --- /dev/null +++ b/api/v1/validation/validation_test.go @@ -0,0 +1,148 @@ +package validation + +// func TestPolicySpec_Validate(t *testing.T) { +// tcp := corev1.ProtocolTCP +// udp := corev1.ProtocolUDP +// port1 := intstr.FromInt(8080) +// port2 := intstr.FromInt(8081) +// invalid := intstr.FromString("invalid") +// invalidPort := intstr.FromInt(99999) +// tests := []struct { +// name string +// Ingress []IngressRule +// Egress []EgressRule +// wantErr bool +// }{ +// { +// name: "simple test", +// Ingress: []IngressRule{ +// { +// From: []networking.IPBlock{ +// { +// CIDR: "1.1.0.0/16", +// Except: []string{"1.1.1.0/24"}, +// }, +// { +// CIDR: "192.168.0.1/32", +// Except: []string{"192.168.0.1/32"}, +// }, +// }, +// Ports: []networking.NetworkPolicyPort{ +// { +// Protocol: nil, +// Port: &port1, +// }, +// { +// Protocol: &tcp, +// Port: &port2, +// }, +// { +// Protocol: &udp, +// Port: &port2, +// }, +// }, +// }, +// }, +// }, +// { +// name: "invalid test", +// Ingress: []IngressRule{ +// { +// From: []networking.IPBlock{ +// { +// CIDR: "1.1.0.0/24", +// Except: []string{"1.1.1.0/16"}, +// }, +// { +// CIDR: "192.168.0.1", +// Except: []string{"192.168.0.2"}, +// }, +// }, +// Ports: []networking.NetworkPolicyPort{ +// { +// Protocol: nil, +// Port: &invalid, +// }, +// }, +// }, +// }, +// wantErr: true, +// }, +// { +// name: "invalid port", +// Ingress: []IngressRule{ +// { +// From: []networking.IPBlock{ +// { +// CIDR: "1.1.0.0/24", +// }, +// }, +// Ports: []networking.NetworkPolicyPort{ +// { +// Protocol: &tcp, +// Port: &invalidPort, +// }, +// }, +// }, +// }, +// wantErr: true, +// }, +// } +// for _, tt := range tests { +// tt := tt +// t.Run(tt.name, func(t *testing.T) { +// p := &PolicySpec{ +// Ingress: tt.Ingress, +// Egress: tt.Egress, +// } +// if err := p.Validate(); (err != nil) != tt.wantErr { +// t.Errorf("PolicySpec.Validate() error = %v, wantErr %v", err, tt.wantErr) +// } +// }) +// } +// } + +// func TestFQDNSelector_GetRegex(t *testing.T) { +// tests := []struct { +// name string +// selector FQDNSelector +// expectedRegex string +// }{ +// { +// name: "match all cases", +// selector: FQDNSelector{ +// MatchPattern: "*", +// }, +// expectedRegex: "(^(" + allowedDNSCharsREGroup + "+[.])+$)|(^[.]$)", +// }, +// { +// name: "selector with match-all and literal", +// selector: FQDNSelector{ +// MatchPattern: "*.com", +// }, +// expectedRegex: "^" + allowedDNSCharsREGroup + "*[.]com[.]$", +// }, +// { +// name: "selector with match-all at the end", +// selector: FQDNSelector{ +// MatchPattern: "example.*", +// }, +// expectedRegex: "^example[.]" + allowedDNSCharsREGroup + "*[.]$", +// }, +// { +// name: "selector with static value", +// selector: FQDNSelector{ +// MatchPattern: "example.com", +// }, +// expectedRegex: "^example[.]com[.]$", +// }, +// } +// for _, tt := range tests { +// tt := tt +// t.Run(tt.name, func(t *testing.T) { +// if r := tt.selector.GetRegex(); tt.expectedRegex != r { +// t.Errorf("FQDNSelector.GetRegex returned %s, expected %s", r, tt.expectedRegex) +// } +// }) +// } +// } diff --git a/cmd/webhook/cmd.go b/cmd/webhook/cmd.go new file mode 100644 index 00000000..8071e5d3 --- /dev/null +++ b/cmd/webhook/cmd.go @@ -0,0 +1,81 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/go-logr/zapr" + v1 "github.com/metal-stack/firewall-controller/api/v1" + "github.com/metal-stack/firewall-controller/api/v1/defaults" + "github.com/metal-stack/firewall-controller/api/v1/validation" + "github.com/metal-stack/firewall-controller/pkg/logger" + "github.com/metal-stack/v" + ctrl "sigs.k8s.io/controller-runtime" +) + +var ( + setupLog = ctrl.Log.WithName("setup") +) + +func main() { + var ( + logLevel string + isVersion bool + metricsAddr string + healthAddr string + certDir string + enableLeaderElection bool + ) + + flag.StringVar(&logLevel, "log-level", "info", "The log level of the webhook") + flag.BoolVar(&isVersion, "v", false, "Show version") + flag.StringVar(&metricsAddr, "metrics-addr", ":2112", "the address the metric endpoint binds to") + flag.StringVar(&healthAddr, "health-addr", ":8081", "the address the health endpoint binds to") + flag.StringVar(&certDir, "cert-dir", "", "The directory that contains the server key and certificate for the webhook server") + flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, + "Enable leader election for controller manager. "+ + "Enabling this will ensure there is only one active controller manager") + + flag.Parse() + + if isVersion { + fmt.Println(v.V.String()) + return + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + MetricsBindAddress: metricsAddr, + HealthProbeBindAddress: healthAddr, + Port: 9443, + LeaderElection: enableLeaderElection, + LeaderElectionID: "firewall-controller-webhook-leader-election", + CertDir: certDir, + }) + if err != nil { + setupLog.Error(err, "unable to create manager") + os.Exit(1) + } + + l, err := logger.NewZapLogger(logLevel) + if err != nil { + setupLog.Error(err, "unable to parse log level") + os.Exit(1) + } + log := zapr.NewLogger(l.Desugar()) + ctrl.SetLogger(log) + + err = ctrl.NewWebhookManagedBy(mgr). + For(&v1.ClusterwideNetworkPolicy{}). + WithDefaulter(defaults.NewDefaulter(log.WithName("defaulting-webhook"))). + WithValidator(validation.NewValidator(log.WithName("validating-webhook"))). + Complete() + if err != nil { + l.Fatalw("unable to create webhook", "error", err) + } + + l.Infow("starting webhook", "version", v.V) + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + l.Fatalw("problem running webhook", "error", err) + } +} diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml deleted file mode 100644 index 9cf26134..00000000 --- a/config/webhook/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -resources: -- manifests.yaml -- service.yaml - -configurations: -- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml deleted file mode 100644 index 25e21e3c..00000000 --- a/config/webhook/kustomizeconfig.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# the following config is for teaching kustomize where to look at when substituting vars. -# 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 - -varReference: -- path: metadata/annotations diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml deleted file mode 100644 index 31e0f829..00000000 --- a/config/webhook/service.yaml +++ /dev/null @@ -1,12 +0,0 @@ - -apiVersion: v1 -kind: Service -metadata: - name: webhook-service - namespace: system -spec: - ports: - - port: 443 - targetPort: 9443 - selector: - control-plane: controller-manager diff --git a/config/webhooks/manifests.yaml b/config/webhooks/manifests.yaml new file mode 100644 index 00000000..1e4ce62f --- /dev/null +++ b/config/webhooks/manifests.yaml @@ -0,0 +1,51 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-metal-stack-io-v1-clusterwide-network-policy + failurePolicy: Fail + name: metal-stack.io + rules: + - apiGroups: + - metal-stack.io + apiVersions: + - v1 + operations: + - CREATE + resources: + - clusterwidenetworkpolicy + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-metal-stack-io-v1-clusterwide-network-policy + failurePolicy: Fail + name: metal-stack.io + rules: + - apiGroups: + - metal-stack.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - clusterwidenetworkpolicy + sideEffects: None diff --git a/controllers/clusterwidenetworkpolicy_validation_controller.go b/controllers/clusterwidenetworkpolicy_validation_controller.go deleted file mode 100644 index 1d69ac61..00000000 --- a/controllers/clusterwidenetworkpolicy_validation_controller.go +++ /dev/null @@ -1,65 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - - corev1 "k8s.io/api/core/v1" - - "github.com/go-logr/logr" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - firewallv1 "github.com/metal-stack/firewall-controller/api/v1" -) - -// ClusterwideNetworkPolicyValidationReconciler validates a ClusterwideNetworkPolicy object -// +kubebuilder:rbac:groups=metal-stack.io,resources=events,verbs=create;patch -type ClusterwideNetworkPolicyValidationReconciler struct { - ShootClient client.Client - Log logr.Logger - Recorder record.EventRecorder -} - -// Validates ClusterwideNetworkPolicy object -// +kubebuilder:rbac:groups=metal-stack.io,resources=clusterwidenetworkpolicies,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=metal-stack.io,resources=clusterwidenetworkpolicies/status,verbs=get;update;patch -func (r *ClusterwideNetworkPolicyValidationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - var clusterNP firewallv1.ClusterwideNetworkPolicy - if err := r.ShootClient.Get(ctx, req.NamespacedName, &clusterNP); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - // if network policy does not belong to the namespace where clusterwide network policies are stored: - // update status with error message - if req.Namespace != firewallv1.ClusterwideNetworkPolicyNamespace { - r.Recorder.Event( - &clusterNP, - corev1.EventTypeWarning, - "Unapplicable", - fmt.Sprintf("cluster wide network policies must be defined in namespace %s otherwise they won't take effect", firewallv1.ClusterwideNetworkPolicyNamespace), - ) - return ctrl.Result{}, nil - } - - err := clusterNP.Spec.Validate() - if err != nil { - r.Recorder.Event( - &clusterNP, - corev1.EventTypeWarning, - "Unapplicable", - fmt.Sprintf("cluster wide network policy is not valid: %v", err), - ) - return ctrl.Result{}, nil - } - - return ctrl.Result{}, nil -} - -// SetupWithManager configures this controller to watch for ClusterwideNetworkPolicy CRD -func (r *ClusterwideNetworkPolicyValidationReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&firewallv1.ClusterwideNetworkPolicy{}). - Complete(r) -} diff --git a/main.go b/main.go index 8f50995f..f3f15e3d 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,6 @@ import ( "github.com/go-logr/logr" "github.com/go-logr/zapr" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" corev1 "k8s.io/api/core/v1" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" @@ -33,6 +31,7 @@ import ( firewallv1 "github.com/metal-stack/firewall-controller/api/v1" "github.com/metal-stack/firewall-controller/controllers" + "github.com/metal-stack/firewall-controller/pkg/logger" "github.com/metal-stack/firewall-controller/pkg/sysctl" "github.com/metal-stack/firewall-controller/pkg/updater" // +kubebuilder:scaffold:imports @@ -94,7 +93,7 @@ func main() { return } - l, err := newZapLogger(logLevel) + l, err := logger.NewZapLogger(logLevel) if err != nil { setupLog.Error(err, "unable to parse log level") os.Exit(1) @@ -259,14 +258,6 @@ func main() { l.Fatalw("unable to create clusterwidenetworkpolicy controller", "error", err) } - if err = (&controllers.ClusterwideNetworkPolicyValidationReconciler{ - ShootClient: shootMgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("ClusterwideNetworkPolicyValidation"), - Recorder: shootMgr.GetEventRecorderFor("FirewallController"), - }).SetupWithManager(shootMgr); err != nil { - l.Fatalw("unable to create clusterwidenetworkpolicyvalidation controller", "error", err) - } - if err = (&controllers.FirewallMonitorReconciler{ ShootClient: shootMgr.GetClient(), Log: ctrl.Log.WithName("controllers").WithName("FirewallMonitorReconciler"), @@ -310,25 +301,6 @@ func main() { } } -func newZapLogger(levelString string) (*zap.SugaredLogger, error) { - level, err := zap.ParseAtomicLevel(levelString) - if err != nil { - return nil, fmt.Errorf("unable to parse log level: %w", err) - } - - cfg := zap.NewProductionConfig() - cfg.Level = level - cfg.EncoderConfig.TimeKey = "timestamp" - cfg.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder - - l, err := cfg.Build() - if err != nil { - return nil, fmt.Errorf("can't initialize zap logger: %w", err) - } - - return l.Sugar(), nil -} - func isFirewallV2GVKPresent(config *rest.Config) error { discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(config) diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go new file mode 100644 index 00000000..ed32a534 --- /dev/null +++ b/pkg/logger/logger.go @@ -0,0 +1,27 @@ +package logger + +import ( + "fmt" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func NewZapLogger(levelString string) (*zap.SugaredLogger, error) { + level, err := zap.ParseAtomicLevel(levelString) + if err != nil { + return nil, fmt.Errorf("unable to parse log level: %w", err) + } + + cfg := zap.NewProductionConfig() + cfg.Level = level + cfg.EncoderConfig.TimeKey = "timestamp" + cfg.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder + + l, err := cfg.Build() + if err != nil { + return nil, fmt.Errorf("can't initialize zap logger: %w", err) + } + + return l.Sugar(), nil +} diff --git a/pkg/nftables/rendering.go b/pkg/nftables/rendering.go index 8f75caba..27b3b36b 100644 --- a/pkg/nftables/rendering.go +++ b/pkg/nftables/rendering.go @@ -24,11 +24,6 @@ type firewallRenderingData struct { func newFirewallRenderingData(f *Firewall) (*firewallRenderingData, error) { ingress, egress := nftablesRules{}, nftablesRules{} for ind, np := range f.clusterwideNetworkPolicies.Items { - err := np.Spec.Validate() - if err != nil { - continue - } - i, e, u := clusterwideNetworkPolicyRules(f.cache, np, f.logAcceptedConnections) ingress = append(ingress, i...) egress = append(egress, e...)