diff --git a/cmd/npv/app/dump.go b/cmd/npv/app/dump.go index bb93769..8d310ab 100644 --- a/cmd/npv/app/dump.go +++ b/cmd/npv/app/dump.go @@ -27,7 +27,7 @@ var dumpCmd = &cobra.Command{ } func runDump(ctx context.Context, w io.Writer, name string) error { - clientset, dynamicClient, _, err := createClients(ctx, name) + clientset, dynamicClient, err := createK8sClients() if err != nil { return err } diff --git a/cmd/npv/app/helper.go b/cmd/npv/app/helper.go index 9f39101..2f1c5e4 100644 --- a/cmd/npv/app/helper.go +++ b/cmd/npv/app/helper.go @@ -4,8 +4,12 @@ import ( "context" "errors" "fmt" + "math/rand" + "strconv" + "strings" "github.com/cilium/cilium/pkg/client" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -14,34 +18,56 @@ import ( "k8s.io/client-go/rest" ) -func createClients(ctx context.Context, name string) (*kubernetes.Clientset, *dynamic.DynamicClient, *client.Client, error) { +const ( + directionEgress = "Egress" + directionIngress = "Ingress" + + policyAllow = "Allow" + policyDeny = "Deny" +) + +var cachedCiliumClients map[string]*client.Client + +func init() { + cachedCiliumClients = make(map[string]*client.Client) +} + +func createK8sClients() (*kubernetes.Clientset, *dynamic.DynamicClient, error) { config, err := rest.InClusterConfig() if err != nil { - return nil, nil, nil, err + return nil, nil, err } - // Create Kubernetes Clients clientset, err := kubernetes.NewForConfig(config) if err != nil { - return nil, nil, nil, err + return nil, nil, err } dynamicClient, err := dynamic.NewForConfig(config) if err != nil { - return nil, nil, nil, err + return nil, nil, err } - // Create Cilium Client - endpoint, err := getProxyEndpoint(ctx, clientset, rootOptions.namespace, name) + return clientset, dynamicClient, nil +} + +func createCiliumClient(ctx context.Context, clientset *kubernetes.Clientset, namespace, name string) (*client.Client, error) { + endpoint, err := getProxyEndpoint(ctx, clientset, namespace, name) if err != nil { - return nil, nil, nil, err + return nil, err } + + if cached, ok := cachedCiliumClients[endpoint]; ok { + return cached, nil + } + ciliumClient, err := client.NewClient(endpoint) if err != nil { - return nil, nil, nil, err + return nil, err } + cachedCiliumClients[endpoint] = ciliumClient - return clientset, dynamicClient, ciliumClient, err + return ciliumClient, err } func getProxyEndpoint(ctx context.Context, c *kubernetes.Clientset, namespace, name string) (string, error) { @@ -89,3 +115,66 @@ func getPodEndpointID(ctx context.Context, d *dynamic.DynamicClient, namespace, return endpointID, nil } + +// key: identity number +// value: CiliumIdentity resource +func getIdentityResourceMap(ctx context.Context, d *dynamic.DynamicClient) (map[int]*unstructured.Unstructured, error) { + gvr := schema.GroupVersionResource{ + Group: "cilium.io", + Version: "v2", + Resource: "ciliumidentities", + } + li, err := d.Resource(gvr).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + ret := make(map[int]*unstructured.Unstructured) + for _, item := range li.Items { + id, err := strconv.Atoi(item.GetName()) + if err != nil { + return nil, err + } + ret[id] = &item + } + return ret, nil +} + +// key: identity number +// value: example pod name +func getIdentityExampleMap(ctx context.Context, d *dynamic.DynamicClient) (map[int]string, error) { + gvr := schema.GroupVersionResource{ + Group: "cilium.io", + Version: "v2", + Resource: "ciliumendpoints", + } + + li, err := d.Resource(gvr).Namespace(corev1.NamespaceAll).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + ret := make(map[int]string) + for _, ep := range li.Items { + identity, ok, err := unstructured.NestedInt64(ep.Object, "status", "identity", "id") + if err != nil { + return nil, err + } + if !ok { + continue + } + if _, ok := ret[int(identity)]; ok { + ret[int(identity)] += "," + ep.GetName() + } else { + ret[int(identity)] = ep.GetName() + } + } + for k, v := range ret { + if strings.Contains(v, ",") { + samples := strings.Split(v, ",") + i := rand.Intn(len(samples)) + ret[k] = samples[i] + } + } + return ret, nil +} diff --git a/cmd/npv/app/inspect.go b/cmd/npv/app/inspect.go new file mode 100644 index 0000000..1fc8073 --- /dev/null +++ b/cmd/npv/app/inspect.go @@ -0,0 +1,247 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "slices" + "strconv" + "strings" + "text/tabwriter" + + "github.com/cilium/cilium/api/v1/client/policy" + "github.com/cilium/cilium/pkg/identity" + "github.com/cilium/cilium/pkg/u8proto" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" +) + +func init() { + rootCmd.AddCommand(inspectCmd) +} + +var inspectCmd = &cobra.Command{ + Use: "inspect", + Short: "Inspect network policies applied to a pod", + Long: `Inspect network policies applied to a pod`, + + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runInspect(context.Background(), cmd.OutOrStdout(), args[0]) + }, +} + +type policyEntryKey struct { + Identity int `json:"Identity"` + Direction int `json:"TrafficDirection"` + Protocol int `json:"Nexthdr"` + BigPort int `json:"DestPortNetwork"` // big endian +} + +// For the meanings of the flags, see: +// https://github.com/cilium/cilium/blob/v1.16.3/bpf/lib/common.h#L394 +type policyEntry struct { + Flags int `json:"Flags"` + Packets int `json:"Packets"` + Bytes int `json:"Bytes"` + Key policyEntryKey `json:"Key"` +} + +func (p policyEntry) IsDenyRule() bool { + return (p.Flags & 1) > 0 +} + +func (p policyEntry) IsEgressRule() bool { + return p.Key.Direction > 0 +} + +func (p policyEntry) IsWildcardProtocol() bool { + return (p.Flags & 2) > 0 +} + +func (p policyEntry) IsWildcardPort() bool { + return (p.Flags & 4) > 0 +} + +// This command aims to show the result of "cilium bpf policy get" from a remote pod. +// https://github.com/cilium/cilium/blob/v1.16.3/cilium-dbg/cmd/bpf_policy_get.go +type inspectEntry struct { + Policy string `json:"policy"` + Direction string `json:"direction"` + Namespace string `json:"namespace"` + Example string `json:"example"` + Identity int `json:"identity"` + WildcardProtocol bool `json:"wildcard_protocol"` + WildcardPort bool `json:"wildcard_port"` + Protocol int `json:"protocol"` + Port int `json:"port"` + Bytes int `json:"bytes"` + Packets int `json:"packets"` +} + +func queryPolicyMap(ctx context.Context, clientset *kubernetes.Clientset, dynamicClient *dynamic.DynamicClient, namespace, name string) ([]policyEntry, error) { + endpointID, err := getPodEndpointID(ctx, dynamicClient, namespace, name) + if err != nil { + return nil, fmt.Errorf("failed to get pod endpoint ID: %w", err) + } + + url, err := getProxyEndpoint(ctx, clientset, namespace, name) + if err != nil { + return nil, fmt.Errorf("failed to get proxy endpoint: %w", err) + } + + url = fmt.Sprintf("%s/policy/%d", url, endpointID) + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to request policy: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + policies := make([]policyEntry, 0) + if err = json.Unmarshal(data, &policies); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return policies, nil +} + +func runInspect(ctx context.Context, w io.Writer, name string) error { + clientset, dynamicClient, err := createK8sClients() + if err != nil { + return err + } + + client, err := createCiliumClient(ctx, clientset, rootOptions.namespace, name) + if err != nil { + return fmt.Errorf("failed to create Cilium client: %w", err) + } + + policies, err := queryPolicyMap(ctx, clientset, dynamicClient, rootOptions.namespace, name) + if err != nil { + return err + } + + ids, err := getIdentityResourceMap(ctx, dynamicClient) + if err != nil { + return err + } + + examples, err := getIdentityExampleMap(ctx, dynamicClient) + if err != nil { + return err + } + + arr := make([]inspectEntry, len(policies)) + for i, p := range policies { + var entry inspectEntry + if p.IsDenyRule() { + entry.Policy = policyDeny + } else { + entry.Policy = policyAllow + } + if p.IsEgressRule() { + entry.Direction = directionEgress + } else { + entry.Direction = directionIngress + } + entry.Namespace = "-" + if id, ok := ids[p.Key.Identity]; ok { + ns, ok, err := unstructured.NestedString(id.Object, "security-labels", "k8s:io.kubernetes.pod.namespace") + if err != nil { + return err + } + if ok { + entry.Namespace = ns + } + } + entry.Example = "-" + if v, ok := examples[p.Key.Identity]; ok { + entry.Example = v + } else { + idObj := identity.NumericIdentity(p.Key.Identity) + if idObj.IsReservedIdentity() { + entry.Example = "reserved:" + idObj.String() + } else if idObj.HasLocalScope() { + // If the identity is in the local scope, it is only valid on the reporting node. + params := policy.GetIdentityIDParams{ + Context: ctx, + ID: strconv.FormatInt(int64(p.Key.Identity), 10), + } + response, err := client.Policy.GetIdentityID(¶ms) + if err != nil { + return fmt.Errorf("failed to get identity: %w", err) + } + if slices.Contains(response.Payload.Labels, "reserved:world") { + // If the identity is a world one, its labels contain multiple nesting CIDRs. + // For example, 0.0.0.0/0, 0.0.0.0/1, ..., 8.8.8.8/32. + // The following code finds the smallest CIDR from the list. + slices.SortFunc(response.Payload.Labels, func(a, b string) int { + aparts := strings.Split(a, "/") + bparts := strings.Split(b, "/") + if len(aparts) < 2 || len(bparts) < 2 { + return len(bparts) - len(aparts) + } + abits, _ := strconv.Atoi(aparts[1]) + bbits, _ := strconv.Atoi(bparts[1]) + return bbits - abits + }) + entry.Example = response.Payload.Labels[0] + } + } + } + entry.Identity = p.Key.Identity + entry.WildcardProtocol = p.IsWildcardProtocol() + entry.WildcardPort = p.IsWildcardPort() + entry.Protocol = p.Key.Protocol + entry.Port = ((p.Key.BigPort & 0xFF) << 8) + ((p.Key.BigPort & 0xFF00) >> 8) + entry.Bytes = p.Bytes + entry.Packets = p.Packets + arr[i] = entry + } + + // I don't know it is safe to sort the result of "cilium bpf policy get", so let's keep the original order. + switch rootOptions.output { + case OutputJson: + text, err := json.MarshalIndent(arr, "", " ") + if err != nil { + return err + } + _, err = w.Write(text) + return err + case OutputSimple: + tw := tabwriter.NewWriter(w, 0, 1, 1, ' ', 0) + if !rootOptions.noHeaders { + if _, err := tw.Write([]byte("POLICY\tDIRECTION\tIDENTITY\tNAMESPACE\tEXAMPLE\tPROTOCOL\tPORT\tBYTES\tPACKETS\n")); err != nil { + return err + } + } + for _, p := range arr { + var protocol, port string + if p.WildcardProtocol { + protocol = "ANY" + } else { + protocol = u8proto.U8proto(p.Protocol).String() + } + if p.WildcardPort { + port = "ANY" + } else { + port = strconv.Itoa(p.Port) + } + if _, err := tw.Write([]byte(fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\t%v\n", p.Policy, p.Direction, p.Identity, p.Namespace, p.Example, protocol, port, p.Bytes, p.Packets))); err != nil { + return err + } + } + return tw.Flush() + default: + return fmt.Errorf("unknown format: %s", rootOptions.output) + } +} diff --git a/cmd/npv/app/list.go b/cmd/npv/app/list.go index 0ab21c3..2cb94c2 100644 --- a/cmd/npv/app/list.go +++ b/cmd/npv/app/list.go @@ -13,9 +13,20 @@ import ( "github.com/cilium/cilium/api/v1/client/endpoint" "github.com/spf13/cobra" "golang.org/x/exp/maps" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/yaml" ) +var listOptions struct { + manifests bool +} + func init() { + listCmd.Flags().BoolVarP(&listOptions.manifests, "manifests", "m", false, "show policy manifests") rootCmd.AddCommand(listCmd) } @@ -30,11 +41,6 @@ var listCmd = &cobra.Command{ }, } -const ( - directionEgress = "EGRESS" - directionIngress = "INGRESS" -) - type derivedFromEntry struct { Direction string `json:"direction"` Kind string `json:"kind"` @@ -59,6 +65,7 @@ func lessDerivedFromEntry(x, y *derivedFromEntry) bool { func parseDerivedFromEntry(input []string, direction string) derivedFromEntry { val := derivedFromEntry{ Direction: direction, + Namespace: "-", } for _, s := range input { switch { @@ -74,9 +81,14 @@ func parseDerivedFromEntry(input []string, direction string) derivedFromEntry { } func runList(ctx context.Context, w io.Writer, name string) error { - _, dynamicClient, client, err := createClients(ctx, name) + clientset, dynamicClient, err := createK8sClients() if err != nil { - return fmt.Errorf("failed to create clients: %w", err) + return fmt.Errorf("failed to create k8s clients: %w", err) + } + + client, err := createCiliumClient(ctx, clientset, rootOptions.namespace, name) + if err != nil { + return fmt.Errorf("failed to create Cilium client: %w", err) } endpointID, err := getPodEndpointID(ctx, dynamicClient, rootOptions.namespace, name) @@ -115,6 +127,10 @@ func runList(ctx context.Context, w io.Writer, name string) error { policyList := maps.Keys(policySet) sort.Slice(policyList, func(i, j int) bool { return lessDerivedFromEntry(&policyList[i], &policyList[j]) }) + if listOptions.manifests { + return listPolicyManifests(ctx, w, dynamicClient, policyList) + } + switch rootOptions.output { case OutputJson: text, err := json.MarshalIndent(policyList, "", " ") @@ -140,3 +156,69 @@ func runList(ctx context.Context, w io.Writer, name string) error { return fmt.Errorf("unknown format: %s", rootOptions.output) } } + +func listPolicyManifests(ctx context.Context, w io.Writer, dynamicClient *dynamic.DynamicClient, policyList []derivedFromEntry) error { + // remove direction info and sort again + for i := range policyList { + policyList[i].Direction = "" + } + sort.Slice(policyList, func(i, j int) bool { return lessDerivedFromEntry(&policyList[i], &policyList[j]) }) + + var previous types.NamespacedName + first := true + for _, p := range policyList { + // a same policy may appear twice from egress and ingress rules, so we need to dedup them + next := types.NamespacedName{ + Namespace: p.Namespace, + Name: p.Name, + } + if previous == next { + continue + } + previous = next + + if !first { + if _, err := fmt.Fprintln(w, "---"); err != nil { + return err + } + } + first = false + + isCNP := p.Kind == "CiliumNetworkPolicy" + gvr := schema.GroupVersionResource{ + Group: "cilium.io", + Version: "v2", + } + var resource *unstructured.Unstructured + if isCNP { + gvr.Resource = "ciliumnetworkpolicies" + cnp, err := dynamicClient.Resource(gvr).Namespace(p.Namespace).Get(ctx, p.Name, metav1.GetOptions{}) + if err != nil { + return err + } + resource = cnp + } else { + gvr.Resource = "ciliumclusterwidenetworkpolicies" + ccnp, err := dynamicClient.Resource(gvr).Get(ctx, p.Name, metav1.GetOptions{}) + if err != nil { + return err + } + resource = ccnp + } + unstructured.RemoveNestedField(resource.Object, "metadata", "annotations", "kubectl.kubernetes.io/last-applied-configuration") + unstructured.RemoveNestedField(resource.Object, "metadata", "creationTimestamp") + unstructured.RemoveNestedField(resource.Object, "metadata", "generation") + unstructured.RemoveNestedField(resource.Object, "metadata", "managedFields") + unstructured.RemoveNestedField(resource.Object, "metadata", "resourceVersion") + unstructured.RemoveNestedField(resource.Object, "metadata", "uid") + + data, err := yaml.Marshal(resource.Object) + if err != nil { + return err + } + if _, err := fmt.Fprintf(w, "%s", string(data)); err != nil { + return err + } + } + return nil +} diff --git a/cmd/npv/app/summary.go b/cmd/npv/app/summary.go new file mode 100644 index 0000000..df0c331 --- /dev/null +++ b/cmd/npv/app/summary.go @@ -0,0 +1,117 @@ +package app + +import ( + "context" + "encoding/json" + "fmt" + "io" + "sort" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + rootCmd.AddCommand(summaryCmd) +} + +var summaryCmd = &cobra.Command{ + Use: "summary", + Short: "Show summary of network policy count", + Long: `Show summary of network policy count`, + + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runSummary(context.Background(), cmd.OutOrStdout()) + }, +} + +type summaryEntry struct { + Namespace string `json:"namespace"` + Name string `json:"name"` + IngressAllow int `json:"ingress_allow"` + IngressDeny int `json:"ingress_deny"` + EgressAllow int `json:"egress_allow"` + EgressDeny int `json:"egress_deny"` +} + +func lessSummaryEntry(x, y *summaryEntry) bool { + ret := strings.Compare(x.Namespace, y.Namespace) + if ret == 0 { + ret = strings.Compare(x.Name, y.Name) + } + return ret < 0 +} + +func runSummary(ctx context.Context, w io.Writer) error { + clientset, dynamicClient, err := createK8sClients() + if err != nil { + return err + } + + summary := make([]summaryEntry, 0) + pods, err := clientset.CoreV1().Pods(rootOptions.namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return err + } + + for _, p := range pods.Items { + var entry summaryEntry + entry.Namespace = p.Namespace + entry.Name = p.Name + + if p.Spec.HostNetwork { + entry.EgressDeny = -1 + entry.EgressAllow = -1 + entry.IngressDeny = -1 + entry.IngressAllow = -1 + } else { + policies, err := queryPolicyMap(ctx, clientset, dynamicClient, rootOptions.namespace, p.Name) + if err != nil { + return err + } + + for _, p := range policies { + switch { + case p.IsEgressRule() && p.IsDenyRule(): + entry.EgressDeny++ + case p.IsEgressRule() && !p.IsDenyRule(): + entry.EgressAllow++ + case !p.IsEgressRule() && p.IsDenyRule(): + entry.IngressDeny++ + case !p.IsEgressRule() && !p.IsDenyRule(): + entry.IngressAllow++ + } + } + } + summary = append(summary, entry) + } + sort.Slice(summary, func(i, j int) bool { return lessSummaryEntry(&summary[i], &summary[j]) }) + + switch rootOptions.output { + case OutputJson: + text, err := json.MarshalIndent(summary, "", " ") + if err != nil { + return err + } + _, err = w.Write(text) + return err + case OutputSimple: + tw := tabwriter.NewWriter(w, 0, 1, 1, ' ', 0) + if !rootOptions.noHeaders { + if _, err := tw.Write([]byte("NAMESPACE\tNAME\tINGRESS-ALLOW\tINGRESS-DENY\tEGRESS-ALLOW\tEGRESS-DENY\n")); err != nil { + return err + } + } + for _, p := range summary { + if _, err := tw.Write([]byte(fmt.Sprintf("%v\t%v\t%v\t%v\t%v\t%v\n", p.Namespace, p.Name, p.IngressAllow, p.IngressDeny, p.EgressAllow, p.EgressDeny))); err != nil { + return err + } + } + return tw.Flush() + default: + return fmt.Errorf("unknown format: %s", rootOptions.output) + } +} diff --git a/e2e/Makefile b/e2e/Makefile index 56e9a16..e879900 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -48,11 +48,11 @@ 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 - $(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-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 + $(MAKE) --no-print-directory run-test-pod-l3-egress-explicit-deny-all $(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-allow-any $(MAKE) --no-print-directory run-test-pod-l4-ingress-explicit-allow-tcp @@ -70,7 +70,9 @@ install-policy-viewer: $(MAKE) -C ../ build PODNAME=$$(kubectl get po -l app=ubuntu -o name | cut -d'/' -f2); \ kubectl cp $(POLICY_VIEWER) $${PODNAME}:/tmp/; \ - kubectl exec $${PODNAME} -- chmod +x /tmp/npv + kubectl exec $${PODNAME} -- chmod +x /tmp/npv; \ + kubectl cp $$(aqua which kubectl) $${PODNAME}:/tmp/; \ + kubectl exec $${PODNAME} -- chmod +x /tmp/kubectl .PHONY: test test: diff --git a/e2e/dump_test.go b/e2e/dump_test.go index 88a85b0..3dd175b 100644 --- a/e2e/dump_test.go +++ b/e2e/dump_test.go @@ -7,8 +7,8 @@ import ( func testDump() { It("should dump endpoint content", func() { - podName := onePodByLabelSelector(Default, "default", "test=self") - ret := runViewerSafe(Default, nil, "dump", podName) + podName := onePodByLabelSelector(Default, "test", "test=self") + ret := runViewerSafe(Default, nil, "dump", "-n=test", podName) state := jqSafe(Default, ret, "-r", ".status.state") Expect(string(state)).To(Equal("ready")) }) diff --git a/e2e/inspect_test.go b/e2e/inspect_test.go new file mode 100644 index 0000000..61c314f --- /dev/null +++ b/e2e/inspect_test.go @@ -0,0 +1,111 @@ +package e2e + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func testInspect() { + cases := []struct { + Selector string + Expected string + }{ + { + Selector: "test=self", + Expected: `Allow,Egress,cidr:8.8.8.8/32,false,false,6,53 +Allow,Egress,cidr:8.8.8.8/32,false,false,17,53 +Allow,Egress,cidr:8.8.8.8/32,false,false,132,53 +Allow,Egress,l3-ingress-explicit-allow-all,true,true,0,0 +Allow,Egress,l3-ingress-explicit-deny-all,true,true,0,0 +Allow,Egress,l3-ingress-implicit-deny-all,true,true,0,0 +Allow,Egress,l4-ingress-explicit-allow-any,false,false,6,53 +Allow,Egress,l4-ingress-explicit-allow-any,false,false,17,53 +Allow,Egress,l4-ingress-explicit-allow-any,false,false,132,53 +Allow,Egress,l4-ingress-explicit-allow-tcp,false,false,6,8080 +Allow,Egress,l4-ingress-explicit-deny-any,false,false,6,53 +Allow,Egress,l4-ingress-explicit-deny-any,false,false,17,53 +Allow,Egress,l4-ingress-explicit-deny-any,false,false,132,53 +Allow,Egress,l4-ingress-explicit-deny-udp,false,false,17,161 +Allow,Ingress,reserved:host,true,true,0,0 +Deny,Egress,cidr:8.8.4.4/32,false,false,6,53 +Deny,Egress,cidr:8.8.4.4/32,false,false,17,53 +Deny,Egress,cidr:8.8.4.4/32,false,false,132,53 +Deny,Egress,l3-egress-explicit-deny-all,true,true,0,0 +Deny,Egress,l4-egress-explicit-deny-any,false,false,6,53 +Deny,Egress,l4-egress-explicit-deny-any,false,false,17,53 +Deny,Egress,l4-egress-explicit-deny-any,false,false,132,53 +Deny,Egress,l4-egress-explicit-deny-tcp,false,false,6,8080`, + }, + { + Selector: "test=l3-ingress-explicit-allow-all", + Expected: `Allow,Ingress,reserved:host,true,true,0,0 +Allow,Ingress,self,true,true,0,0`, + }, + { + Selector: "test=l3-ingress-implicit-deny-all", + Expected: `Allow,Ingress,reserved:host,true,true,0,0`, + }, + { + Selector: "test=l3-ingress-explicit-deny-all", + Expected: `Allow,Ingress,reserved:host,true,true,0,0 +Deny,Ingress,self,true,true,0,0`, + }, + { + Selector: "test=l3-egress-implicit-deny-all", + Expected: `Allow,Ingress,reserved:host,true,true,0,0`, + }, + { + Selector: "test=l3-egress-explicit-deny-all", + Expected: `Allow,Ingress,reserved:host,true,true,0,0`, + }, + { + Selector: "test=l4-ingress-explicit-allow-any", + Expected: `Allow,Ingress,reserved:host,true,true,0,0 +Allow,Ingress,self,false,false,6,53 +Allow,Ingress,self,false,false,17,53 +Allow,Ingress,self,false,false,132,53`, + }, + { + Selector: "test=l4-ingress-explicit-allow-tcp", + Expected: `Allow,Ingress,reserved:host,true,true,0,0 +Allow,Ingress,self,false,false,6,8080`, + }, + { + Selector: "test=l4-ingress-explicit-deny-any", + Expected: `Allow,Ingress,reserved:host,true,true,0,0 +Deny,Ingress,self,false,false,6,53 +Deny,Ingress,self,false,false,17,53 +Deny,Ingress,self,false,false,132,53`, + }, + { + Selector: "test=l4-ingress-explicit-deny-udp", + Expected: `Allow,Ingress,reserved:host,true,true,0,0 +Deny,Ingress,self,false,false,17,161`, + }, + { + Selector: "test=l4-egress-explicit-deny-any", + Expected: `Allow,Ingress,reserved:host,true,true,0,0`, + }, + { + Selector: "test=l4-egress-explicit-deny-tcp", + Expected: `Allow,Ingress,reserved:host,true,true,0,0`, + }, + } + + It("should inspect policy configuration", func() { + for _, c := range cases { + podName := onePodByLabelSelector(Default, "test", c.Selector) + result := runViewerSafe(Default, nil, "inspect", "-o=json", "-n=test", podName) + // remove hash suffix from pod names + result = jqSafe(Default, result, "-r", `[.[] | .example = (.example | split("-") | .[0:5] | join("-"))]`) + result = jqSafe(Default, result, "-r", `[.[] | .example = (.example | if startswith("self") then "self" else . end)]`) + // "npv inspect" returns a unstable result, so we need to sort it in test + result = jqSafe(Default, result, "-r", `sort_by(.policy, .direction, .example, .wildcard_protocol, .wildcard_port, .protocol, .port)`) + result = jqSafe(Default, result, "-r", `.[] | [.policy, .direction, .example, .wildcard_protocol, .wildcard_port, .protocol, .port] | @csv`) + resultString := strings.Replace(string(result), `"`, "", -1) + Expect(resultString).To(Equal(c.Expected), "compare failed. selector: %s\nactual: %s\nexpected: %s", c.Selector, resultString, c.Expected) + } + }) +} diff --git a/e2e/list_test.go b/e2e/list_test.go index a5ca46a..591c558 100644 --- a/e2e/list_test.go +++ b/e2e/list_test.go @@ -14,81 +14,197 @@ func testList() { }{ { Selector: "test=self", - Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline -EGRESS,CiliumNetworkPolicy,default,l3-egress -EGRESS,CiliumNetworkPolicy,default,l4-egress -INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Egress,CiliumNetworkPolicy,test,l3-egress +Egress,CiliumNetworkPolicy,test,l4-egress +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline`, }, { - Selector: "test=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-explicit-allow-all", + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumNetworkPolicy,test,l3-ingress-explicit-allow-all`, }, { - Selector: "test=l3-ingress-implicit-deny", - Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline -INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + Selector: "test=l3-ingress-implicit-deny-all", + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline`, }, { - Selector: "test=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-ingress-explicit-deny-all", + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumNetworkPolicy,test,l3-ingress-explicit-deny-all`, }, { - Selector: "test=l3-egress-implicit-deny", - Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline -INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + Selector: "test=l3-egress-implicit-deny-all", + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline`, }, { - Selector: "test=l3-egress-explicit-deny", - Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline -INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + Selector: "test=l3-egress-explicit-deny-all", + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,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`, + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumNetworkPolicy,test,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`, + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumNetworkPolicy,test,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`, + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumNetworkPolicy,test,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`, + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumNetworkPolicy,test,l4-ingress-explicit-deny-udp`, }, { Selector: "test=l4-egress-explicit-deny-any", - Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline -INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,l3-baseline`, }, { Selector: "test=l4-egress-explicit-deny-tcp", - Expected: `EGRESS,CiliumNetworkPolicy,default,l3-baseline -INGRESS,CiliumNetworkPolicy,default,l3-baseline`, + Expected: `Egress,CiliumClusterwideNetworkPolicy,-,l3-baseline +Ingress,CiliumClusterwideNetworkPolicy,-,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", "--no-headers", podName) + podName := onePodByLabelSelector(Default, "test", c.Selector) + result := runViewerSafe(Default, nil, "list", "-o=json", "-n=test", 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) + Expect(resultString).To(Equal(c.Expected), "compare failed. selector: %s\nactual: %s\nexpected: %s", c.Selector, resultString, c.Expected) } }) } + +func testListManifests() { + expected := `apiVersion: cilium.io/v2 +kind: CiliumClusterwideNetworkPolicy +metadata: + annotations: {} + name: l3-baseline +spec: + egressDeny: + - toEndpoints: + - matchLabels: + k8s:test: scapegoat + endpointSelector: + matchLabels: + k8s:group: test + ingressDeny: + - fromEndpoints: + - matchLabels: + k8s:test: scapegoat +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + annotations: {} + name: l3-egress + namespace: test +spec: + egress: + - toEndpoints: + - matchLabels: + k8s:test: l3-ingress-explicit-allow-all + - toEndpoints: + - matchLabels: + k8s:test: l3-ingress-no-rule + - toEndpoints: + - matchLabels: + k8s:test: l3-ingress-implicit-deny-all + - toEndpoints: + - matchLabels: + k8s:test: l3-ingress-explicit-deny-all + egressDeny: + - toEndpoints: + - matchLabels: + k8s:test: l3-egress-explicit-deny-all + endpointSelector: + matchLabels: + k8s:test: self +--- +apiVersion: cilium.io/v2 +kind: CiliumNetworkPolicy +metadata: + annotations: {} + name: l4-egress + namespace: test +spec: + 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" + protocol: UDP + - toCIDR: + - 8.8.8.8/32 + toPorts: + - ports: + - port: "53" + 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 + - toCIDR: + - 8.8.4.4/32 + toPorts: + - ports: + - port: "53" + endpointSelector: + matchLabels: + k8s:test: self` + + It("should list applied policy manifests", func() { + podName := onePodByLabelSelector(Default, "test", "test=self") + result := strings.TrimSpace(string(runViewerSafe(Default, nil, "list", "-n=test", "-m", podName))) + Expect(result).To(Equal(expected), "compare failed.\nactual: %s\nexpected: %s", result, expected) + }) +} diff --git a/e2e/suite_test.go b/e2e/suite_test.go index 250b5be..dd7f8fa 100644 --- a/e2e/suite_test.go +++ b/e2e/suite_test.go @@ -25,4 +25,7 @@ var _ = Describe("Test network-policy-viewer", func() { func runTest() { Context("dump", testDump) Context("list", testList) + Context("list-manifests", testListManifests) + Context("inspect", testInspect) + Context("summary", testSummary) } diff --git a/e2e/summary_test.go b/e2e/summary_test.go new file mode 100644 index 0000000..0b9665e --- /dev/null +++ b/e2e/summary_test.go @@ -0,0 +1,33 @@ +package e2e + +import ( + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +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-deny-all,1,1,0,0 +l3-ingress-implicit-deny-all,1,0,0,0 +l4-egress-explicit-deny-any,1,0,0,0 +l4-egress-explicit-deny-tcp,1,0,0,0 +l4-ingress-explicit-allow-any,4,0,0,0 +l4-ingress-explicit-allow-tcp,2,0,0,0 +l4-ingress-explicit-deny-any,1,3,0,0 +l4-ingress-explicit-deny-udp,1,1,0,0 +self,1,0,14,8` + + It("should show summary", func() { + result := runViewerSafe(Default, nil, "summary", "-o=json", "-n=test") + // 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", `.[] | [.name, .ingress_allow, .ingress_deny, .egress_allow, .egress_deny] | @csv`) + resultString := strings.Replace(string(result), `"`, "", -1) + Expect(resultString).To(Equal(expected), "compare failed.\nactual: %s\nexpected: %s", resultString, expected) + }) +} diff --git a/e2e/testdata/cilium-agent-proxy.yaml b/e2e/testdata/cilium-agent-proxy.yaml index 0727244..6688dd4 100644 --- a/e2e/testdata/cilium-agent-proxy.yaml +++ b/e2e/testdata/cilium-agent-proxy.yaml @@ -24,6 +24,8 @@ spec: mountPath: /sys/fs/bpf securityContext: capabilities: + add: + - BPF drop: - ALL volumes: diff --git a/e2e/testdata/kustomization.yaml b/e2e/testdata/kustomization.yaml index a3e7cf8..8b69462 100644 --- a/e2e/testdata/kustomization.yaml +++ b/e2e/testdata/kustomization.yaml @@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - cilium-agent-proxy.yaml + - namespace.yaml - ubuntu.yaml images: - name: ghcr.io/cybozu-go/cilium-agent-proxy diff --git a/e2e/testdata/namespace.yaml b/e2e/testdata/namespace.yaml new file mode 100644 index 0000000..7c265c0 --- /dev/null +++ b/e2e/testdata/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: test diff --git a/e2e/testdata/policy/README.md b/e2e/testdata/policy/README.md index d685ede..4f03477 100644 --- a/e2e/testdata/policy/README.md +++ b/e2e/testdata/policy/README.md @@ -2,14 +2,16 @@ | 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 | - | +| l3-ingress-explicit-allow-all | allow | allow | +| l3-ingress-implicit-deny-all | allow | - | +| l3-ingress-explicit-deny-all | allow | deny | +| l3-egress-implicit-deny-all | - | - | +| l3-egress-explicit-deny-all | 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) | - | +| 8.8.8.8 (Google Public DNS) | allow (L4) | - | +| 8.8.4.4 (Google Public DNS) | deny (L4) | - | diff --git a/e2e/testdata/policy/l3.yaml b/e2e/testdata/policy/l3.yaml index 15c42ef..b7352a9 100644 --- a/e2e/testdata/policy/l3.yaml +++ b/e2e/testdata/policy/l3.yaml @@ -1,6 +1,7 @@ apiVersion: cilium.io/v2 -kind: CiliumNetworkPolicy +kind: CiliumClusterwideNetworkPolicy metadata: + namespace: test name: l3-baseline spec: endpointSelector: @@ -18,6 +19,7 @@ spec: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: + namespace: test name: l3-egress spec: endpointSelector: @@ -26,29 +28,30 @@ spec: egress: - toEndpoints: - matchLabels: - k8s:test: l3-ingress-explicit-allow + k8s:test: l3-ingress-explicit-allow-all - toEndpoints: - matchLabels: k8s:test: l3-ingress-no-rule - toEndpoints: - matchLabels: - k8s:test: l3-ingress-implicit-deny + k8s:test: l3-ingress-implicit-deny-all - toEndpoints: - matchLabels: - k8s:test: l3-ingress-explicit-deny + k8s:test: l3-ingress-explicit-deny-all egressDeny: - toEndpoints: - matchLabels: - k8s:test: l3-egress-explicit-deny + k8s:test: l3-egress-explicit-deny-all --- apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: - name: l3-ingress-explicit-allow + namespace: test + name: l3-ingress-explicit-allow-all spec: endpointSelector: matchLabels: - k8s:test: l3-ingress-explicit-allow + k8s:test: l3-ingress-explicit-allow-all ingress: - fromEndpoints: - matchLabels: @@ -57,11 +60,12 @@ spec: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: - name: l3-ingress-explicit-deny + namespace: test + name: l3-ingress-explicit-deny-all spec: endpointSelector: matchLabels: - k8s:test: l3-ingress-explicit-deny + k8s:test: l3-ingress-explicit-deny-all ingressDeny: - fromEndpoints: - matchLabels: diff --git a/e2e/testdata/policy/l4.yaml b/e2e/testdata/policy/l4.yaml index 565d6a2..f1d3051 100644 --- a/e2e/testdata/policy/l4.yaml +++ b/e2e/testdata/policy/l4.yaml @@ -1,6 +1,7 @@ apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: + namespace: test name: l4-egress spec: endpointSelector: @@ -33,6 +34,11 @@ spec: - ports: - port: "161" # SNMP (UDP) protocol: UDP + - toCIDR: + - 8.8.8.8/32 + toPorts: + - ports: + - port: "53" egressDeny: - toEndpoints: - matchLabels: @@ -47,10 +53,16 @@ spec: - ports: - port: "8080" protocol: TCP + - toCIDR: + - 8.8.4.4/32 + toPorts: + - ports: + - port: "53" --- apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: + namespace: test name: l4-ingress-explicit-allow-any spec: endpointSelector: @@ -67,6 +79,7 @@ spec: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: + namespace: test name: l4-ingress-explicit-allow-tcp spec: endpointSelector: @@ -84,6 +97,7 @@ spec: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: + namespace: test name: l4-ingress-explicit-deny-any spec: endpointSelector: @@ -100,6 +114,7 @@ spec: apiVersion: cilium.io/v2 kind: CiliumNetworkPolicy metadata: + namespace: test name: l4-ingress-explicit-deny-udp spec: endpointSelector: diff --git a/e2e/testdata/template/ubuntu.yaml b/e2e/testdata/template/ubuntu.yaml index 31aaf69..2dc8cb7 100644 --- a/e2e/testdata/template/ubuntu.yaml +++ b/e2e/testdata/template/ubuntu.yaml @@ -1,7 +1,7 @@ apiVersion: apps/v1 kind: Deployment metadata: - namespace: default + namespace: test name: ubuntu spec: replicas: 1 @@ -13,7 +13,6 @@ spec: labels: app: ubuntu spec: - serviceAccountName: ubuntu securityContext: runAsUser: 1000 runAsGroup: 1000 diff --git a/e2e/testdata/ubuntu.yaml b/e2e/testdata/ubuntu.yaml index 2319064..30d8f47 100644 --- a/e2e/testdata/ubuntu.yaml +++ b/e2e/testdata/ubuntu.yaml @@ -21,6 +21,9 @@ rules: - cilium.io resources: - ciliumendpoints + - ciliumidentities + - ciliumnetworkpolicies + - ciliumclusterwidenetworkpolicies verbs: - get - list diff --git a/go.mod b/go.mod index 0904b62..c919a74 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,10 @@ require ( 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/api v0.29.3 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 + sigs.k8s.io/yaml v1.4.0 ) require ( @@ -92,11 +94,9 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.29.3 // indirect k8s.io/klog/v2 v2.120.0 // indirect k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910 // indirect k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect )