diff --git a/api/v2/config/controller.go b/api/v2/config/controller.go index f12a185..ab5ca92 100644 --- a/api/v2/config/controller.go +++ b/api/v2/config/controller.go @@ -132,9 +132,6 @@ func (c *NewControllerConfig) validate() error { if c.SeedConfig == nil { return fmt.Errorf("seed config must be specified") } - if c.SeedNamespace == "" { - return fmt.Errorf("seed namespace must be specified") - } if c.SeedAPIServerURL == "" { return fmt.Errorf("seed api server url must be specified") } diff --git a/api/v2/types_firewall.go b/api/v2/types_firewall.go index 34b18e8..373107e 100644 --- a/api/v2/types_firewall.go +++ b/api/v2/types_firewall.go @@ -74,6 +74,9 @@ type FirewallSpec struct { // EgressRules contains egress rules configured for this firewall. EgressRules []EgressRuleSNAT `json:"egressRules,omitempty"` + // InitialRuleSet is the initial firewall ruleset applied before the firewall-controller starts running. + InitialRuleSet *InitialRuleSet `json:"initialRuleSet,omitempty"` + // Interval on which rule reconciliation by the firewall-controller should happen. Interval string `json:"interval,omitempty"` // DryRun if set to true, firewall rules are not applied. For devel-purposes only. @@ -122,6 +125,46 @@ type FirewallTemplateSpec struct { Spec FirewallSpec `json:"spec,omitempty"` } +// InitialRuleSet is the initial rule set deployed on the firewall. +type InitialRuleSet struct { + // Egress rules to be deployed initially on the firewall. + Egress []EgressRule `json:"egress,omitempty"` + // Ingress rules to be deployed initially on the firewall. + Ingress []IngressRule `json:"ingress,omitempty"` +} + +// NetworkProtocol represents the kind of network protocol. +type NetworkProtocol string + +const ( + // NetworkProtocolTCP represents tcp connections. + NetworkProtocolTCP = "TCP" + // NetworkProtocolUDP represents udp connections. + NetworkProtocolUDP = "UDP" +) + +type EgressRule struct { + // Comment provides a human readable description of this rule. + Comment string `json:"comment,omitempty"` + // Ports contains all affected network ports. + Ports []int32 `json:"ports"` + // Protocol constraints the protocol this rule applies to. + Protocol NetworkProtocol `json:"protocol"` + // To source address cidrs this rule applies to. + To []string `json:"to"` +} + +type IngressRule struct { + // Comment provides a human readable description of this rule. + Comment string `json:"comment,omitempty"` + // Ports contains all affected network ports. + Ports []int32 `json:"ports"` + // Protocol constraints the protocol this rule applies to. + Protocol NetworkProtocol `json:"protocol"` + // From source address cidrs this rule applies to. + From []string `json:"from"` +} + // EgressRuleSNAT holds a Source-NAT rule type EgressRuleSNAT struct { // NetworkID is the network for which the egress rule will be configured. diff --git a/api/v2/types_utils.go b/api/v2/types_utils.go index f1356fb..9d9dcc4 100644 --- a/api/v2/types_utils.go +++ b/api/v2/types_utils.go @@ -40,7 +40,8 @@ const ( ConditionUnknown ConditionStatus = "Unknown" ) -type Conditions []Condition // nolint:recvcheck +//nolint:recvcheck +type Conditions []Condition // NewCondition creates a new condition. func NewCondition(t ConditionType, status ConditionStatus, reason, message string) Condition { diff --git a/api/v2/validation/firewalldeployment.go b/api/v2/validation/firewalldeployment.go index ab19882..eaa2215 100644 --- a/api/v2/validation/firewalldeployment.go +++ b/api/v2/validation/firewalldeployment.go @@ -49,14 +49,15 @@ func (*firewallDeploymentValidator) validateSpec(log logr.Logger, f *v2.Firewall }) if err != nil { allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), f.Selector, "")) - } - - if !selector.Empty() { - labels := labels.Set(f.Template.Labels) - if !selector.Matches(labels) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "metadata", "labels"), f.Template.Labels, "`selector` does not match template `labels`")) + } else { + if !selector.Empty() { + labels := labels.Set(f.Template.Labels) + if !selector.Matches(labels) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "metadata", "labels"), f.Template.Labels, "`selector` does not match template `labels`")) + } } } + } allErrs = append(allErrs, NewFirewallValidator(log).Instance().validateSpec(&f.Template.Spec, fldPath.Child("template").Child("spec"))...) diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index b9d9399..802ad3b 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -161,6 +161,31 @@ func (in DeviceStatsByDevice) DeepCopy() DeviceStatsByDevice { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EgressRule) DeepCopyInto(out *EgressRule) { + *out = *in + if in.Ports != nil { + in, out := &in.Ports, &out.Ports + *out = make([]int32, len(*in)) + copy(*out, *in) + } + if in.To != nil { + in, out := &in.To, &out.To + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EgressRule. +func (in *EgressRule) DeepCopy() *EgressRule { + if in == nil { + return nil + } + out := new(EgressRule) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EgressRuleSNAT) DeepCopyInto(out *EgressRuleSNAT) { *out = *in @@ -633,6 +658,11 @@ func (in *FirewallSpec) DeepCopyInto(out *FirewallSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.InitialRuleSet != nil { + in, out := &in.InitialRuleSet, &out.InitialRuleSet + *out = new(InitialRuleSet) + (*in).DeepCopyInto(*out) + } if in.DNSPort != nil { in, out := &in.DNSPort, &out.DNSPort *out = new(uint) @@ -780,6 +810,60 @@ func (in IDSStatsByDevice) DeepCopy() IDSStatsByDevice { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressRule) DeepCopyInto(out *IngressRule) { + *out = *in + if in.Ports != nil { + in, out := &in.Ports, &out.Ports + *out = make([]int32, len(*in)) + copy(*out, *in) + } + if in.From != nil { + in, out := &in.From, &out.From + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressRule. +func (in *IngressRule) DeepCopy() *IngressRule { + if in == nil { + return nil + } + out := new(IngressRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InitialRuleSet) DeepCopyInto(out *InitialRuleSet) { + *out = *in + if in.Egress != nil { + in, out := &in.Egress, &out.Egress + *out = make([]EgressRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = make([]IngressRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InitialRuleSet. +func (in *InitialRuleSet) DeepCopy() *InitialRuleSet { + if in == nil { + return nil + } + out := new(InitialRuleSet) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InterfaceStat) DeepCopyInto(out *InterfaceStat) { *out = *in diff --git a/config/crds/firewall.metal-stack.io_firewalldeployments.yaml b/config/crds/firewall.metal-stack.io_firewalldeployments.yaml index 3100ea4..57cf768 100644 --- a/config/crds/firewall.metal-stack.io_firewalldeployments.yaml +++ b/config/crds/firewall.metal-stack.io_firewalldeployments.yaml @@ -180,6 +180,75 @@ spec: Image is the os image of the firewall. An update on this field requires the recreation of the physical firewall and can therefore lead to traffic interruption for the cluster. type: string + initialRuleSet: + description: InitialRuleSet is the initial firewall ruleset + applied before the firewall-controller starts running. + properties: + egress: + description: Egress rules to be deployed initially on + the firewall. + items: + properties: + comment: + description: Comment provides a human readable description + of this rule. + type: string + ports: + description: Ports contains all affected network + ports. + items: + format: int32 + type: integer + type: array + protocol: + description: Protocol constraints the protocol this + rule applies to. + type: string + to: + description: To source address cidrs this rule applies + to. + items: + type: string + type: array + required: + - ports + - protocol + - to + type: object + type: array + ingress: + description: Ingress rules to be deployed initially on + the firewall. + items: + properties: + comment: + description: Comment provides a human readable description + of this rule. + type: string + from: + description: From source address cidrs this rule + applies to. + items: + type: string + type: array + ports: + description: Ports contains all affected network + ports. + items: + format: int32 + type: integer + type: array + protocol: + description: Protocol constraints the protocol this + rule applies to. + type: string + required: + - from + - ports + - protocol + type: object + type: array + type: object internalPrefixes: description: |- InternalPrefixes specify prefixes which are considered local to the partition or all regions. This is used for the traffic counters. diff --git a/config/crds/firewall.metal-stack.io_firewalls.yaml b/config/crds/firewall.metal-stack.io_firewalls.yaml index c82118b..8bd9940 100644 --- a/config/crds/firewall.metal-stack.io_firewalls.yaml +++ b/config/crds/firewall.metal-stack.io_firewalls.yaml @@ -135,6 +135,70 @@ spec: Image is the os image of the firewall. An update on this field requires the recreation of the physical firewall and can therefore lead to traffic interruption for the cluster. type: string + initialRuleSet: + description: InitialRuleSet is the initial firewall ruleset applied + before the firewall-controller starts running. + properties: + egress: + description: Egress rules to be deployed initially on the firewall. + items: + properties: + comment: + description: Comment provides a human readable description + of this rule. + type: string + ports: + description: Ports contains all affected network ports. + items: + format: int32 + type: integer + type: array + protocol: + description: Protocol constraints the protocol this rule + applies to. + type: string + to: + description: To source address cidrs this rule applies to. + items: + type: string + type: array + required: + - ports + - protocol + - to + type: object + type: array + ingress: + description: Ingress rules to be deployed initially on the firewall. + items: + properties: + comment: + description: Comment provides a human readable description + of this rule. + type: string + from: + description: From source address cidrs this rule applies + to. + items: + type: string + type: array + ports: + description: Ports contains all affected network ports. + items: + format: int32 + type: integer + type: array + protocol: + description: Protocol constraints the protocol this rule + applies to. + type: string + required: + - from + - ports + - protocol + type: object + type: array + type: object internalPrefixes: description: |- InternalPrefixes specify prefixes which are considered local to the partition or all regions. This is used for the traffic counters. diff --git a/config/crds/firewall.metal-stack.io_firewallsets.yaml b/config/crds/firewall.metal-stack.io_firewallsets.yaml index ae2878a..1260fb7 100644 --- a/config/crds/firewall.metal-stack.io_firewallsets.yaml +++ b/config/crds/firewall.metal-stack.io_firewallsets.yaml @@ -172,6 +172,75 @@ spec: Image is the os image of the firewall. An update on this field requires the recreation of the physical firewall and can therefore lead to traffic interruption for the cluster. type: string + initialRuleSet: + description: InitialRuleSet is the initial firewall ruleset + applied before the firewall-controller starts running. + properties: + egress: + description: Egress rules to be deployed initially on + the firewall. + items: + properties: + comment: + description: Comment provides a human readable description + of this rule. + type: string + ports: + description: Ports contains all affected network + ports. + items: + format: int32 + type: integer + type: array + protocol: + description: Protocol constraints the protocol this + rule applies to. + type: string + to: + description: To source address cidrs this rule applies + to. + items: + type: string + type: array + required: + - ports + - protocol + - to + type: object + type: array + ingress: + description: Ingress rules to be deployed initially on + the firewall. + items: + properties: + comment: + description: Comment provides a human readable description + of this rule. + type: string + from: + description: From source address cidrs this rule + applies to. + items: + type: string + type: array + ports: + description: Ports contains all affected network + ports. + items: + format: int32 + type: integer + type: array + protocol: + description: Protocol constraints the protocol this + rule applies to. + type: string + required: + - from + - ports + - protocol + type: object + type: array + type: object internalPrefixes: description: |- InternalPrefixes specify prefixes which are considered local to the partition or all regions. This is used for the traffic counters. diff --git a/controllers/deployment/controller.go b/controllers/deployment/controller.go index 6366b87..33f1c1d 100644 --- a/controllers/deployment/controller.go +++ b/controllers/deployment/controller.go @@ -31,7 +31,7 @@ func SetupWithManager(log logr.Logger, recorder record.EventRecorder, mgr ctrl.M lastSetCreation: map[string]time.Time{}, }) - return ctrl.NewControllerManagedBy(mgr). + controller := ctrl.NewControllerManagedBy(mgr). For( &v2.FirewallDeployment{}, builder.WithPredicates( @@ -57,9 +57,13 @@ func SetupWithManager(log logr.Logger, recorder record.EventRecorder, mgr ctrl.M ), ), ), - ). - WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))). - Complete(g) + ) + + if c.GetSeedNamespace() != "" { + controller = controller.WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))) + } + + return controller.Complete(g) } func SetupWebhookWithManager(log logr.Logger, mgr ctrl.Manager, c *config.ControllerConfig) error { diff --git a/controllers/deployment/infrastructure_status.go b/controllers/deployment/infrastructure_status.go index 1caa62b..93a9dcd 100644 --- a/controllers/deployment/infrastructure_status.go +++ b/controllers/deployment/infrastructure_status.go @@ -27,7 +27,7 @@ func (c *controller) updateInfrastructureStatus(r *controllers.Ctx[*v2.FirewallD }) err := c.c.GetSeedClient().Get(r.Ctx, client.ObjectKey{ - Namespace: c.c.GetSeedNamespace(), + Namespace: r.Target.Namespace, Name: infrastructureName, }, infraObj) if err != nil { @@ -129,7 +129,7 @@ func (c *controller) updateInfrastructureStatus(r *controllers.Ctx[*v2.FirewallD }) err = c.c.GetSeedClient().Get(r.Ctx, client.ObjectKey{ - Namespace: c.c.GetSeedNamespace(), + Namespace: r.Target.Namespace, Name: "acl", }, aclObj) if err != nil { @@ -150,6 +150,7 @@ func (c *controller) updateInfrastructureStatus(r *controllers.Ctx[*v2.FirewallD } func extractInfrastructureNameFromSeedNamespace(namespace string) (string, bool) { + // TODO: is this safe to not skip in the future? if !strings.HasPrefix(namespace, "shoot--") { return "", false } diff --git a/controllers/deployment/infrastructure_status_test.go b/controllers/deployment/infrastructure_status_test.go index 96fa1cb..53983b4 100644 --- a/controllers/deployment/infrastructure_status_test.go +++ b/controllers/deployment/infrastructure_status_test.go @@ -12,6 +12,7 @@ import ( "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -454,6 +455,11 @@ func Test_controller_updateInfrastructureStatus(t *testing.T) { } err = ctrl.updateInfrastructureStatus(&controllers.Ctx[*v2.FirewallDeployment]{ + Target: &v2.FirewallDeployment{ + ObjectMeta: v1.ObjectMeta{ + Namespace: testNamespace, + }, + }, Ctx: ctx, Log: log, }, "mycluster1", tt.ownedFirewalls) diff --git a/controllers/deployment/reconcile.go b/controllers/deployment/reconcile.go index 2b5625f..455d088 100644 --- a/controllers/deployment/reconcile.go +++ b/controllers/deployment/reconcile.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" v2 "github.com/metal-stack/firewall-controller-manager/api/v2" "github.com/metal-stack/firewall-controller-manager/controllers" @@ -83,7 +84,7 @@ func (c *controller) Reconcile(r *controllers.Ctx[*v2.FirewallDeployment]) error r.Log.Info("swapped latest set to shortest distance", "distance", v2.FirewallShortestDistance) } - infrastructureName, ok := extractInfrastructureNameFromSeedNamespace(c.c.GetSeedNamespace()) + infrastructureName, ok := extractInfrastructureNameFromSeedNamespace(r.Target.Namespace) if ok { var ownedFirewalls []*v2.Firewall for _, set := range ownedSets { @@ -166,6 +167,12 @@ func (c *controller) createFirewallSet(r *controllers.Ctx[*v2.FirewallDeployment }, } + if r.Target.Annotations != nil { + if val, ok := r.Target.Annotations[v2.FirewallNoControllerConnectionAnnotation]; ok { + set.Annotations[v2.FirewallNoControllerConnectionAnnotation] = val + } + } + err = c.c.GetSeedClient().Create(r.Ctx, set, &client.CreateOptions{}) if err != nil { cond := v2.NewCondition(v2.FirewallDeplomentProgressing, v2.ConditionFalse, "FirewallSetCreateError", fmt.Sprintf("Error creating firewall set: %s.", err)) @@ -242,5 +249,11 @@ func (c *controller) isNewSetRequired(r *controllers.Ctx[*v2.FirewallDeployment] return true } + // TODO: improve and write tests + if !cmp.Equal(oldS.InitialRuleSet, newS.InitialRuleSet) { + r.Log.Info("firewall initial rule set have changed") + return true + } + return false } diff --git a/controllers/firewall/controller.go b/controllers/firewall/controller.go index 6c18b9a..3ab23fd 100644 --- a/controllers/firewall/controller.go +++ b/controllers/firewall/controller.go @@ -88,7 +88,7 @@ func SetupWithManager(log logr.Logger, recorder record.EventRecorder, mgr ctrl.M }), }) - return ctrl.NewControllerManagedBy(mgr). + controller := ctrl.NewControllerManagedBy(mgr). For( &v2.Firewall{}, builder.WithPredicates( @@ -99,9 +99,13 @@ func SetupWithManager(log logr.Logger, recorder record.EventRecorder, mgr ctrl.M ), ). // don't think about owning the firewall monitor here, it's in the shoot cluster, we cannot watch two clusters with controller-runtime - Named("Firewall"). - WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))). - Complete(g) + Named("Firewall") + + if c.GetSeedNamespace() != "" { + controller = controller.WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))) + } + + return controller.Complete(g) } func SetupWebhookWithManager(log logr.Logger, mgr ctrl.Manager, c *config.ControllerConfig) error { diff --git a/controllers/firewall/reconcile.go b/controllers/firewall/reconcile.go index 89c07bc..d3ba472 100644 --- a/controllers/firewall/reconcile.go +++ b/controllers/firewall/reconcile.go @@ -46,7 +46,7 @@ func (c *controller) Reconcile(r *controllers.Ctx[*v2.Firewall]) error { return err } - // requeueing in order to continue checking progression + // requeuing in order to continue checking progression return controllers.RequeueAfter(10*time.Second, "firewall creation is progressing") case 1: f = fws[0] @@ -142,18 +142,42 @@ func (c *controller) createFirewall(r *controllers.Ctx[*v2.Firewall]) (*models.V tags = append(tags, v2.FirewallSetTag(ref.Name)) } + var rules *models.V1FirewallRules + if r.Target.Spec.InitialRuleSet != nil { + rules = &models.V1FirewallRules{} + + for _, rule := range r.Target.Spec.InitialRuleSet.Egress { + rules.Egress = append(rules.Egress, &models.V1FirewallEgressRule{ + Comment: rule.Comment, + Ports: rule.Ports, + Protocol: string(rule.Protocol), + To: rule.To, + }) + } + + for _, rule := range r.Target.Spec.InitialRuleSet.Ingress { + rules.Ingress = append(rules.Ingress, &models.V1FirewallIngressRule{ + Comment: rule.Comment, + From: rule.From, + Ports: rule.Ports, + Protocol: string(rule.Protocol), + }) + } + } + createRequest := &models.V1FirewallCreateRequest{ - Description: "created by firewall-controller-manager", - Name: r.Target.Name, - Hostname: r.Target.Name, - Sizeid: &r.Target.Spec.Size, - Projectid: &r.Target.Spec.Project, - Partitionid: &r.Target.Spec.Partition, - Imageid: &r.Target.Spec.Image, - SSHPubKeys: r.Target.Spec.SSHPublicKeys, - Networks: networks, - UserData: r.Target.Spec.Userdata, - Tags: tags, + Description: "created by firewall-controller-manager", + Name: r.Target.Name, + Hostname: r.Target.Name, + Sizeid: &r.Target.Spec.Size, + Projectid: &r.Target.Spec.Project, + Partitionid: &r.Target.Spec.Partition, + Imageid: &r.Target.Spec.Image, + SSHPubKeys: r.Target.Spec.SSHPublicKeys, + Networks: networks, + UserData: r.Target.Spec.Userdata, + Tags: tags, + FirewallRules: rules, } resp, err := c.c.GetMetal().Firewall().AllocateFirewall(firewall.NewAllocateFirewallParams().WithBody(createRequest).WithContext(r.Ctx), nil) diff --git a/controllers/generic_controller.go b/controllers/generic_controller.go index 0d60c16..2afbc3c 100644 --- a/controllers/generic_controller.go +++ b/controllers/generic_controller.go @@ -74,7 +74,7 @@ func (g *GenericController[O]) logger(req ctrl.Request) logr.Logger { } func (g GenericController[O]) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - if req.Namespace != g.namespace { // should already be filtered out through predicate, but we will check anyway + if g.namespace != "" && req.Namespace != g.namespace { // should already be filtered out through predicate, but we will check anyway return ctrl.Result{}, nil } diff --git a/controllers/monitor/controller.go b/controllers/monitor/controller.go index cc5d5ee..c84c2c4 100644 --- a/controllers/monitor/controller.go +++ b/controllers/monitor/controller.go @@ -22,7 +22,7 @@ func SetupWithManager(log logr.Logger, mgr ctrl.Manager, c *config.ControllerCon c: c, }).WithoutStatus() - return ctrl.NewControllerManagedBy(mgr). + controller := ctrl.NewControllerManagedBy(mgr). For(&v2.FirewallMonitor{}, builder.WithPredicates( predicate.Not( @@ -30,9 +30,13 @@ func SetupWithManager(log logr.Logger, mgr ctrl.Manager, c *config.ControllerCon ), ), ). - Named("FirewallMonitor"). - WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetShootNamespace()))). - Complete(g) + Named("FirewallMonitor") + + if c.GetSeedNamespace() != "" { + controller = controller.WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))) + } + + return controller.Complete(g) } func (c *controller) New() *v2.FirewallMonitor { diff --git a/controllers/monitor/reconcile.go b/controllers/monitor/reconcile.go index 4bcf1cb..0c5a9b8 100644 --- a/controllers/monitor/reconcile.go +++ b/controllers/monitor/reconcile.go @@ -3,6 +3,7 @@ package monitor import ( "context" "fmt" + "slices" "strconv" "time" @@ -34,17 +35,22 @@ func (c *controller) Reconcile(r *controllers.Ctx[*v2.FirewallMonitor]) error { } func (c *controller) updateFirewallStatus(r *controllers.Ctx[*v2.FirewallMonitor]) (*v2.Firewall, error) { - fw := &v2.Firewall{ - ObjectMeta: metav1.ObjectMeta{ - Name: r.Target.Name, - Namespace: c.c.GetSeedNamespace(), - }, - } - err := c.c.GetSeedClient().Get(r.Ctx, client.ObjectKeyFromObject(fw), fw) + fws := &v2.FirewallList{} + + err := c.c.GetSeedClient().List(r.Ctx, fws) if err != nil { + return nil, fmt.Errorf("unable to list firewalls: %w", err) + } + + idx := slices.IndexFunc(fws.Items, func(fw v2.Firewall) bool { + return fw.Name == r.Target.Name // TODO: not sure if this is safe to do? + }) + if idx < 0 { return nil, fmt.Errorf("associated firewall of monitor not found: %w", err) } + fw := &fws.Items[idx] + old := fw.DeepCopy() firewall.SetFirewallStatusFromMonitor(fw, r.Target) @@ -72,7 +78,7 @@ func (c *controller) rollSetAnnotation(r *controllers.Ctx[*v2.FirewallMonitor]) fw := &v2.Firewall{ ObjectMeta: metav1.ObjectMeta{ Name: r.Target.Name, - Namespace: c.c.GetSeedNamespace(), + Namespace: r.Target.Namespace, }, } diff --git a/controllers/set/controller.go b/controllers/set/controller.go index 06909c5..426a2ee 100644 --- a/controllers/set/controller.go +++ b/controllers/set/controller.go @@ -26,7 +26,7 @@ func SetupWithManager(log logr.Logger, recorder record.EventRecorder, mgr ctrl.M c: c, }) - return ctrl.NewControllerManagedBy(mgr). + controller := ctrl.NewControllerManagedBy(mgr). For( &v2.FirewallSet{}, builder.WithPredicates( @@ -47,9 +47,13 @@ func SetupWithManager(log logr.Logger, recorder record.EventRecorder, mgr ctrl.M ), ), ), - ). - WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))). - Complete(g) + ) + + if c.GetSeedNamespace() != "" { + controller = controller.WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))) + } + + return controller.Complete(g) } func SetupWebhookWithManager(log logr.Logger, mgr ctrl.Manager, c *config.ControllerConfig) error { diff --git a/controllers/set/reconcile.go b/controllers/set/reconcile.go index 6591ea3..c48d958 100644 --- a/controllers/set/reconcile.go +++ b/controllers/set/reconcile.go @@ -157,6 +157,15 @@ func (c *controller) createFirewall(r *controllers.Ctx[*v2.FirewallSet]) (*v2.Fi meta.Annotations[v2.FirewallNoControllerConnectionAnnotation] = "true" } + if r.Target.Annotations != nil { + if val, ok := r.Target.Annotations[v2.FirewallNoControllerConnectionAnnotation]; ok { + if meta.Annotations == nil { + meta.Annotations = map[string]string{} + } + meta.Annotations[v2.FirewallNoControllerConnectionAnnotation] = val + } + } + fw := &v2.Firewall{ ObjectMeta: *meta, Spec: r.Target.Spec.Template.Spec, diff --git a/controllers/update/controller.go b/controllers/update/controller.go index 991adde..3b4ba53 100644 --- a/controllers/update/controller.go +++ b/controllers/update/controller.go @@ -34,16 +34,20 @@ func SetupWithManager(log logr.Logger, recorder record.EventRecorder, mgr ctrl.M imageCache: newImageCache(c.GetMetal()), }).WithoutStatus() - return ctrl.NewControllerManagedBy(mgr). + controller := ctrl.NewControllerManagedBy(mgr). For( &v2.FirewallDeployment{}, builder.WithPredicates( v2.AnnotationAddedPredicate(v2.MaintenanceAnnotation), ), ). - Named("FirewallDeployment"). - WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))). - Complete(g) + Named("FirewallDeployment") + + if c.GetSeedNamespace() != "" { + controller = controller.WithEventFilter(predicate.NewPredicateFuncs(controllers.SkipOtherNamespace(c.GetSeedNamespace()))) + } + + return controller.Complete(g) } func (c *controller) New() *v2.FirewallDeployment { diff --git a/main.go b/main.go index f1b9f67..c1f13fa 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,7 @@ func main() { shootTokenSecret string shootTokenPath string sshKeySecret string + sshKeySecretNamespace string namespace string gracefulShutdownTimeout time.Duration reconcileInterval time.Duration @@ -72,7 +73,7 @@ func main() { 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.StringVar(&namespace, "namespace", "default", "the namespace this controller is running") + flag.StringVar(&namespace, "namespace", "", "the namespace this controller is running") flag.DurationVar(&reconcileInterval, "reconcile-interval", 10*time.Minute, "duration after which a resource is getting reconciled at minimum") flag.DurationVar(&firewallHealthTimeout, "firewall-health-timeout", 20*time.Minute, "duration after a created firewall not getting ready is considered dead") flag.DurationVar(&createTimeout, "create-timeout", 10*time.Minute, "duration after which a firewall in the creation phase will be recreated") @@ -88,10 +89,15 @@ func main() { flag.StringVar(&shootKubeconfigSecret, "shoot-kubeconfig-secret-name", "", "the secret name of the generic kubeconfig for shoot access") flag.StringVar(&shootTokenSecret, "shoot-token-secret-name", "", "the secret name of the token for shoot access") flag.StringVar(&sshKeySecret, "ssh-key-secret-name", "", "the secret name of the ssh key for machine access") + flag.StringVar(&sshKeySecretNamespace, "ssh-key-secret-namespace", "", "the secret name of the ssh key for machine access") flag.StringVar(&shootTokenPath, "shoot-token-path", "", "the path where to store the token file for shoot access") flag.Parse() + if sshKeySecretNamespace == "" { + sshKeySecretNamespace = namespace + } + slogHandler, err := controllers.NewLogger(logLevel) if err != nil { ctrl.Log.WithName("setup").Error(err, "unable to parse log level") @@ -110,7 +116,7 @@ func main() { log.Fatalf("unable to create metal client %v", err) } - seedMgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + mgrConfig := ctrl.Options{ Scheme: scheme, Metrics: server.Options{ BindAddress: metricsAddr, @@ -121,15 +127,21 @@ func main() { }), Cache: cache.Options{ SyncPeriod: &reconcileInterval, - DefaultNamespaces: map[string]cache.Config{ - namespace: {}, - }, }, HealthProbeBindAddress: healthAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "firewall-controller-manager-leader-election", GracefulShutdownTimeout: &gracefulShutdownTimeout, - }) + } + + if namespace != "" { + l.Info("running in dedicated namespace only", "namespace", namespace) + mgrConfig.Cache.DefaultNamespaces = map[string]cache.Config{ + namespace: {}, + } + } + + seedMgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrConfig) if err != nil { log.Fatalf("unable to setup firewall-controller-manager %v", err) } @@ -196,7 +208,7 @@ func main() { // secret for this controller and expose the access secrets through the firewall // status resource, which can be read by the firewall-controller // - the firewall-controller can then create a client from these secrets but - // it has to contiuously update the token file because the token will expire + // it has to continuously update the token file because the token will expire // - we can re-use the same approach for this controller as well and do not have // to do any additional mounts for the deployment of the controller // @@ -247,7 +259,7 @@ func main() { ShootAPIServerURL: shootApiURL, ShootAccess: externalShootAccess, SSHKeySecretName: sshKeySecret, - SSHKeySecretNamespace: namespace, + SSHKeySecretNamespace: sshKeySecretNamespace, ShootAccessHelper: internalShootAccessHelper, Metal: mclient, ClusterTag: fmt.Sprintf("%s=%s", tag.ClusterID, clusterID),