diff --git a/src/k8s/go.mod b/src/k8s/go.mod index 00d8b9633..48562f653 100644 --- a/src/k8s/go.mod +++ b/src/k8s/go.mod @@ -5,7 +5,7 @@ go 1.22.6 require ( dario.cat/mergo v1.0.0 github.com/canonical/go-dqlite v1.22.0 - github.com/canonical/k8s-snap-api v1.0.11 + github.com/canonical/k8s-snap-api v1.0.12 github.com/canonical/lxd v0.0.0-20240822122218-e7b2a7a83230 github.com/canonical/microcluster/v3 v3.0.0-20240827143335-f7a4d3984970 github.com/go-logr/logr v1.4.2 diff --git a/src/k8s/go.sum b/src/k8s/go.sum index e2bf4dcf9..fb5671c2b 100644 --- a/src/k8s/go.sum +++ b/src/k8s/go.sum @@ -99,8 +99,8 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXe github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/canonical/go-dqlite v1.22.0 h1:DuJmfcREl4gkQJyvZzjl2GHFZROhbPyfdjDRQXpkOyw= github.com/canonical/go-dqlite v1.22.0/go.mod h1:Uvy943N8R4CFUAs59A1NVaziWY9nJ686lScY7ywurfg= -github.com/canonical/k8s-snap-api v1.0.11 h1:nGtwrUQBLiaL3HUXFx2gb4kq6qVpl2yNwMwHVX0dEok= -github.com/canonical/k8s-snap-api v1.0.11/go.mod h1:LDPoIYCeYnfgOFrwVPJ/4edGU264w7BB7g0GsVi36AY= +github.com/canonical/k8s-snap-api v1.0.12 h1:ofS2+JRlPMnpWgHLmnE4QEUqWv9Dgrmsv3hrjI0O4zQ= +github.com/canonical/k8s-snap-api v1.0.12/go.mod h1:LDPoIYCeYnfgOFrwVPJ/4edGU264w7BB7g0GsVi36AY= github.com/canonical/lxd v0.0.0-20240822122218-e7b2a7a83230 h1:YOqZ+/14OPZ+/TOXpRHIX3KLT0C+wZVpewKIwlGUmW0= github.com/canonical/lxd v0.0.0-20240822122218-e7b2a7a83230/go.mod h1:YVGI7HStOKsV+cMyXWnJ7RaMPaeWtrkxyIPvGWbgACc= github.com/canonical/microcluster/v3 v3.0.0-20240827143335-f7a4d3984970 h1:UrnpglbXELlxtufdk6DGDytu2JzyzuS3WTsOwPrkQLI= diff --git a/src/k8s/pkg/k8sd/features/cilium/internal.go b/src/k8s/pkg/k8sd/features/cilium/internal.go index e386eb1c5..88849ade7 100644 --- a/src/k8s/pkg/k8sd/features/cilium/internal.go +++ b/src/k8s/pkg/k8sd/features/cilium/internal.go @@ -1,13 +1,55 @@ package cilium import ( + "fmt" + "slices" + "strconv" + "strings" + apiv1_annotations "github.com/canonical/k8s-snap-api/api/v1/annotations/cilium" "github.com/canonical/k8s/pkg/k8sd/types" ) +const ( + // minVLANIDValue is the minimum valid 802.1Q VLAN ID value + minVLANIDValue = 0 + // maxVLANIDValue is the maximum valid 802.1Q VLAN ID value + maxVLANIDValue = 4094 +) + type config struct { devices string directRoutingDevice string + vlanBPFBypass []int +} + +func validateVLANBPFBypass(vlanList string) ([]int, error) { + vlanList = strings.TrimSpace(vlanList) + // Maintain compatibility with the Cilium chart definition + vlanList = strings.Trim(vlanList, "{}") + vlans := strings.Split(vlanList, ",") + + vlanTags := make([]int, 0, len(vlans)) + seenTags := make(map[int]struct{}) + + for _, vlan := range vlans { + vlanID, err := strconv.Atoi(strings.TrimSpace(vlan)) + if err != nil { + return []int{}, fmt.Errorf("failed to parse VLAN tag: %w", err) + } + if vlanID < minVLANIDValue || vlanID > maxVLANIDValue { + return []int{}, fmt.Errorf("VLAN tag must be between 0 and %d", maxVLANIDValue) + } + + if _, ok := seenTags[vlanID]; ok { + continue + } + seenTags[vlanID] = struct{}{} + vlanTags = append(vlanTags, vlanID) + } + + slices.Sort(vlanTags) + return vlanTags, nil } func internalConfig(annotations types.Annotations) (config, error) { @@ -21,5 +63,13 @@ func internalConfig(annotations types.Annotations) (config, error) { c.directRoutingDevice = v } + if v, ok := annotations[apiv1_annotations.AnnotationVLANBPFBypass]; ok { + vlanTags, err := validateVLANBPFBypass(v) + if err != nil { + return config{}, fmt.Errorf("failed to parse VLAN BPF bypass list: %w", err) + } + c.vlanBPFBypass = vlanTags + } + return c, nil } diff --git a/src/k8s/pkg/k8sd/features/cilium/internal_test.go b/src/k8s/pkg/k8sd/features/cilium/internal_test.go index e171fbbb3..14af95736 100644 --- a/src/k8s/pkg/k8sd/features/cilium/internal_test.go +++ b/src/k8s/pkg/k8sd/features/cilium/internal_test.go @@ -20,6 +20,7 @@ func TestInternalConfig(t *testing.T) { expectedConfig: config{ devices: "", directRoutingDevice: "", + vlanBPFBypass: nil, }, expectError: false, }, @@ -28,22 +29,113 @@ func TestInternalConfig(t *testing.T) { annotations: map[string]string{ apiv1_annotations.AnnotationDevices: "eth+ lxdbr+", apiv1_annotations.AnnotationDirectRoutingDevice: "eth0", + apiv1_annotations.AnnotationVLANBPFBypass: "1,2,3", }, expectedConfig: config{ devices: "eth+ lxdbr+", directRoutingDevice: "eth0", + vlanBPFBypass: []int{1, 2, 3}, + }, + expectError: false, + }, + { + name: "Single valid VLAN", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "1", + }, + expectedConfig: config{ + vlanBPFBypass: []int{1}, + }, + expectError: false, + }, + { + name: "Multiple valid VLANs", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "1,2,3,4,5", + }, + expectedConfig: config{ + vlanBPFBypass: []int{1, 2, 3, 4, 5}, + }, + expectError: false, + }, + { + name: "Wildcard VLAN", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "0", + }, + expectedConfig: config{ + vlanBPFBypass: []int{0}, + }, + expectError: false, + }, + { + name: "Invalid VLAN tag format", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "abc", + }, + expectError: true, + }, + { + name: "VLAN tag out of range", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "4095", + }, + expectError: true, + }, + { + name: "VLAN tag negative", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "-1", + }, + expectError: true, + }, + { + name: "Duplicate VLAN tags", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "1,2,2,3", + }, + expectedConfig: config{ + vlanBPFBypass: []int{1, 2, 3}, + }, + expectError: false, + }, + { + name: "Mixed spaces and commas", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: " 1, 2,3 ,4 , 5 ", + }, + expectedConfig: config{ + vlanBPFBypass: []int{1, 2, 3, 4, 5}, + }, + expectError: false, + }, + { + name: "Invalid mixed with valid", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "1,abc,3", + }, + expectError: true, + }, + { + name: "Nil annotations", + annotations: nil, + expectedConfig: config{}, + expectError: false, + }, + { + name: "VLAN with curly braces", + annotations: map[string]string{ + apiv1_annotations.AnnotationVLANBPFBypass: "{1,2,3}", + }, + expectedConfig: config{ + vlanBPFBypass: []int{1, 2, 3}, }, expectError: false, }, } { t.Run(tc.name, func(t *testing.T) { g := NewWithT(t) - annotations := make(map[string]string) - for k, v := range tc.annotations { - annotations[k] = v - } - - parsed, err := internalConfig(annotations) + parsed, err := internalConfig(tc.annotations) if tc.expectError { g.Expect(err).To(HaveOccurred()) } else { diff --git a/src/k8s/pkg/k8sd/features/cilium/network.go b/src/k8s/pkg/k8sd/features/cilium/network.go index 425e2dc6e..e9a8085bb 100644 --- a/src/k8s/pkg/k8sd/features/cilium/network.go +++ b/src/k8s/pkg/k8sd/features/cilium/network.go @@ -80,7 +80,13 @@ func ApplyNetwork(ctx context.Context, snap snap.Snap, apiserver types.APIServer ciliumNodePortValues["directRoutingDevice"] = config.directRoutingDevice } + bpfValues := map[string]any{} + if config.vlanBPFBypass != nil { + bpfValues["vlanBypass"] = config.vlanBPFBypass + } + values := map[string]any{ + "bpf": bpfValues, "image": map[string]any{ "repository": ciliumAgentImageRepo, "tag": CiliumAgentImageTag,