Skip to content

Commit

Permalink
Implement manifest command (#16)
Browse files Browse the repository at this point in the history
* Implement manifest command

Signed-off-by: Daichi Sakaue <[email protected]>
Co-authored-by: Tomoki Sugiura <[email protected]>
  • Loading branch information
yokaze and chez-shanpu authored Nov 25, 2024
1 parent c57e825 commit 10f88e5
Show file tree
Hide file tree
Showing 12 changed files with 498 additions and 5 deletions.
6 changes: 6 additions & 0 deletions cmd/npv/app/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
28 changes: 27 additions & 1 deletion cmd/npv/app/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
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/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -97,12 +98,29 @@ func getPodEndpointID(ctx context.Context, d *dynamic.DynamicClient, namespace,
return 0, err
}
if !found {
return 0, errors.New("endpoint resource is broken")
return 0, fmt.Errorf("endpoint resource %s/%s is broken", namespace, name)
}

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, fmt.Errorf("pod %s/%s does not have security identity", namespace, name)
}

return identity, nil
}

// key: identity number
// value: CiliumIdentity resource
func getIdentityResourceMap(ctx context.Context, d *dynamic.DynamicClient) (map[int]*unstructured.Unstructured, error) {
Expand Down Expand Up @@ -145,6 +163,14 @@ func getIdentityEndpoints(ctx context.Context, d *dynamic.DynamicClient) (map[in
return ret, nil
}

func parseNamespacedName(nn string) (types.NamespacedName, error) {
li := strings.Split(nn, "/")
if len(li) != 2 {
return types.NamespacedName{}, errors.New("input is not NAMESPACE/NAME")
}
return types.NamespacedName{Namespace: li[0], Name: li[1]}, nil
}

func writeSimpleOrJson(w io.Writer, content any, header []string, count int, values func(index int) []any) error {
switch rootOptions.output {
case OutputJson:
Expand Down
13 changes: 13 additions & 0 deletions cmd/npv/app/manifest.go
Original file line number Diff line number Diff line change
@@ -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`,
}
183 changes: 183 additions & 0 deletions cmd/npv/app/manifest_generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package app

import (
"context"
"errors"
"fmt"
"io"
"strconv"

"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"sigs.k8s.io/yaml"
)

var manifestGenerateOptions struct {
name string
egress bool
ingress bool
allow bool
deny bool
from string
to string
}

func init() {
manifestGenerateCmd.Flags().StringVar(&manifestGenerateOptions.name, "name", "", "resource name")
manifestGenerateCmd.Flags().BoolVar(&manifestGenerateOptions.egress, "egress", false, "generate egress rule")
manifestGenerateCmd.Flags().BoolVar(&manifestGenerateOptions.ingress, "ingress", false, "generate ingress rule")
manifestGenerateCmd.Flags().BoolVar(&manifestGenerateOptions.allow, "allow", false, "generate allow rule")
manifestGenerateCmd.Flags().BoolVar(&manifestGenerateOptions.deny, "deny", false, "generate deny rule")
manifestGenerateCmd.Flags().StringVar(&manifestGenerateOptions.from, "from", "", "egress pod")
manifestGenerateCmd.Flags().StringVar(&manifestGenerateOptions.to, "to", "", "ingress pod")
manifestCmd.AddCommand(manifestGenerateCmd)
}

var manifestGenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate CiliumNetworkPolicy",
Long: `Generate CiliumNetworkPolicy`,

Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
return runManifestGenerate(context.Background(), cmd.OutOrStdout())
},
}

func runManifestGenerate(ctx context.Context, w io.Writer) error {
egress := manifestGenerateOptions.egress
ingress := manifestGenerateOptions.ingress
allow := manifestGenerateOptions.allow
deny := manifestGenerateOptions.deny
from := manifestGenerateOptions.from
to := manifestGenerateOptions.to

if egress == ingress {
return errors.New("one of --egress or --ingress should be specified")
}
if allow == deny {
return errors.New("one of --allow or --deny should be specified")
}

sub, err := parseNamespacedName(from)
if err != nil {
return errors.New("--from and --to should be specified as NAMESPACE/POD")
}

obj, err := parseNamespacedName(to)
if err != nil {
return errors.New("--from and --to should be specified as NAMESPACE/POD")
}

if ingress {
sub, obj = obj, sub
}

// Parameters are all up, let's start querying API server
_, dynamicClient, err := createK8sClients()
if err != nil {
return err
}

subIdentity, err := getPodIdentity(ctx, dynamicClient, sub.Namespace, sub.Name)
if err != nil {
return err
}

subResource, err := dynamicClient.Resource(gvrIdentity).Get(ctx, strconv.Itoa(int(subIdentity)), metav1.GetOptions{})
if err != nil {
return err
}

subLabels, ok, err := unstructured.NestedStringMap(subResource.Object, "security-labels")
if err != nil {
return err
}
if !ok {
return fmt.Errorf("pod %s/%s is not assigned security labels", sub.Namespace, sub.Name)
}

objIdentity, err := getPodIdentity(ctx, dynamicClient, obj.Namespace, obj.Name)
if err != nil {
return err
}

objResource, err := dynamicClient.Resource(gvrIdentity).Get(ctx, strconv.Itoa(int(objIdentity)), metav1.GetOptions{})
if err != nil {
return err
}

objLabels, ok, err := unstructured.NestedStringMap(objResource.Object, "security-labels")
if err != nil {
return err
}
if !ok {
return fmt.Errorf("pod %s/%s is not assigned security labels", obj.Namespace, obj.Name)
}

policyName := manifestGenerateOptions.name
if policyName == "" {
direction := "egress"
policy := "allow"
if ingress {
direction = "ingress"
}
if deny {
policy = "deny"
}
policyName = fmt.Sprintf("%s-%s-%d-%d", direction, policy, subIdentity, objIdentity)
}

var manifest unstructured.Unstructured
manifest.SetGroupVersionKind(gvkNetworkPolicy)
manifest.SetNamespace(sub.Namespace)
manifest.SetName(policyName)
err = unstructured.SetNestedStringMap(manifest.Object, subLabels, "spec", "endpointSelector", "matchLabels")
if err != nil {
return err
}

objMap := make(map[string]any)
for k, v := range objLabels {
objMap[k] = v
}

var section, field string
switch {
case egress && allow:
section = "egress"
field = "toEndpoints"
case egress && deny:
section = "egressDeny"
field = "toEndpoints"
case ingress && allow:
section = "ingress"
field = "fromEndpoints"
case ingress && deny:
section = "ingressDeny"
field = "fromEndpoints"
}

err = unstructured.SetNestedField(manifest.Object, []any{
map[string]any{
field: []any{
map[string]any{
"matchLabels": objMap,
},
},
},
}, "spec", section)
if err != nil {
return err
}

data, err := yaml.Marshal(manifest.Object)
if err != nil {
return err
}
if _, err := fmt.Fprintf(w, "%s", string(data)); err != nil {
return err
}
return nil
}
110 changes: 110 additions & 0 deletions cmd/npv/app/manifest_range.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package app

import (
"context"
"errors"
"io"
"sort"
"strings"

"github.com/spf13/cobra"
)

var manifestRangeOptions struct {
from string
to string
}

func init() {
manifestRangeCmd.Flags().StringVar(&manifestRangeOptions.from, "from", "", "egress pod")
manifestRangeCmd.Flags().StringVar(&manifestRangeOptions.to, "to", "", "ingress pod")
manifestCmd.AddCommand(manifestRangeCmd)
}

var manifestRangeCmd = &cobra.Command{
Use: "range",
Short: "List affected pods of a generated manifest",
Long: `List affected pods of a generated manifest`,

Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
return runManifestRange(context.Background(), cmd.OutOrStdout())
},
}

type manifestRangeEntry struct {
Part string `json:"part"`
Namespace string `json:"namespace"`
Name string `json:"name"`
}

func lessManifestRangeEntry(x, y *manifestRangeEntry) bool {
ret := strings.Compare(x.Part, y.Part)
if ret == 0 {
ret = strings.Compare(x.Namespace, y.Namespace)
}
if ret == 0 {
ret = strings.Compare(x.Name, y.Name)
}
return ret < 0
}

func runManifestRange(ctx context.Context, w io.Writer) error {
if manifestRangeOptions.from == "" || manifestRangeOptions.to == "" {
return errors.New("--from and --to options are required")
}

from, err := parseNamespacedName(manifestRangeOptions.from)
if err != nil {
return errors.New("--from and --to should be specified as NAMESPACE/POD")
}

to, err := parseNamespacedName(manifestRangeOptions.to)
if err != nil {
return errors.New("--from and --to should be specified as NAMESPACE/POD")
}

_, dynamicClient, err := createK8sClients()
if err != nil {
return err
}

fromIdentity, err := getPodIdentity(ctx, dynamicClient, from.Namespace, from.Name)
if err != nil {
return err
}

toIdentity, err := getPodIdentity(ctx, dynamicClient, to.Namespace, to.Name)
if err != nil {
return err
}

idEndpoints, err := getIdentityEndpoints(ctx, dynamicClient)
if err != nil {
return err
}

arr := make([]manifestRangeEntry, 0)
sort.Slice(arr, func(i, j int) bool { return lessManifestRangeEntry(&arr[i], &arr[j]) })

for _, ep := range idEndpoints[int(fromIdentity)] {
entry := manifestRangeEntry{
Part: "From",
Namespace: ep.GetNamespace(),
Name: ep.GetName(),
}
arr = append(arr, entry)
}
for _, ep := range idEndpoints[int(toIdentity)] {
entry := manifestRangeEntry{
Part: "To",
Namespace: ep.GetNamespace(),
Name: ep.GetName(),
}
arr = append(arr, entry)
}
return writeSimpleOrJson(w, arr, []string{"PART", "NAMESPACE", "NAME"}, len(arr), func(index int) []any {
ep := arr[index]
return []any{ep.Part, ep.Namespace, ep.Name}
})
}
5 changes: 4 additions & 1 deletion e2e/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -41,14 +43,15 @@ 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 -

.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
Expand Down
Loading

0 comments on commit 10f88e5

Please sign in to comment.