diff --git a/README.md b/README.md index 18204c65..48d65d98 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,8 @@ Annotation (Suffix) | Values | Default | Description `nodebalancer-id` | string | | The ID of the NodeBalancer to front the service. When not specified, a new NodeBalancer will be created. This can be configured on service creation or patching `hostname-only-ingress` | [bool](#annotation-bool-values) | `false` | When `true`, the LoadBalancerStatus for the service will only contain the Hostname. This is useful for bypassing kube-proxy's rerouting of in-cluster requests originally intended for the external LoadBalancer to the service's constituent pod IPs. `tags` | string | | A comma seperated list of tags to be applied to the createad NodeBalancer instance -`firewall-id` | string | | The Firewall ID that's applied to the NodeBalancer instance. +`firewall-id` | string | | An existing Cloud Firewall ID to be attached to the NodeBalancer instance. See [Firewalls](#firewalls). +`firewall-acl` | string | | The Firewall rules to be applied to the NodeBalancer. Adding this annotation creates a new CCM managed Linode CloudFirewall instance. See [Firewalls](#firewalls). #### Deprecated Annotations These annotations are deprecated, and will be removed in a future release. @@ -77,6 +78,56 @@ Key | Values | Default | Description `proxy-protocol` | `none`, `v1`, `v2` | `none` | Specifies whether to use a version of Proxy Protocol on the underlying NodeBalancer. Overwrites `default-proxy-protocol`. `tls-secret-name` | string | | Specifies a secret to use for TLS. The secret type should be `kubernetes.io/tls`. +#### Firewalls +Firewall rules can be applied to the CCM Managed NodeBalancers in two distinct ways. + +##### CCM Managed Firewall +To use this feature, ensure that the linode api token used with the ccm has the `add_firewalls` grant. + +The CCM accepts firewall ACLs in json form. The ACL can either be an `allowList` or a `denyList`. Supplying both is not supported. Supplying neither is not supported. The `allowList` sets up a CloudFirewall that `ACCEPT`s traffic only from the specified IPs/CIDRs and `DROP`s everything else. The `denyList` sets up a CloudFirewall that `DROP`s traffic only from the specified IPs/CIDRs and `ACCEPT`s everything else. Ports are automatically inferred from the service configuration. + +See [Firewall rules](https://www.linode.com/docs/api/networking/#firewall-create__request-body-schema) for more details on how to specify the IPs/CIDRs + +Example usage of an ACL to allow traffic from a specific set of addresses + +```yaml +kind: Service +apiVersion: v1 +metadata: + name: https-lb + annotations: + service.beta.kubernetes.io/linode-loadbalancer-firewall-acl: | + { + "allowList": { + "ipv4": ["192.166.0.0/16", "172.23.41.0/24"], + "ipv6": ["2001:DB8::/128"] + }, + } +spec: + type: LoadBalancer + selector: + app: nginx-https-example + ports: + - name: http + protocol: TCP + port: 80 + targetPort: http + - name: https + protocol: TCP + port: 443 + targetPort: https +``` + + +##### User Managed Firewall +Users can create CloudFirewall instances, supply their own rules and attach them to the NodeBalancer. To do so, set the +`service.beta.kubernetes.io/linode-loadbalancer-firewall-id` annotation to the ID of the cloud firewall. The CCM does not manage the lifecycle of the CloudFirewall Instance in this case. Users are responsible for ensuring the policies are correct. + +**Note**
+If the user supplies a firewall-id, and later switches to using an ACL, the CCM will take over the CloudFirewall Instance. To avoid this, delete the service, and re-create it so the original CloudFirewall is left undisturbed. + + + ### Nodes Kubernetes Nodes can be configured with the following annotations. diff --git a/cloud/linode/annotations.go b/cloud/linode/annotations.go index a4185e2f..18c0f874 100644 --- a/cloud/linode/annotations.go +++ b/cloud/linode/annotations.go @@ -27,6 +27,7 @@ const ( annLinodeHostnameOnlyIngress = "service.beta.kubernetes.io/linode-loadbalancer-hostname-only-ingress" annLinodeLoadBalancerTags = "service.beta.kubernetes.io/linode-loadbalancer-tags" annLinodeCloudFirewallID = "service.beta.kubernetes.io/linode-loadbalancer-firewall-id" + annLinodeCloudFirewallACL = "service.beta.kubernetes.io/linode-loadbalancer-firewall-acl" annLinodeNodePrivateIP = "node.k8s.linode.com/private-ip" annLinodeHostUUID = "node.k8s.linode.com/host-uuid" diff --git a/cloud/linode/client.go b/cloud/linode/client.go index 1734366c..a2fcde09 100644 --- a/cloud/linode/client.go +++ b/cloud/linode/client.go @@ -32,6 +32,8 @@ type Client interface { CreateFirewallDevice(ctx context.Context, firewallID int, opts linodego.FirewallDeviceCreateOptions) (*linodego.FirewallDevice, error) CreateFirewall(ctx context.Context, opts linodego.FirewallCreateOptions) (*linodego.Firewall, error) DeleteFirewall(ctx context.Context, fwid int) error + GetFirewall(context.Context, int) (*linodego.Firewall, error) + UpdateFirewallRules(context.Context, int, linodego.FirewallRuleSet) (*linodego.FirewallRuleSet, error) } // linodego.Client implements Client diff --git a/cloud/linode/fake_linode_test.go b/cloud/linode/fake_linode_test.go index 43841532..d3a37e61 100644 --- a/cloud/linode/fake_linode_test.go +++ b/cloud/linode/fake_linode_test.go @@ -24,8 +24,8 @@ type fakeAPI struct { nb map[string]*linodego.NodeBalancer nbc map[string]*linodego.NodeBalancerConfig nbn map[string]*linodego.NodeBalancerNode - fw map[int]*linodego.Firewall - fwd map[int]map[int]*linodego.FirewallDevice + fw map[int]*linodego.Firewall // map of firewallID -> firewall + fwd map[int]map[int]*linodego.FirewallDevice // map of firewallID -> firewallDeviceID:FirewallDevice requests map[fakeRequest]struct{} } @@ -186,12 +186,15 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { }, Data: data, } - rr, _ := json.Marshal(resp) + rr, err := json.Marshal(resp) + if err != nil { + f.t.Fatal(err) + } _, _ = w.Write(rr) return } - rx, _ = regexp.Compile("/nodebalancers/[0-9]+/firewalls") + rx = regexp.MustCompile("/nodebalancers/[0-9]+/firewalls") if rx.MatchString(urlPath) { id := strings.Split(urlPath, "/")[2] devID, err := strconv.Atoi(id) @@ -726,13 +729,47 @@ func (f *fakeAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) { rr, _ := json.Marshal(resp) _, _ = w.Write(rr) + } else if strings.Contains(urlPath, "firewalls") { + // path is networking/firewalls/%d/rules + parts := strings.Split(urlPath[1:], "/") + fwrs := new(linodego.FirewallRuleSet) + if err := json.NewDecoder(r.Body).Decode(fwrs); err != nil { + f.t.Fatal(err) + } + + fwID, err := strconv.Atoi(parts[2]) + if err != nil { + f.t.Fatal(err) + } + + if firewall, found := f.fw[fwID]; found { + firewall.Rules.Inbound = fwrs.Inbound + firewall.Rules.InboundPolicy = fwrs.InboundPolicy + // outbound rules do not apply, ignoring. + f.fw[fwID] = firewall + resp, err := json.Marshal(firewall) + if err != nil { + f.t.Fatal(err) + } + _, _ = w.Write(resp) + return + } + + w.WriteHeader(404) + resp := linodego.APIError{ + Errors: []linodego.APIErrorReason{ + {Reason: "Not Found"}, + }, + } + rr, _ := json.Marshal(resp) + _, _ = w.Write(rr) } } } func createFirewallDevice(fwId int, f *fakeAPI, fdco linodego.FirewallDeviceCreateOptions) linodego.FirewallDevice { fwd := linodego.FirewallDevice{ - ID: rand.Intn(9999), + ID: fdco.ID, Entity: linodego.FirewallDeviceEntity{ ID: fdco.ID, Type: fdco.Type, diff --git a/cloud/linode/loadbalancers.go b/cloud/linode/loadbalancers.go index fbbbe6a4..7270ed8a 100644 --- a/cloud/linode/loadbalancers.go +++ b/cloud/linode/loadbalancers.go @@ -12,6 +12,8 @@ import ( "strings" "time" + "golang.org/x/exp/slices" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -24,7 +26,11 @@ import ( "github.com/linode/linodego" ) -var errNoNodesAvailable = errors.New("no nodes available for nodebalancer") +var ( + errNoNodesAvailable = errors.New("No nodes available for nodebalancer") + errInvalidFWConfig = errors.New("Specify either an allowList or a denyList for a firewall") + errTooManyFirewalls = errors.New("Too many firewalls attached to a nodebalancer") +) type lbNotFoundError struct { serviceNn string @@ -39,9 +45,8 @@ func (e lbNotFoundError) Error() string { } type loadbalancers struct { - client Client - zone string - + client Client + zone string kubeClient kubernetes.Interface } @@ -226,9 +231,9 @@ func (l *loadbalancers) EnsureLoadBalancer(ctx context.Context, clusterName stri return lbStatus, nil } -// getNodeBalancerDeviceId gets the deviceID of the nodeBalancer that is attached to the firewall. -func (l *loadbalancers) getNodeBalancerDeviceId(ctx context.Context, firewallID, nbID, page int) (int, bool, error) { - devices, err := l.client.ListFirewallDevices(ctx, firewallID, &linodego.ListOptions{PageSize: 500, PageOptions: &linodego.PageOptions{Page: page}}) +// getNodeBalancerDeviceID gets the deviceID of the nodeBalancer that is attached to the firewall. +func (l *loadbalancers) getNodeBalancerDeviceID(ctx context.Context, firewallID, nbID int) (int, bool, error) { + devices, err := l.client.ListFirewallDevices(ctx, firewallID, &linodego.ListOptions{}) if err != nil { return 0, false, err } @@ -246,55 +251,49 @@ func (l *loadbalancers) getNodeBalancerDeviceId(ctx context.Context, firewallID, return 0, false, nil } -// updateNodeBalancerFirewall updates the firewall attached to the nodebalancer -// -// Firewall is updated in 3 scenario's: -// - Add new firewall to the nodeBalancer -// - Update firewall attached to the nodebalancer -// - Remove firewall attached -func (l *loadbalancers) updateNodeBalancerFirewall(ctx context.Context, service *v1.Service, nb *linodego.NodeBalancer) error { +// Updates a service that has a firewallID annotation set. +// If an annotation is set, and the nodebalancer has a firewall that matches the ID, nothing to do +// If there's more than one firewall attached to the node-balancer, an error is returned as its not a supported use case. +// If there's only one firewall attached and it doesn't match what's in the annotation, the new firewall is attached and the old one removed +func (l *loadbalancers) updateFirewallwithID(ctx context.Context, service *v1.Service, nb *linodego.NodeBalancer) error { var newFirewallID int var err error - // get the new firewall id form the annotaiton (if any). - fwid, ok := service.GetAnnotations()[annLinodeCloudFirewallID] - if ok { - newFirewallID, err = strconv.Atoi(fwid) - if err != nil { - return err - } + fwID := service.GetAnnotations()[annLinodeCloudFirewallID] + newFirewallID, err = strconv.Atoi(fwID) + if err != nil { + return err } - // get the list of attached firewalls to the node-balancer. + // See if a firewall is attached to the nodebalancer first. firewalls, err := l.client.ListNodeBalancerFirewalls(ctx, nb.ID, &linodego.ListOptions{}) if err != nil { - if !ok { - if err.Error() != "[404] Not Found" { - return err - } else { - return nil - } - } + return err } - - // if there are no attached firewalls and no annotation added; return. - if !ok && len(firewalls) == 0 { - return nil + if len(firewalls) > 1 { + klog.Errorf("Found more than one firewall attached to nodebalancer: %d, firewall IDs: %v", nb.ID, firewalls) + return errTooManyFirewalls } - // get the ID of the firewall that is already attached to the nodeBalancer. + // get the ID of the firewall that is already attached to the nodeBalancer, if we have one. var existingFirewallID int - if len(firewalls) != 0 { + if len(firewalls) == 1 { existingFirewallID = firewalls[0].ID } - // if existing firewall and new firewall differs, update accordingly. + // if existing firewall and new firewall differs, attach the new firewall and remove the old. if existingFirewallID != newFirewallID { - + // attach new firewall. + _, err = l.client.CreateFirewallDevice(ctx, newFirewallID, linodego.FirewallDeviceCreateOptions{ + ID: nb.ID, + Type: "nodebalancer", + }) + if err != nil { + return err + } // remove the existing firewall if it exists if existingFirewallID != 0 { - - deviceID, deviceExists, err := l.getNodeBalancerDeviceId(ctx, existingFirewallID, nb.ID, 1) + deviceID, deviceExists, err := l.getNodeBalancerDeviceID(ctx, existingFirewallID, nb.ID) if err != nil { return err } @@ -308,10 +307,99 @@ func (l *loadbalancers) updateNodeBalancerFirewall(ctx context.Context, service return err } } + } + return nil +} + +func ipsChanged(ips *linodego.NetworkAddresses, rules []linodego.FirewallRule) bool { + var ruleIPv4s []string + var ruleIPv6s []string + + for _, rule := range rules { + if rule.Addresses.IPv4 != nil { + ruleIPv4s = append(ruleIPv4s, *rule.Addresses.IPv4...) + } + if rule.Addresses.IPv6 != nil { + ruleIPv6s = append(ruleIPv6s, *rule.Addresses.IPv6...) + } + } + + if len(ruleIPv4s) > 0 && ips.IPv4 == nil { + return true + } + + if len(ruleIPv6s) > 0 && ips.IPv6 == nil { + return true + } - // attach new firewall if an ID is provided in the annotation. - if newFirewallID != 0 { - _, err = l.client.CreateFirewallDevice(ctx, newFirewallID, linodego.FirewallDeviceCreateOptions{ + if ips.IPv4 != nil { + for _, ipv4 := range *ips.IPv4 { + if !slices.Contains(ruleIPv4s, ipv4) { + return true + } + } + } + + if ips.IPv6 != nil { + for _, ipv6 := range *ips.IPv6 { + if !slices.Contains(ruleIPv6s, ipv6) { + return true + } + } + } + + return false +} + +func firewallRuleChanged(old linodego.FirewallRuleSet, newACL aclConfig) bool { + var ips *linodego.NetworkAddresses + if newACL.AllowList != nil { + // this is a allowList, this means that the rules should have `DROP` as inboundpolicy + if old.InboundPolicy != "DROP" { + return true + } + if (newACL.AllowList.IPv4 != nil || newACL.AllowList.IPv6 != nil) && len(old.Inbound) == 0 { + return true + } + ips = newACL.AllowList + } + + if newACL.DenyList != nil { + if old.InboundPolicy != "ACCEPT" { + return true + } + + if (newACL.DenyList.IPv4 != nil || newACL.DenyList.IPv6 != nil) && len(old.Inbound) == 0 { + return true + } + ips = newACL.DenyList + } + + return ipsChanged(ips, old.Inbound) +} + +func (l *loadbalancers) updateFWwithACL(ctx context.Context, service *v1.Service, nb *linodego.NodeBalancer) error { + // See if a firewall is attached to the nodebalancer first. + firewalls, err := l.client.ListNodeBalancerFirewalls(ctx, nb.ID, &linodego.ListOptions{}) + if err != nil { + return err + } + + switch len(firewalls) { + case 0: + { + // need to create a fw and attach it to our nb + fwcreateOpts, err := l.createFirewallOptsForSvc(l.GetLoadBalancerName(ctx, "", service), l.getLoadBalancerTags(ctx, "", service), service) + if err != nil { + return err + } + + fw, err := l.client.CreateFirewall(ctx, *fwcreateOpts) + if err != nil { + return err + } + // attach new firewall. + _, err = l.client.CreateFirewallDevice(ctx, fw.ID, linodego.FirewallDeviceCreateOptions{ ID: nb.ID, Type: "nodebalancer", }) @@ -319,7 +407,95 @@ func (l *loadbalancers) updateNodeBalancerFirewall(ctx context.Context, service return err } } + case 1: + { + // We do not want to get into the complexity of reconciling differences, might as well just pull what's in the svc annotation now and update the fw. + var acl aclConfig + err := json.Unmarshal([]byte(service.GetAnnotations()[annLinodeCloudFirewallACL]), &acl) + if err != nil { + return err + } + + changed := firewallRuleChanged(firewalls[0].Rules, acl) + if !changed { + return nil + } + + fwCreateOpts, err := l.createFirewallOptsForSvc(service.Name, []string{""}, service) + if err != nil { + return err + } + _, err = l.client.UpdateFirewallRules(ctx, firewalls[0].ID, fwCreateOpts.Rules) + if err != nil { + return err + } + } + default: + klog.Errorf("Found more than one firewall attached to nodebalancer: %d, firewall IDs: %v", nb.ID, firewalls) + return errTooManyFirewalls + } + return nil +} + +// updateNodeBalancerFirewall reconciles the firewall attached to the nodebalancer +// +// This function does the following +// 1. If a firewallID annotation is present, it checks if the nodebalancer has a firewall attached, and if it matches the annotationID +// a. If the IDs match, nothing to do here. +// b. If they don't match, the nb is attached to the new firewall and removed from the old one. +// 2. If a firewallACL annotation is present, +// a. it checks if the nodebalancer has a firewall attached, if a fw exists, it updates rules +// b. if a fw does not exist, it creates one +// 3. If neither of these annotations are present, +// a. AND if no firewalls are attached to the nodebalancer, nothing to do. +// b. if the NB has ONE firewall attached, remove it from nb, and clean up if nothing else is attached to it +// c. If there are more than one fw attached to it, then its a problem, return an err +// 4. If both these annotations are present, the firewallID takes precedence, and the ACL annotation is ignored. +// IF a user creates a fw ID externally, and then switches to using a ACL, the CCM will take over the fw that's attached to the nodebalancer. + +func (l *loadbalancers) updateNodeBalancerFirewall(ctx context.Context, service *v1.Service, nb *linodego.NodeBalancer) error { + // get the new firewall id from the annotation (if any). + _, fwIDExists := service.GetAnnotations()[annLinodeCloudFirewallID] + if fwIDExists { // If an ID exists, we ignore everything else and handle just that + return l.updateFirewallwithID(ctx, service, nb) + } + + // See if a acl exists + _, fwACLExists := service.GetAnnotations()[annLinodeCloudFirewallACL] + if fwACLExists { // if an ACL exists, but no ID, just update the ACL on the fw. + return l.updateFWwithACL(ctx, service, nb) + } + + // No firewall ID or ACL annotation, see if there are firewalls attached to our nb + firewalls, err := l.client.ListNodeBalancerFirewalls(ctx, nb.ID, &linodego.ListOptions{}) + if err != nil { + return err + } + + if len(firewalls) == 0 { + return nil + } + if len(firewalls) > 1 { + klog.Errorf("Found more than one firewall attached to nodebalancer: %d, firewall IDs: %v", nb.ID, firewalls) + return errTooManyFirewalls + } + + err = l.client.DeleteFirewallDevice(ctx, firewalls[0].ID, nb.ID) + if err != nil { + return err } + // once we delete the device, we should see if there's anything attached to that firewall + devices, err := l.client.ListFirewallDevices(ctx, firewalls[0].ID, &linodego.ListOptions{}) + if err != nil { + return err + } + + if len(devices) == 0 { + // nothing attached to it, clean it up + return l.client.DeleteFirewall(ctx, firewalls[0].ID) + } + // else let that firewall linger, don't mess with it. + return nil } @@ -351,7 +527,6 @@ func (l *loadbalancers) updateNodeBalancer(ctx context.Context, clusterName stri } } - // update node-balancer firewall err = l.updateNodeBalancerFirewall(ctx, service, nb) if err != nil { return err @@ -428,6 +603,7 @@ func (l *loadbalancers) updateNodeBalancer(ctx context.Context, clusterName stri return fmt.Errorf("[port %d] error rebuilding NodeBalancer config: %v", int(port.Port), err) } } + return nil } @@ -572,7 +748,11 @@ func (l *loadbalancers) getNodeBalancerByID(ctx context.Context, service *v1.Ser } func (l *loadbalancers) getLoadBalancerTags(_ context.Context, clusterName string, service *v1.Service) []string { - tags := []string{clusterName} + tags := []string{} + if clusterName != "" { + tags = append(tags, clusterName) + } + tagStr, ok := service.GetAnnotations()[annLinodeLoadBalancerTags] if ok { return append(tags, strings.Split(tagStr, ",")...) @@ -580,6 +760,65 @@ func (l *loadbalancers) getLoadBalancerTags(_ context.Context, clusterName strin return tags } +// processACL takes the IPs, aclType, label etc and formats them into the passed linodego.FirewallCreateOptions pointer. +func processACL(fwcreateOpts *linodego.FirewallCreateOptions, aclType, label, svcName, ports string, ips linodego.NetworkAddresses) { + fwcreateOpts.Rules.Inbound = append(fwcreateOpts.Rules.Inbound, linodego.FirewallRule{ + Action: aclType, + Label: fmt.Sprintf("%s-%s", aclType, svcName), + Description: fmt.Sprintf("Created by linode-ccm: %s, for %s", label, svcName), + Protocol: linodego.TCP, // Nodebalancers support only TCP. + Ports: ports, + Addresses: ips, + }) + fwcreateOpts.Rules.OutboundPolicy = "ACCEPT" + if aclType == "ACCEPT" { + // if an allowlist is present, we drop everything else. + fwcreateOpts.Rules.InboundPolicy = "DROP" + } else { + // if a denylist is present, we accept everything else. + fwcreateOpts.Rules.InboundPolicy = "ACCEPT" + } +} + +type aclConfig struct { + AllowList *linodego.NetworkAddresses `json:"allowList"` + DenyList *linodego.NetworkAddresses `json:"denyList"` +} + +func (l *loadbalancers) createFirewallOptsForSvc(label string, tags []string, svc *v1.Service) (*linodego.FirewallCreateOptions, error) { + // Fetch acl from annotation + aclString := svc.GetAnnotations()[annLinodeCloudFirewallACL] + fwcreateOpts := linodego.FirewallCreateOptions{ + Label: label, + Tags: tags, + } + servicePorts := make([]string, 0, len(svc.Spec.Ports)) + for _, port := range svc.Spec.Ports { + servicePorts = append(servicePorts, strconv.Itoa(int(port.Port))) + } + + portsString := strings.Join(servicePorts[:], ",") + var acl aclConfig + err := json.Unmarshal([]byte(aclString), &acl) + if err != nil { + return nil, err + } + // it is a problem if both are set, or if both are not set + if (acl.AllowList != nil && acl.DenyList != nil) || (acl.AllowList == nil && acl.DenyList == nil) { + return nil, errInvalidFWConfig + } + + aclType := "ACCEPT" + allowedIPs := acl.AllowList + if acl.DenyList != nil { + aclType = "DROP" + allowedIPs = acl.DenyList + } + + processACL(&fwcreateOpts, aclType, label, svc.Name, portsString, *allowedIPs) + return &fwcreateOpts, nil +} + func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName string, service *v1.Service, configs []*linodego.NodeBalancerConfigCreateOptions) (lb *linodego.NodeBalancer, err error) { connThrottle := getConnectionThrottle(service) @@ -600,6 +839,22 @@ func (l *loadbalancers) createNodeBalancer(ctx context.Context, clusterName stri return nil, err } createOpts.FirewallID = firewallID + } else { + // There's no firewallID already set, see if we need to create a new fw, look for the acl annotation. + _, ok := service.GetAnnotations()[annLinodeCloudFirewallACL] + if ok { + fwcreateOpts, err := l.createFirewallOptsForSvc(label, tags, service) + if err != nil { + return nil, err + } + + firewall, err := l.client.CreateFirewall(ctx, *fwcreateOpts) + if err != nil { + return nil, err + } + createOpts.FirewallID = firewall.ID + } + // no need to deal with firewalls, continue creating nb's } return l.client.CreateNodeBalancer(ctx, createOpts) diff --git a/cloud/linode/loadbalancers_test.go b/cloud/linode/loadbalancers_test.go index 311e6f15..2747f85a 100644 --- a/cloud/linode/loadbalancers_test.go +++ b/cloud/linode/loadbalancers_test.go @@ -126,6 +126,22 @@ func TestCCMLoadBalancers(t *testing.T) { name: "Create Load Balancer With Invalid Firewall ID", f: testCreateNodeBalancerWithInvalidFirewall, }, + { + name: "Create Load Balancer With Valid Firewall ACL - AllowList", + f: testCreateNodeBalancerWithAllowList, + }, + { + name: "Create Load Balancer With Valid Firewall ACL - DenyList", + f: testCreateNodeBalancerWithDenyList, + }, + { + name: "Create Load Balancer With Invalid Firewall ACL - Both Allow and Deny", + f: testCreateNodeBalanceWithBothAllowOrDenyList, + }, + { + name: "Create Load Balancer With Invalid Firewall ACL - NO Allow Or Deny", + f: testCreateNodeBalanceWithNoAllowOrDenyList, + }, { name: "Update Load Balancer - Add Annotation", f: testUpdateLoadBalancerAddAnnotation, @@ -151,17 +167,33 @@ func TestCCMLoadBalancers(t *testing.T) { f: testUpdateLoadBalancerAddProxyProtocol, }, { - name: "Update Load Balancer - Add new Firewall", + name: "Update Load Balancer - Add new Firewall ID", f: testUpdateLoadBalancerAddNewFirewall, }, { - name: "Update Load Balancer - Update Firewall", + name: "Update Load Balancer - Update Firewall ID", f: testUpdateLoadBalancerUpdateFirewall, }, { - name: "Update Load Balancer - Delete Firewall", + name: "Update Load Balancer - Delete Firewall ID", f: testUpdateLoadBalancerDeleteFirewall, }, + { + name: "Update Load Balancer - Update Firewall ACL", + f: testUpdateLoadBalancerUpdateFirewallACL, + }, + { + name: "Update Load Balancer - Remove Firewall ID & Add ACL", + f: testUpdateLoadBalancerUpdateFirewallRemoveIDaddACL, + }, + { + name: "Update Load Balancer - Remove Firewall ACL & Add ID", + f: testUpdateLoadBalancerUpdateFirewallRemoveACLaddID, + }, + { + name: "Update Load Balancer - Add a new Firewall ACL", + f: testUpdateLoadBalancerAddNewFirewallACL, + }, { name: "Build Load Balancer Request", f: testBuildLoadBalancerRequest, @@ -226,7 +258,7 @@ func stubService(fake *fake.Clientset, service *v1.Service) { _, _ = fake.CoreV1().Services("").Create(context.TODO(), service, metav1.CreateOptions{}) } -func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI, firewallID *string) error { +func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI, annotations map[string]string) error { svc := &v1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: randString(), @@ -253,11 +285,9 @@ func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI, f }, }, } - - if firewallID != nil { - svc.Annotations[annLinodeCloudFirewallID] = *firewallID + for key, value := range annotations { + svc.Annotations[key] = value } - lb := &loadbalancers{client, "us-west", nil} nodes := []*v1.Node{ {ObjectMeta: metav1.ObjectMeta{Name: "node-1"}}, @@ -302,6 +332,19 @@ func testCreateNodeBalancer(t *testing.T, client *linodego.Client, _ *fakeAPI, f t.Logf("actual: %v", nb.Tags) } + _, ok := annotations[annLinodeCloudFirewallACL] + if ok { + // a firewall was configured for this + firewalls, err := client.ListNodeBalancerFirewalls(context.TODO(), nb.ID, &linodego.ListOptions{}) + if err != nil { + t.Errorf("Expected nil error, got %v", err) + } + + if len(firewalls) == 0 { + t.Errorf("Expected 1 firewall, got %d", len(firewalls)) + } + } + defer func() { _ = lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc) }() return nil } @@ -313,18 +356,81 @@ func testCreateNodeBalancerWithOutFirewall(t *testing.T, client *linodego.Client } } +func testCreateNodeBalanceWithNoAllowOrDenyList(t *testing.T, client *linodego.Client, f *fakeAPI) { + annotations := map[string]string{ + annLinodeCloudFirewallACL: `{}`, + } + + err := testCreateNodeBalancer(t, client, f, annotations) + if err == nil || !stderrors.Is(err, errInvalidFWConfig) { + t.Fatalf("expected a %v error, got %v", errInvalidFWConfig, err) + } +} + +func testCreateNodeBalanceWithBothAllowOrDenyList(t *testing.T, client *linodego.Client, f *fakeAPI) { + annotations := map[string]string{ + annLinodeCloudFirewallACL: `{ + "allowList": { + "ipv4": ["2.2.2.2"] + }, + "denyList": { + "ipv4": ["2.2.2.2"] + } + }`, + } + + err := testCreateNodeBalancer(t, client, f, annotations) + if err == nil || !stderrors.Is(err, errInvalidFWConfig) { + t.Fatalf("expected a %v error, got %v", errInvalidFWConfig, err) + } +} + +func testCreateNodeBalancerWithAllowList(t *testing.T, client *linodego.Client, f *fakeAPI) { + annotations := map[string]string{ + annLinodeCloudFirewallACL: `{ + "allowList": { + "ipv4": ["2.2.2.2"] + } + }`, + } + + err := testCreateNodeBalancer(t, client, f, annotations) + if err != nil { + t.Fatalf("expected a non-nil error, got %v", err) + } +} + +func testCreateNodeBalancerWithDenyList(t *testing.T, client *linodego.Client, f *fakeAPI) { + annotations := map[string]string{ + annLinodeCloudFirewallACL: `{ + "denyList": { + "ipv4": ["2.2.2.2"] + } + }`, + } + + err := testCreateNodeBalancer(t, client, f, annotations) + if err != nil { + t.Fatalf("expected a non-nil error, got %v", err) + } +} + func testCreateNodeBalancerWithFirewall(t *testing.T, client *linodego.Client, f *fakeAPI) { - firewallID := "123" - err := testCreateNodeBalancer(t, client, f, &firewallID) + annotations := map[string]string{ + annLinodeCloudFirewallID: "123", + } + err := testCreateNodeBalancer(t, client, f, annotations) if err != nil { t.Fatalf("expected a nil error, got %v", err) } } func testCreateNodeBalancerWithInvalidFirewall(t *testing.T, client *linodego.Client, f *fakeAPI) { - firewallID := "qwerty" + annotations := map[string]string{ + annLinodeCloudFirewallID: "qwerty", + } expectedError := "strconv.Atoi: parsing \"qwerty\": invalid syntax" - err := testCreateNodeBalancer(t, client, f, &firewallID) + err := testCreateNodeBalancer(t, client, f, annotations) if err.Error() != expectedError { t.Fatalf("expected a %s error, got %v", expectedError, err) } @@ -651,7 +757,7 @@ func testUpdateLoadBalancerAddTLSPort(t *testing.T, client *linodego.Client, _ * } if !reflect.DeepEqual(expectedPorts, observedPorts) { - t.Errorf("NodeBalancer ports mismatch: expected %v, got %v", `expectedPorts`, observedPorts) + t.Errorf("NodeBalancer ports mismatch: expected %v, got %v", expectedPorts, observedPorts) } } @@ -854,6 +960,490 @@ func testUpdateLoadBalancerAddNewFirewall(t *testing.T, client *linodego.Client, } } +func testUpdateLoadBalancerAddNewFirewallACL(t *testing.T, client *linodego.Client, fakeAPI *fakeAPI) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: randString(), + UID: "foobar123", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: randString(), + Protocol: "TCP", + Port: int32(80), + NodePort: int32(30000), + }, + }, + }, + } + + nodes := []*v1.Node{ + { + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "127.0.0.1", + }, + }, + }, + }, + } + + lb := &loadbalancers{client, "us-west", nil} + fakeClientset := fake.NewSimpleClientset() + lb.kubeClient = fakeClientset + + defer func() { + _ = lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc) + }() + lbStatus, err := lb.EnsureLoadBalancer(context.TODO(), "linodelb", svc, nodes) + if err != nil { + t.Errorf("EnsureLoadBalancer returned an error: %s", err) + } + svc.Status.LoadBalancer = *lbStatus + stubService(fakeClientset, svc) + + nb, err := lb.getNodeBalancerByStatus(context.TODO(), svc) + if err != nil { + t.Fatalf("failed to get NodeBalancer via status: %s", err) + } + + firewalls, err := lb.client.ListNodeBalancerFirewalls(context.TODO(), nb.ID, &linodego.ListOptions{}) + if err != nil { + t.Fatalf("Failed to list nodeBalancer firewalls %s", err) + } + + if len(firewalls) != 0 { + t.Fatalf("Firewalls attached when none specified") + } + + svc.ObjectMeta.SetAnnotations(map[string]string{ + annLinodeCloudFirewallACL: `{ + "allowList": { + "ipv4": ["2.2.2.2"] + } + }`, + }) + + err = lb.UpdateLoadBalancer(context.TODO(), "linodelb", svc, nodes) + if err != nil { + t.Errorf("UpdateLoadBalancer returned an error: %s", err) + } + + nbUpdated, err := lb.getNodeBalancerByStatus(context.TODO(), svc) + if err != nil { + t.Fatalf("failed to get NodeBalancer via status: %s", err) + } + + firewallsNew, err := lb.client.ListNodeBalancerFirewalls(context.TODO(), nbUpdated.ID, &linodego.ListOptions{}) + if err != nil { + t.Fatalf("failed to List Firewalls %s", err) + } + + if len(firewallsNew) == 0 { + t.Fatalf("No firewalls found") + } + + if firewallsNew[0].Rules.InboundPolicy != "DROP" { + t.Errorf("expected DROP inbound policy, got %s", firewallsNew[0].Rules.InboundPolicy) + } + + fwIPs := firewallsNew[0].Rules.Inbound[0].Addresses.IPv4 + if fwIPs == nil { + t.Errorf("expected 2.2.2.2, got %v", fwIPs) + } +} + +func testUpdateLoadBalancerUpdateFirewallRemoveACLaddID(t *testing.T, client *linodego.Client, fakeAPI *fakeAPI) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: randString(), + UID: "foobar123", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: randString(), + Protocol: "TCP", + Port: int32(80), + NodePort: int32(30000), + }, + }, + }, + } + + nodes := []*v1.Node{ + { + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "127.0.0.1", + }, + }, + }, + }, + } + + lb := &loadbalancers{client, "us-west", nil} + fakeClientset := fake.NewSimpleClientset() + lb.kubeClient = fakeClientset + + svc.ObjectMeta.SetAnnotations(map[string]string{ + annLinodeCloudFirewallACL: `{ + "allowList": { + "ipv4": ["2.2.2.2"] + } + }`, + }) + + defer func() { + _ = lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc) + }() + lbStatus, err := lb.EnsureLoadBalancer(context.TODO(), "linodelb", svc, nodes) + if err != nil { + t.Errorf("EnsureLoadBalancer returned an error: %s", err) + } + svc.Status.LoadBalancer = *lbStatus + stubService(fakeClientset, svc) + + nb, err := lb.getNodeBalancerByStatus(context.TODO(), svc) + if err != nil { + t.Fatalf("failed to get NodeBalancer via status: %s", err) + } + + firewalls, err := lb.client.ListNodeBalancerFirewalls(context.TODO(), nb.ID, &linodego.ListOptions{}) + if err != nil { + t.Fatalf("Failed to list nodeBalancer firewalls %s", err) + } + + if len(firewalls) == 0 { + t.Fatalf("No firewalls attached") + } + + if firewalls[0].Rules.InboundPolicy != "DROP" { + t.Errorf("expected DROP inbound policy, got %s", firewalls[0].Rules.InboundPolicy) + } + + fwIPs := firewalls[0].Rules.Inbound[0].Addresses.IPv4 + if fwIPs == nil { + t.Errorf("expected IP, got %v", fwIPs) + } + + firewall, err := lb.createFirewall(context.TODO(), linodego.FirewallCreateOptions{ + Label: "test", + Rules: linodego.FirewallRuleSet{Inbound: []linodego.FirewallRule{{ + Action: "ACCEPT", + Label: "inbound-rule123", + Description: "inbound rule123", + Ports: "4321", + Protocol: linodego.TCP, + Addresses: linodego.NetworkAddresses{ + IPv4: &[]string{"0.0.0.0/0"}, + }, + }}, Outbound: []linodego.FirewallRule{}, InboundPolicy: "ACCEPT", OutboundPolicy: "ACCEPT"}, + }) + if err != nil { + t.Errorf("Error creating firewall %s", err) + } + defer func() { + _ = lb.deleteFirewall(context.TODO(), firewall) + }() + + svc.ObjectMeta.SetAnnotations(map[string]string{ + annLinodeCloudFirewallID: strconv.Itoa(firewall.ID), + }) + + err = lb.UpdateLoadBalancer(context.TODO(), "linodelb", svc, nodes) + if err != nil { + t.Errorf("UpdateLoadBalancer returned an error: %s", err) + } + + nbUpdated, err := lb.getNodeBalancerByStatus(context.TODO(), svc) + if err != nil { + t.Fatalf("failed to get NodeBalancer via status: %s", err) + } + + firewallsNew, err := lb.client.ListNodeBalancerFirewalls(context.TODO(), nbUpdated.ID, &linodego.ListOptions{}) + if err != nil { + t.Fatalf("failed to List Firewalls %s", err) + } + + if len(firewallsNew) == 0 { + t.Fatalf("No attached firewalls found") + } + + if firewallsNew[0].Rules.InboundPolicy != "ACCEPT" { + t.Errorf("expected ACCEPT inbound policy, got %s", firewallsNew[0].Rules.InboundPolicy) + } + + fwIPs = firewallsNew[0].Rules.Inbound[0].Addresses.IPv4 + if fwIPs == nil { + t.Errorf("expected 2.2.2.2, got %v", fwIPs) + } + + if firewallsNew[0].ID != firewall.ID { + t.Errorf("Firewall ID does not match what we created, something wrong.") + } +} + +func testUpdateLoadBalancerUpdateFirewallRemoveIDaddACL(t *testing.T, client *linodego.Client, fakeAPI *fakeAPI) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: randString(), + UID: "foobar123", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: randString(), + Protocol: "TCP", + Port: int32(80), + NodePort: int32(30000), + }, + }, + }, + } + + nodes := []*v1.Node{ + { + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "127.0.0.1", + }, + }, + }, + }, + } + + lb := &loadbalancers{client, "us-west", nil} + fakeClientset := fake.NewSimpleClientset() + lb.kubeClient = fakeClientset + + firewall, err := lb.createFirewall(context.TODO(), linodego.FirewallCreateOptions{ + Label: "test", + Rules: linodego.FirewallRuleSet{Inbound: []linodego.FirewallRule{{ + Action: "ACCEPT", + Label: "inbound-rule123", + Description: "inbound rule123", + Ports: "4321", + Protocol: linodego.TCP, + Addresses: linodego.NetworkAddresses{ + IPv4: &[]string{"0.0.0.0/0"}, + }, + }}, Outbound: []linodego.FirewallRule{}, InboundPolicy: "ACCEPT", OutboundPolicy: "ACCEPT"}, + }) + if err != nil { + t.Errorf("Error creating firewall %s", err) + } + defer func() { + _ = lb.deleteFirewall(context.TODO(), firewall) + }() + + svc.ObjectMeta.SetAnnotations(map[string]string{ + annLinodeCloudFirewallID: strconv.Itoa(firewall.ID), + }) + + defer func() { + _ = lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc) + }() + lbStatus, err := lb.EnsureLoadBalancer(context.TODO(), "linodelb", svc, nodes) + if err != nil { + t.Errorf("EnsureLoadBalancer returned an error: %s", err) + } + svc.Status.LoadBalancer = *lbStatus + stubService(fakeClientset, svc) + + nb, err := lb.getNodeBalancerByStatus(context.TODO(), svc) + if err != nil { + t.Fatalf("failed to get NodeBalancer via status: %s", err) + } + + firewalls, err := lb.client.ListNodeBalancerFirewalls(context.TODO(), nb.ID, &linodego.ListOptions{}) + if err != nil { + t.Fatalf("Failed to list nodeBalancer firewalls %s", err) + } + + if len(firewalls) == 0 { + t.Fatalf("No firewalls attached") + } + + if firewalls[0].Rules.InboundPolicy != "ACCEPT" { + t.Errorf("expected ACCEPT inbound policy, got %s", firewalls[0].Rules.InboundPolicy) + } + + fwIPs := firewalls[0].Rules.Inbound[0].Addresses.IPv4 + if fwIPs == nil { + t.Errorf("expected IP, got %v", fwIPs) + } + svc.ObjectMeta.SetAnnotations(map[string]string{ + annLinodeCloudFirewallACL: `{ + "allowList": { + "ipv4": ["2.2.2.2"] + } + }`, + }) + + err = lb.UpdateLoadBalancer(context.TODO(), "linodelb", svc, nodes) + if err != nil { + t.Errorf("UpdateLoadBalancer returned an error: %s", err) + } + + nbUpdated, err := lb.getNodeBalancerByStatus(context.TODO(), svc) + if err != nil { + t.Fatalf("failed to get NodeBalancer via status: %s", err) + } + + firewallsNew, err := lb.client.ListNodeBalancerFirewalls(context.TODO(), nbUpdated.ID, &linodego.ListOptions{}) + if err != nil { + t.Fatalf("failed to List Firewalls %s", err) + } + + if len(firewallsNew) == 0 { + t.Fatalf("No attached firewalls found") + } + + if firewallsNew[0].Rules.InboundPolicy != "DROP" { + t.Errorf("expected DROP inbound policy, got %s", firewallsNew[0].Rules.InboundPolicy) + } + + fwIPs = firewallsNew[0].Rules.Inbound[0].Addresses.IPv4 + if fwIPs == nil { + t.Errorf("expected 2.2.2.2, got %v", fwIPs) + } + + if firewallsNew[0].ID != firewall.ID { + t.Errorf("Firewall ID does not match, something wrong.") + } +} + +func testUpdateLoadBalancerUpdateFirewallACL(t *testing.T, client *linodego.Client, fakeAPI *fakeAPI) { + svc := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: randString(), + UID: "foobar123", + Annotations: map[string]string{ + annLinodeCloudFirewallACL: `{ + "allowList": { + "ipv4": ["2.2.2.2"] + } + }`, + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: randString(), + Protocol: "TCP", + Port: int32(80), + NodePort: int32(30000), + }, + }, + }, + } + + nodes := []*v1.Node{ + { + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + { + Type: v1.NodeInternalIP, + Address: "127.0.0.1", + }, + }, + }, + }, + } + + lb := &loadbalancers{client, "us-west", nil} + fakeClientset := fake.NewSimpleClientset() + lb.kubeClient = fakeClientset + + defer func() { + _ = lb.EnsureLoadBalancerDeleted(context.TODO(), "linodelb", svc) + }() + lbStatus, err := lb.EnsureLoadBalancer(context.TODO(), "linodelb", svc, nodes) + if err != nil { + t.Errorf("EnsureLoadBalancer returned an error: %s", err) + } + svc.Status.LoadBalancer = *lbStatus + stubService(fakeClientset, svc) + + nb, err := lb.getNodeBalancerByStatus(context.TODO(), svc) + if err != nil { + t.Fatalf("failed to get NodeBalancer via status: %s", err) + } + + firewalls, err := lb.client.ListNodeBalancerFirewalls(context.TODO(), nb.ID, &linodego.ListOptions{}) + if err != nil { + t.Fatalf("Failed to list nodeBalancer firewalls %s", err) + } + + if len(firewalls) == 0 { + t.Fatalf("No firewalls attached") + } + + if firewalls[0].Rules.InboundPolicy != "DROP" { + t.Errorf("expected DROP inbound policy, got %s", firewalls[0].Rules.InboundPolicy) + } + + fwIPs := firewalls[0].Rules.Inbound[0].Addresses.IPv4 + if fwIPs == nil { + t.Errorf("expected 2.2.2.2, got %v", fwIPs) + } + + fmt.Printf("got %v", fwIPs) + + svc.ObjectMeta.SetAnnotations(map[string]string{ + annLinodeCloudFirewallACL: `{ + "allowList": { + "ipv4": ["2.2.2.2"], + "ipv6": ["dead:beef::/128"] + } + }`, + }) + + err = lb.UpdateLoadBalancer(context.TODO(), "linodelb", svc, nodes) + if err != nil { + t.Errorf("UpdateLoadBalancer returned an error: %s", err) + } + + nbUpdated, err := lb.getNodeBalancerByStatus(context.TODO(), svc) + if err != nil { + t.Fatalf("failed to get NodeBalancer via status: %s", err) + } + + firewallsNew, err := lb.client.ListNodeBalancerFirewalls(context.TODO(), nbUpdated.ID, &linodego.ListOptions{}) + if err != nil { + t.Fatalf("failed to List Firewalls %s", err) + } + + if len(firewallsNew) == 0 { + t.Fatalf("No attached firewalls found") + } + + fwIPs = firewallsNew[0].Rules.Inbound[0].Addresses.IPv4 + if fwIPs == nil { + t.Errorf("expected non nil IPv4, got %v", fwIPs) + } + + if len(*fwIPs) != 1 { + t.Errorf("expected one IPv4, got %v", fwIPs) + } + + if firewallsNew[0].Rules.Inbound[0].Addresses.IPv6 == nil { + t.Errorf("expected non nil IPv6, got %v", firewallsNew[0].Rules.Inbound[0].Addresses.IPv6) + } + + if len(*firewallsNew[0].Rules.Inbound[0].Addresses.IPv6) != 1 { + t.Errorf("expected one IPv6, got %v", firewallsNew[0].Rules.Inbound[0].Addresses.IPv6) + } +} + func testUpdateLoadBalancerUpdateFirewall(t *testing.T, client *linodego.Client, fakeAPI *fakeAPI) { firewallCreateOpts := linodego.FirewallCreateOptions{ Label: "test", @@ -1002,9 +1592,6 @@ func testUpdateLoadBalancerDeleteFirewall(t *testing.T, client *linodego.Client, ObjectMeta: metav1.ObjectMeta{ Name: randString(), UID: "foobar123", - Annotations: map[string]string{ - annLinodeThrottle: "15", - }, }, Spec: v1.ServiceSpec{ Ports: []v1.ServicePort{ diff --git a/cloud/linode/mock_client_test.go b/cloud/linode/mock_client_test.go index 073ccd4c..d7f5b984 100644 --- a/cloud/linode/mock_client_test.go +++ b/cloud/linode/mock_client_test.go @@ -151,6 +151,21 @@ func (mr *MockClientMockRecorder) DeleteNodeBalancerConfig(arg0, arg1, arg2 inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNodeBalancerConfig", reflect.TypeOf((*MockClient)(nil).DeleteNodeBalancerConfig), arg0, arg1, arg2) } +// GetFirewall mocks base method. +func (m *MockClient) GetFirewall(arg0 context.Context, arg1 int) (*linodego.Firewall, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFirewall", arg0, arg1) + ret0, _ := ret[0].(*linodego.Firewall) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFirewall indicates an expected call of GetFirewall. +func (mr *MockClientMockRecorder) GetFirewall(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirewall", reflect.TypeOf((*MockClient)(nil).GetFirewall), arg0, arg1) +} + // GetInstance mocks base method. func (m *MockClient) GetInstance(arg0 context.Context, arg1 int) (*linodego.Instance, error) { m.ctrl.T.Helper() @@ -286,6 +301,21 @@ func (mr *MockClientMockRecorder) RebuildNodeBalancerConfig(arg0, arg1, arg2, ar return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RebuildNodeBalancerConfig", reflect.TypeOf((*MockClient)(nil).RebuildNodeBalancerConfig), arg0, arg1, arg2, arg3) } +// UpdateFirewallRules mocks base method. +func (m *MockClient) UpdateFirewallRules(arg0 context.Context, arg1 int, arg2 linodego.FirewallRuleSet) (*linodego.FirewallRuleSet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateFirewallRules", arg0, arg1, arg2) + ret0, _ := ret[0].(*linodego.FirewallRuleSet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateFirewallRules indicates an expected call of UpdateFirewallRules. +func (mr *MockClientMockRecorder) UpdateFirewallRules(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFirewallRules", reflect.TypeOf((*MockClient)(nil).UpdateFirewallRules), arg0, arg1, arg2) +} + // UpdateNodeBalancer mocks base method. func (m *MockClient) UpdateNodeBalancer(arg0 context.Context, arg1 int, arg2 linodego.NodeBalancerUpdateOptions) (*linodego.NodeBalancer, error) { m.ctrl.T.Helper() diff --git a/examples/http-nginx-firewalled.yaml b/examples/http-nginx-firewalled.yaml new file mode 100644 index 00000000..0bf70efd --- /dev/null +++ b/examples/http-nginx-firewalled.yaml @@ -0,0 +1,47 @@ +--- +kind: Service +apiVersion: v1 +metadata: + name: http-lb + namespace: kube-system + annotations: + service.beta.kubernetes.io/linode-loadbalancer-default-protocol: "http" + service.beta.kubernetes.io/linode-loadbalancer-firewall-acl: | + { + "allowList": { + "ipv4": ["8.8.8.8/32"], + "ipv6": ["dead:beef::/64"] + } + } +spec: + type: LoadBalancer + selector: + app: nginx-http-example + ports: + - name: http + protocol: TCP + port: 80 + targetPort: 80 + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-http-deployment + namespace: kube-system +spec: + replicas: 2 + selector: + matchLabels: + app: nginx-http-example + template: + metadata: + labels: + app: nginx-http-example + spec: + containers: + - name: nginx + image: nginx + ports: + - containerPort: 80 + protocol: TCP diff --git a/go.mod b/go.mod index 214412d8..96c38329 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/linode/linodego v1.26.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a k8s.io/api v0.21.0 k8s.io/apimachinery v0.21.0 k8s.io/client-go v0.21.0 @@ -70,17 +71,17 @@ require ( go.uber.org/multierr v1.3.0 // indirect go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee // indirect go.uber.org/zap v1.13.0 // indirect - golang.org/x/crypto v0.16.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.19.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect - golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/tools v0.17.0 // indirect google.golang.org/appengine v1.6.5 // indirect google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect google.golang.org/grpc v1.27.1 // indirect diff --git a/go.sum b/go.sum index 8ef40708..ef6c9a1d 100644 --- a/go.sum +++ b/go.sum @@ -526,8 +526,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -538,6 +538,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -561,8 +563,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -598,8 +601,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -615,8 +618,9 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -664,8 +668,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -673,8 +677,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -737,8 +741,9 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=