diff --git a/cmd/list.go b/cmd/list.go index 03a5861..fd884d9 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -5,12 +5,14 @@ import ( "encoding/json" "fmt" "io" + "sort" "strconv" "strings" "text/tabwriter" "github.com/cilium/cilium/api/v1/client/endpoint" "github.com/spf13/cobra" + "golang.org/x/exp/maps" ) func init() { @@ -40,6 +42,20 @@ type derivedFromEntry struct { Name string `json:"name"` } +func lessDerivedFromEntry(x, y *derivedFromEntry) bool { + ret := strings.Compare(x.Direction, y.Direction) + if ret == 0 { + ret = strings.Compare(x.Kind, y.Kind) + } + if ret == 0 { + ret = strings.Compare(x.Namespace, y.Namespace) + } + if ret == 0 { + ret = strings.Compare(x.Name, y.Name) + } + return ret < 0 +} + func parseDerivedFromEntry(input []string, direction string) derivedFromEntry { val := derivedFromEntry{ Direction: direction, @@ -77,22 +93,28 @@ func runList(ctx context.Context, w io.Writer, name string) error { return err } - policyList := make([]derivedFromEntry, 0) + // The same rule appears multiple times in the response, so we need to dedup it + policySet := make(map[derivedFromEntry]struct{}) ingressRules := response.Payload.Status.Policy.Realized.L4.Ingress for _, rule := range ingressRules { for _, r := range rule.DerivedFromRules { - policyList = append(policyList, parseDerivedFromEntry(r, directionIngress)) + entry := parseDerivedFromEntry(r, directionIngress) + policySet[entry] = struct{}{} } } egressRules := response.Payload.Status.Policy.Realized.L4.Egress for _, rule := range egressRules { for _, r := range rule.DerivedFromRules { - policyList = append(policyList, parseDerivedFromEntry(r, directionEgress)) + entry := parseDerivedFromEntry(r, directionEgress) + policySet[entry] = struct{}{} } } + policyList := maps.Keys(policySet) + sort.Slice(policyList, func(i, j int) bool { return lessDerivedFromEntry(&policyList[i], &policyList[j]) }) + switch rootOptions.output { case OutputJson: text, err := json.MarshalIndent(policyList, "", " ") @@ -103,13 +125,13 @@ func runList(ctx context.Context, w io.Writer, name string) error { return err case OutputSimple: tw := tabwriter.NewWriter(w, 0, 1, 1, ' ', 0) - _, err := tw.Write([]byte("DIRECTION\tKIND\tNAMESPACE\tNAME\n")) - if err != nil { - return err + if !rootOptions.noHeaders { + if _, err := tw.Write([]byte("DIRECTION\tKIND\tNAMESPACE\tNAME\n")); err != nil { + return err + } } for _, p := range policyList { - _, err := tw.Write([]byte(fmt.Sprintf("%v\t%v\t%v\t%v\n", p.Direction, p.Kind, p.Namespace, p.Name))) - if err != nil { + if _, err := tw.Write([]byte(fmt.Sprintf("%v\t%v\t%v\t%v\n", p.Direction, p.Kind, p.Namespace, p.Name))); err != nil { return err } } diff --git a/cmd/root.go b/cmd/root.go index 3451d32..d97d6a8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,6 +17,7 @@ var rootOptions struct { proxySelector string proxyPort uint16 output string + noHeaders bool } func init() { @@ -24,6 +25,7 @@ func init() { rootCmd.PersistentFlags().StringVar(&rootOptions.proxySelector, "proxy-selector", "app.kubernetes.io/name=cilium-agent-proxy", "label selector to find the proxy pods") rootCmd.PersistentFlags().Uint16Var(&rootOptions.proxyPort, "proxy-port", 8080, "port number of the proxy endpoints") rootCmd.PersistentFlags().StringVarP(&rootOptions.output, "output", "o", OutputSimple, "output format") + rootCmd.PersistentFlags().BoolVar(&rootOptions.noHeaders, "no-headers", false, "stop printing header") } var rootCmd = &cobra.Command{} diff --git a/e2e/Makefile b/e2e/Makefile index b2f73e5..d80f725 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -33,22 +33,33 @@ start: $(MAKE) --no-print-directory wait-for-workloads run-test-pod-%: + @# https://github.com/orgs/aquaproj/discussions/2964 + @echo Hello | yq > /dev/null cat testdata/template/ubuntu.yaml | \ yq '.metadata.name = "$*"' | \ yq '.spec.selector.matchLabels = {"test": "$*"}' | \ - yq '.spec.template.metadata.labels = {"test": "$*"}' | \ + yq '.spec.template.metadata.labels = {"test": "$*", "group": "test"}' | \ kubectl apply -f - .PHONY: install-test-pod install-test-pod: $(MAKE) --no-print-directory run-test-pod-self $(MAKE) --no-print-directory run-test-pod-l3-ingress-explicit-allow - $(MAKE) --no-print-directory run-test-pod-l3-ingress-no-rule + $(MAKE) --no-print-directory run-test-pod-l3-ingress-implicit-deny $(MAKE) --no-print-directory run-test-pod-l3-ingress-explicit-deny $(MAKE) --no-print-directory run-test-pod-l3-egress-implicit-deny $(MAKE) --no-print-directory run-test-pod-l3-egress-explicit-deny + + $(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-allow-any + $(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-allow-tcp + $(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-deny-any + $(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-deny-udp + $(MAKE) --no-print-directory run-test-pod-l4-egress-explicit-deny-any + $(MAKE) --no-print-directory run-test-pod-l4-egress-explicit-deny-tcp $(MAKE) --no-print-directory wait-for-workloads + kubectl apply -f testdata/policy/l3.yaml + kubectl apply -f testdata/policy/l4.yaml .PHONY: install-policy-viewer install-policy-viewer: diff --git a/e2e/list_test.go b/e2e/list_test.go index 2f64dee..a5ca46a 100644 --- a/e2e/list_test.go +++ b/e2e/list_test.go @@ -1,28 +1,12 @@ package e2e import ( - "encoding/json" - "fmt" - "reflect" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -func testJson(g Gomega, target []byte, expected string) { - var t, e interface{} - err := json.Unmarshal(target, &t) - g.Expect(err).NotTo(HaveOccurred(), "actual: %s", target) - - err = json.Unmarshal([]byte(expected), &e) - g.Expect(err).NotTo(HaveOccurred(), "expected: %s", expected) - - if !reflect.DeepEqual(t, e) { - err := fmt.Errorf("compare failed. actual: %s, expected: %s", target, expected) - g.Expect(err).NotTo(HaveOccurred()) - } -} - func testList() { cases := []struct { Selector string @@ -30,42 +14,81 @@ func testList() { }{ { Selector: "test=self", - Expected: `[{ - "direction": "EGRESS", - "kind": "CiliumNetworkPolicy", - "namespace": "default", - "name": "l3-egress" - }]`, + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +EGRESS,CiliumNetworkPolicy,default,l3-egress +EGRESS,CiliumNetworkPolicy,default,l4-egress +INGRESS,CiliumNetworkPolicy,default,l3-baseline`, }, { Selector: "test=l3-ingress-explicit-allow", - Expected: `[{ - "direction": "INGRESS", - "kind": "CiliumNetworkPolicy", - "namespace": "default", - "name": "l3-ingress-explicit-allow" - }]`, + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-ingress-explicit-allow`, }, { - Selector: "test=l3-ingress-no-rule", - Expected: `[]`, + Selector: "test=l3-ingress-implicit-deny", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline`, }, { Selector: "test=l3-ingress-explicit-deny", - Expected: `[{ - "direction": "INGRESS", - "kind": "CiliumNetworkPolicy", - "namespace": "default", - "name": "l3-ingress-explicit-deny" - }]`, + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-ingress-explicit-deny`, + }, + { + Selector: "test=l3-egress-implicit-deny", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + }, + { + Selector: "test=l3-egress-explicit-deny", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + }, + { + Selector: "test=l4-ingress-explicit-allow-any", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l4-ingress-explicit-allow-any`, + }, + { + Selector: "test=l4-ingress-explicit-allow-tcp", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l4-ingress-explicit-allow-tcp`, + }, + { + Selector: "test=l4-ingress-explicit-deny-any", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l4-ingress-explicit-deny-any`, + }, + { + Selector: "test=l4-ingress-explicit-deny-udp", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l4-ingress-explicit-deny-udp`, + }, + { + Selector: "test=l4-egress-explicit-deny-any", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + }, + { + Selector: "test=l4-egress-explicit-deny-tcp", + Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline +INGRESS,CiliumNetworkPolicy,default,l3-baseline`, }, } It("should list applied policies", func() { for _, c := range cases { podName := onePodByLabelSelector(Default, "default", c.Selector) - result := runViewerSafe(Default, nil, "list", "-o=json", podName) - testJson(Default, result, c.Expected) + result := runViewerSafe(Default, nil, "list", "-o=json", "--no-headers", podName) + result = jqSafe(Default, result, "-r", ".[] | [.direction, .kind, .namespace, .name] | @csv") + resultString := strings.Replace(string(result), `"`, "", -1) + Expect(resultString).To(Equal(c.Expected), "compare failed. selector: %s, actual: %s, expected: %s", c.Selector, resultString, c.Expected) } }) } diff --git a/e2e/testdata/policy/README.md b/e2e/testdata/policy/README.md new file mode 100644 index 0000000..d685ede --- /dev/null +++ b/e2e/testdata/policy/README.md @@ -0,0 +1,15 @@ +# NetworkPolicy Configuration for Test Pods + +| Target | From self (Egress) | To pod (Ingress) | +|-|-|-| +| l3-ingress-explicit-allow | allow | allow | +| l3-ingress-implicit-deny | allow | - | +| l3-ingress-explicit-deny | allow | deny | +| l3-egress-implicit-deny | - | - | +| l3-egress-explicit-deny | deny | - | +| l4-ingress-explicit-allow-any | allow (L4) | allow (L4) | +| l4-ingress-explicit-allow-tcp | allow (L4) | allow (L4) | +| l4-ingress-explicit-deny-any | allow (L4) | deny (L4) | +| l4-ingress-explicit-deny-udp | allow (L4) | deny (L4) | +| l4-egress-explicit-deny-any | deny (L4) | - | +| l4-egress-explicit-deny-tcp | deny (L4) | - | diff --git a/e2e/testdata/policy/l3.yaml b/e2e/testdata/policy/l3.yaml index ccc3433..15c42ef 100644 --- a/e2e/testdata/policy/l3.yaml +++ b/e2e/testdata/policy/l3.yaml @@ -1,5 +1,22 @@ apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy +metadata: + name: l3-baseline +spec: + endpointSelector: + matchLabels: + k8s:group: test + ingressDeny: + - fromEndpoints: + - matchLabels: + k8s:test: scapegoat + egressDeny: + - toEndpoints: + - matchLabels: + k8s:test: scapegoat +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy metadata: name: l3-egress spec: @@ -10,6 +27,9 @@ spec: - toEndpoints: - matchLabels: k8s:test: l3-ingress-explicit-allow + - toEndpoints: + - matchLabels: + k8s:test: l3-ingress-no-rule - toEndpoints: - matchLabels: k8s:test: l3-ingress-implicit-deny diff --git a/e2e/testdata/policy/l4.yaml b/e2e/testdata/policy/l4.yaml new file mode 100644 index 0000000..565d6a2 --- /dev/null +++ b/e2e/testdata/policy/l4.yaml @@ -0,0 +1,115 @@ +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: l4-egress +spec: + endpointSelector: + matchLabels: + k8s:test: self + egress: + - toEndpoints: + - matchLabels: + k8s:test: l4-ingress-explicit-allow-any + toPorts: + - ports: + - port: "53" + - toEndpoints: + - matchLabels: + k8s:test: l4-ingress-explicit-allow-tcp + toPorts: + - ports: + - port: "8080" + protocol: TCP + - toEndpoints: + - matchLabels: + k8s:test: l4-ingress-explicit-deny-any + toPorts: + - ports: + - port: "53" + - toEndpoints: + - matchLabels: + k8s:test: l4-ingress-explicit-deny-udp + toPorts: + - ports: + - port: "161" # SNMP (UDP) + protocol: UDP + egressDeny: + - toEndpoints: + - matchLabels: + k8s:test: l4-egress-explicit-deny-any + toPorts: + - ports: + - port: "53" + - toEndpoints: + - matchLabels: + k8s:test: l4-egress-explicit-deny-tcp + toPorts: + - ports: + - port: "8080" + protocol: TCP +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: l4-ingress-explicit-allow-any +spec: + endpointSelector: + matchLabels: + k8s:test: l4-ingress-explicit-allow-any + ingress: + - fromEndpoints: + - matchLabels: + k8s:test: self + toPorts: + - ports: + - port: "53" +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: l4-ingress-explicit-allow-tcp +spec: + endpointSelector: + matchLabels: + k8s:test: l4-ingress-explicit-allow-tcp + ingress: + - fromEndpoints: + - matchLabels: + k8s:test: self + toPorts: + - ports: + - port: "8080" + protocol: TCP +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: l4-ingress-explicit-deny-any +spec: + endpointSelector: + matchLabels: + k8s:test: l4-ingress-explicit-deny-any + ingressDeny: + - fromEndpoints: + - matchLabels: + k8s:test: self + toPorts: + - ports: + - port: "53" +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + name: l4-ingress-explicit-deny-udp +spec: + endpointSelector: + matchLabels: + k8s:test: l4-ingress-explicit-deny-udp + ingressDeny: + - fromEndpoints: + - matchLabels: + k8s:test: self + toPorts: + - ports: + - port: "161" # SNMP (UDP) + protocol: UDP diff --git a/e2e/utils_test.go b/e2e/utils_test.go index b89dcb9..c84bdc8 100644 --- a/e2e/utils_test.go +++ b/e2e/utils_test.go @@ -26,12 +26,11 @@ func runCommand(path string, input []byte, args ...string) ([]byte, []byte, erro if input != nil { cmd.Stdin = bytes.NewReader(input) } - err := cmd.Run() - if err == nil { - return stdout.Bytes(), stderr.Bytes(), nil + if err := cmd.Run(); err != nil { + _, file := filepath.Split(path) + return stdout.Bytes(), stderr.Bytes(), fmt.Errorf("%s failed with %s: stderr=%s", file, err, stderr) } - _, file := filepath.Split(path) - return stdout.Bytes(), stderr.Bytes(), fmt.Errorf("%s failed with %s: stderr=%s", file, err, stderr) + return stdout.Bytes(), stderr.Bytes(), nil } func kubectl(input []byte, args ...string) ([]byte, []byte, error) { @@ -65,6 +64,6 @@ func onePodByLabelSelector(g Gomega, namespace, selector string) string { data := kubectlSafe(g, nil, "get", "pod", "-n", namespace, "-l", selector, "-o=json") count, err := strconv.Atoi(string(jqSafe(g, data, "-r", ".items | length"))) g.Expect(err).NotTo(HaveOccurred()) - g.Expect(count).To(Equal(1)) + g.Expect(count).To(Equal(1), "namespace: %s, selector: %s", namespace, selector) return string(jqSafe(g, data, "-r", ".items[0].metadata.name")) } diff --git a/go.mod b/go.mod index 704bde4..0904b62 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 github.com/spf13/cobra v1.8.1 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 ) @@ -77,7 +78,6 @@ require ( go.opentelemetry.io/otel/trace v1.21.0 // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.8.0 // indirect