diff --git a/cmd/npv/app/const.go b/cmd/npv/app/const.go index b077207..fd9fbab 100644 --- a/cmd/npv/app/const.go +++ b/cmd/npv/app/const.go @@ -33,3 +33,9 @@ var gvrClusterwideNetworkPolicy schema.GroupVersionResource = schema.GroupVersio Version: "v2", Resource: "ciliumclusterwidenetworkpolicies", } + +var gvkNetworkPolicy schema.GroupVersionKind = schema.GroupVersionKind{ + Group: "cilium.io", + Version: "v2", + Kind: "CiliumNetworkPolicy", +} diff --git a/cmd/npv/app/helper.go b/cmd/npv/app/helper.go index 2627d31..9d7d370 100644 --- a/cmd/npv/app/helper.go +++ b/cmd/npv/app/helper.go @@ -103,6 +103,23 @@ func getPodEndpointID(ctx context.Context, d *dynamic.DynamicClient, namespace, return endpointID, nil } +func getPodIdentity(ctx context.Context, d *dynamic.DynamicClient, namespace, name string) (int64, error) { + ep, err := d.Resource(gvrEndpoint).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return 0, err + } + + identity, found, err := unstructured.NestedInt64(ep.Object, "status", "identity", "id") + if err != nil { + return 0, err + } + if !found { + return 0, errors.New("pod does not have security identity") + } + + return identity, nil +} + // key: identity number // value: CiliumIdentity resource func getIdentityResourceMap(ctx context.Context, d *dynamic.DynamicClient) (map[int]*unstructured.Unstructured, error) { diff --git a/cmd/npv/app/inspect.go b/cmd/npv/app/inspect.go index 1a442e2..c8440b2 100644 --- a/cmd/npv/app/inspect.go +++ b/cmd/npv/app/inspect.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "math/rand" "net/http" "slices" "strconv" diff --git a/cmd/npv/app/manifest.go b/cmd/npv/app/manifest.go new file mode 100644 index 0000000..fbe3871 --- /dev/null +++ b/cmd/npv/app/manifest.go @@ -0,0 +1,13 @@ +package app + +import "github.com/spf13/cobra" + +func init() { + rootCmd.AddCommand(manifestCmd) +} + +var manifestCmd = &cobra.Command{ + Use: "manifest", + Short: "Generate CiliumNetworkPolicy", + Long: `Generate CiliumNetworkPolicy`, +} diff --git a/cmd/npv/app/manifest_blast.go b/cmd/npv/app/manifest_blast.go new file mode 100644 index 0000000..eebaaa3 --- /dev/null +++ b/cmd/npv/app/manifest_blast.go @@ -0,0 +1,106 @@ +package app + +import ( + "context" + "errors" + "io" + "sort" + "strings" + + "github.com/spf13/cobra" +) + +var manifestBlastOptions struct { + from string + to string +} + +func init() { + manifestBlastCmd.Flags().StringVar(&manifestBlastOptions.from, "from", "", "egress pod") + manifestBlastCmd.Flags().StringVar(&manifestBlastOptions.to, "to", "", "ingress pod") + manifestCmd.AddCommand(manifestBlastCmd) +} + +var manifestBlastCmd = &cobra.Command{ + Use: "blast", + Short: "Show blast radius of a generated manifest", + Long: `Show blast radius of a generated manifest`, + + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runManifestBlast(context.Background(), cmd.OutOrStdout()) + }, +} + +type manifestBlastEntry struct { + Direction string `json:"direction"` + Namespace string `json:"namespace"` + Name string `json:"name"` +} + +func lessManifestBlastEntry(x, y *manifestBlastEntry) bool { + ret := strings.Compare(x.Direction, y.Direction) + if ret == 0 { + ret = strings.Compare(x.Namespace, y.Namespace) + } + if ret == 0 { + ret = strings.Compare(x.Name, y.Name) + } + return ret < 0 +} + +func runManifestBlast(ctx context.Context, w io.Writer) error { + if manifestBlastOptions.from == "" || manifestBlastOptions.to == "" { + return errors.New("--from and --to options are required") + } + + fromSlice := strings.Split(manifestBlastOptions.from, "/") + toSlice := strings.Split(manifestBlastOptions.to, "/") + if len(fromSlice) != 2 || len(toSlice) != 2 { + return errors.New("--from and --to should be NAMESPACE/POD") + } + + _, dynamicClient, err := createK8sClients() + if err != nil { + return err + } + + fromIdentity, err := getPodIdentity(ctx, dynamicClient, fromSlice[0], fromSlice[1]) + if err != nil { + return err + } + + toIdentity, err := getPodIdentity(ctx, dynamicClient, toSlice[0], toSlice[1]) + if err != nil { + return err + } + + idEndpoints, err := getIdentityEndpoints(ctx, dynamicClient) + if err != nil { + return err + } + + arr := make([]manifestBlastEntry, 0) + sort.Slice(arr, func(i, j int) bool { return lessManifestBlastEntry(&arr[i], &arr[j]) }) + + for _, ep := range idEndpoints[int(fromIdentity)] { + entry := manifestBlastEntry{ + Direction: directionEgress, + Namespace: ep.GetNamespace(), + Name: ep.GetName(), + } + arr = append(arr, entry) + } + for _, ep := range idEndpoints[int(toIdentity)] { + entry := manifestBlastEntry{ + Direction: directionIngress, + Namespace: ep.GetNamespace(), + Name: ep.GetName(), + } + arr = append(arr, entry) + } + return writeSimpleOrJson(w, arr, []string{"DIRECTION", "NAMESPACE", "NAME"}, len(arr), func(index int) []any { + ep := arr[index] + return []any{ep.Direction, ep.Namespace, ep.Name} + }) +} diff --git a/e2e/Makefile b/e2e/Makefile index e879900..845a1b6 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -7,6 +7,8 @@ CACHE_DIR := $(shell pwd)/../cache POLICY_VIEWER := $(BIN_DIR)/npv HELM := helm --repository-cache $(CACHE_DIR)/helm/repository --repository-config $(CACHE_DIR)/helm/repositories.yaml +DEPLOYMENT_REPLICAS ?= 1 + ##@ Basic .PHONY: help @@ -41,6 +43,7 @@ run-test-pod-%: @echo Hello | yq > /dev/null cat testdata/template/ubuntu.yaml | \ yq '.metadata.name = "$*"' | \ + yq '.spec.replicas = $(DEPLOYMENT_REPLICAS)' | \ yq '.spec.selector.matchLabels = {"test": "$*"}' | \ yq '.spec.template.metadata.labels = {"test": "$*", "group": "test"}' | \ kubectl apply -f - @@ -48,7 +51,7 @@ run-test-pod-%: .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-all + $(MAKE) --no-print-directory DEPLOYMENT_REPLICAS=2 run-test-pod-l3-ingress-explicit-allow-all $(MAKE) --no-print-directory run-test-pod-l3-ingress-implicit-deny-all $(MAKE) --no-print-directory run-test-pod-l3-ingress-explicit-deny-all $(MAKE) --no-print-directory run-test-pod-l3-egress-implicit-deny-all diff --git a/e2e/manifest_test.go b/e2e/manifest_test.go new file mode 100644 index 0000000..4ab0247 --- /dev/null +++ b/e2e/manifest_test.go @@ -0,0 +1,30 @@ +package e2e + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func testManifestGenerate() { + +} + +func testManifestBlast() { + expected := `Egress,test,self +Ingress,test,l3-ingress-explicit-allow-all +Ingress,test,l3-ingress-explicit-allow-all` + + It("should show blast radius", func() { + from := "--from=test/" + onePodByLabelSelector(Default, "test", "test=self") + to := "--to=test/" + onePodByLabelSelector(Default, "test", "test=l3-ingress-explicit-allow-all") + result := runViewerSafe(Default, nil, "manifest", "blast", from, to, "-o=json") + // remove hash suffix from pod names + result = jqSafe(Default, result, "-r", `[.[] | .name = (.name | split("-") | .[0:5] | join("-"))]`) + result = jqSafe(Default, result, "-r", `[.[] | .name = (.name | if startswith("self") then "self" else . end)]`) + result = jqSafe(Default, result, "-r", `.[] | [.direction, .namespace, .name] | @csv`) + resultString := strings.Replace(string(result), `"`, "", -1) + Expect(resultString).To(Equal(expected), "compare failed.\nactual: %s\nexpected: %s", resultString, expected) + }) +} diff --git a/e2e/suite_test.go b/e2e/suite_test.go index 89db8af..77b7415 100644 --- a/e2e/suite_test.go +++ b/e2e/suite_test.go @@ -30,4 +30,6 @@ func runTest() { Context("id-summary", testIdSummary) Context("inspect", testInspect) Context("summary", testSummary) + Context("manifest-generate", testManifestGenerate) + Context("manifest-blast", testManifestBlast) } diff --git a/e2e/summary_test.go b/e2e/summary_test.go index 0b9665e..d3faf8d 100644 --- a/e2e/summary_test.go +++ b/e2e/summary_test.go @@ -11,6 +11,7 @@ func testSummary() { expected := `l3-egress-explicit-deny-all,1,0,0,0 l3-egress-implicit-deny-all,1,0,0,0 l3-ingress-explicit-allow-all,2,0,0,0 +l3-ingress-explicit-allow-all,2,0,0,0 l3-ingress-explicit-deny-all,1,1,0,0 l3-ingress-implicit-deny-all,1,0,0,0 l4-egress-explicit-deny-any,1,0,0,0 diff --git a/e2e/testdata/policy/README.md b/e2e/testdata/policy/README.md index 4f03477..a14106c 100644 --- a/e2e/testdata/policy/README.md +++ b/e2e/testdata/policy/README.md @@ -2,7 +2,8 @@ | Target | From self (Egress) | To pod (Ingress) | |-|-|-| -| l3-ingress-explicit-allow-all | allow | allow | +| l3-ingress-explicit-allow-all (1) | allow | allow | +| l3-ingress-explicit-allow-all (2) | allow | allow | | l3-ingress-implicit-deny-all | allow | - | | l3-ingress-explicit-deny-all | allow | deny | | l3-egress-implicit-deny-all | - | - | diff --git a/e2e/utils_test.go b/e2e/utils_test.go index c84bdc8..daf7fa3 100644 --- a/e2e/utils_test.go +++ b/e2e/utils_test.go @@ -64,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), "namespace: %s, selector: %s", namespace, selector) + g.Expect(count).To(BeNumerically(">=", 1), "namespace: %s, selector: %s", namespace, selector) return string(jqSafe(g, data, "-r", ".items[0].metadata.name")) }