Skip to content

Commit 10f88e5

Browse files
yokazechez-shanpu
andauthored
Implement manifest command (#16)
* Implement manifest command Signed-off-by: Daichi Sakaue <[email protected]> Co-authored-by: Tomoki Sugiura <[email protected]>
1 parent c57e825 commit 10f88e5

File tree

12 files changed

+498
-5
lines changed

12 files changed

+498
-5
lines changed

cmd/npv/app/const.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,9 @@ var gvrClusterwideNetworkPolicy schema.GroupVersionResource = schema.GroupVersio
3333
Version: "v2",
3434
Resource: "ciliumclusterwidenetworkpolicies",
3535
}
36+
37+
var gvkNetworkPolicy schema.GroupVersionKind = schema.GroupVersionKind{
38+
Group: "cilium.io",
39+
Version: "v2",
40+
Kind: "CiliumNetworkPolicy",
41+
}

cmd/npv/app/helper.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
corev1 "k8s.io/api/core/v1"
1515
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1616
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17+
"k8s.io/apimachinery/pkg/types"
1718
"k8s.io/client-go/dynamic"
1819
"k8s.io/client-go/kubernetes"
1920
"k8s.io/client-go/rest"
@@ -97,12 +98,29 @@ func getPodEndpointID(ctx context.Context, d *dynamic.DynamicClient, namespace,
9798
return 0, err
9899
}
99100
if !found {
100-
return 0, errors.New("endpoint resource is broken")
101+
return 0, fmt.Errorf("endpoint resource %s/%s is broken", namespace, name)
101102
}
102103

103104
return endpointID, nil
104105
}
105106

107+
func getPodIdentity(ctx context.Context, d *dynamic.DynamicClient, namespace, name string) (int64, error) {
108+
ep, err := d.Resource(gvrEndpoint).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
109+
if err != nil {
110+
return 0, err
111+
}
112+
113+
identity, found, err := unstructured.NestedInt64(ep.Object, "status", "identity", "id")
114+
if err != nil {
115+
return 0, err
116+
}
117+
if !found {
118+
return 0, fmt.Errorf("pod %s/%s does not have security identity", namespace, name)
119+
}
120+
121+
return identity, nil
122+
}
123+
106124
// key: identity number
107125
// value: CiliumIdentity resource
108126
func getIdentityResourceMap(ctx context.Context, d *dynamic.DynamicClient) (map[int]*unstructured.Unstructured, error) {
@@ -145,6 +163,14 @@ func getIdentityEndpoints(ctx context.Context, d *dynamic.DynamicClient) (map[in
145163
return ret, nil
146164
}
147165

166+
func parseNamespacedName(nn string) (types.NamespacedName, error) {
167+
li := strings.Split(nn, "/")
168+
if len(li) != 2 {
169+
return types.NamespacedName{}, errors.New("input is not NAMESPACE/NAME")
170+
}
171+
return types.NamespacedName{Namespace: li[0], Name: li[1]}, nil
172+
}
173+
148174
func writeSimpleOrJson(w io.Writer, content any, header []string, count int, values func(index int) []any) error {
149175
switch rootOptions.output {
150176
case OutputJson:

cmd/npv/app/manifest.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package app
2+
3+
import "github.com/spf13/cobra"
4+
5+
func init() {
6+
rootCmd.AddCommand(manifestCmd)
7+
}
8+
9+
var manifestCmd = &cobra.Command{
10+
Use: "manifest",
11+
Short: "Generate CiliumNetworkPolicy",
12+
Long: `Generate CiliumNetworkPolicy`,
13+
}

cmd/npv/app/manifest_generate.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"strconv"
9+
10+
"github.com/spf13/cobra"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+
"sigs.k8s.io/yaml"
14+
)
15+
16+
var manifestGenerateOptions struct {
17+
name string
18+
egress bool
19+
ingress bool
20+
allow bool
21+
deny bool
22+
from string
23+
to string
24+
}
25+
26+
func init() {
27+
manifestGenerateCmd.Flags().StringVar(&manifestGenerateOptions.name, "name", "", "resource name")
28+
manifestGenerateCmd.Flags().BoolVar(&manifestGenerateOptions.egress, "egress", false, "generate egress rule")
29+
manifestGenerateCmd.Flags().BoolVar(&manifestGenerateOptions.ingress, "ingress", false, "generate ingress rule")
30+
manifestGenerateCmd.Flags().BoolVar(&manifestGenerateOptions.allow, "allow", false, "generate allow rule")
31+
manifestGenerateCmd.Flags().BoolVar(&manifestGenerateOptions.deny, "deny", false, "generate deny rule")
32+
manifestGenerateCmd.Flags().StringVar(&manifestGenerateOptions.from, "from", "", "egress pod")
33+
manifestGenerateCmd.Flags().StringVar(&manifestGenerateOptions.to, "to", "", "ingress pod")
34+
manifestCmd.AddCommand(manifestGenerateCmd)
35+
}
36+
37+
var manifestGenerateCmd = &cobra.Command{
38+
Use: "generate",
39+
Short: "Generate CiliumNetworkPolicy",
40+
Long: `Generate CiliumNetworkPolicy`,
41+
42+
Args: cobra.ExactArgs(0),
43+
RunE: func(cmd *cobra.Command, args []string) error {
44+
return runManifestGenerate(context.Background(), cmd.OutOrStdout())
45+
},
46+
}
47+
48+
func runManifestGenerate(ctx context.Context, w io.Writer) error {
49+
egress := manifestGenerateOptions.egress
50+
ingress := manifestGenerateOptions.ingress
51+
allow := manifestGenerateOptions.allow
52+
deny := manifestGenerateOptions.deny
53+
from := manifestGenerateOptions.from
54+
to := manifestGenerateOptions.to
55+
56+
if egress == ingress {
57+
return errors.New("one of --egress or --ingress should be specified")
58+
}
59+
if allow == deny {
60+
return errors.New("one of --allow or --deny should be specified")
61+
}
62+
63+
sub, err := parseNamespacedName(from)
64+
if err != nil {
65+
return errors.New("--from and --to should be specified as NAMESPACE/POD")
66+
}
67+
68+
obj, err := parseNamespacedName(to)
69+
if err != nil {
70+
return errors.New("--from and --to should be specified as NAMESPACE/POD")
71+
}
72+
73+
if ingress {
74+
sub, obj = obj, sub
75+
}
76+
77+
// Parameters are all up, let's start querying API server
78+
_, dynamicClient, err := createK8sClients()
79+
if err != nil {
80+
return err
81+
}
82+
83+
subIdentity, err := getPodIdentity(ctx, dynamicClient, sub.Namespace, sub.Name)
84+
if err != nil {
85+
return err
86+
}
87+
88+
subResource, err := dynamicClient.Resource(gvrIdentity).Get(ctx, strconv.Itoa(int(subIdentity)), metav1.GetOptions{})
89+
if err != nil {
90+
return err
91+
}
92+
93+
subLabels, ok, err := unstructured.NestedStringMap(subResource.Object, "security-labels")
94+
if err != nil {
95+
return err
96+
}
97+
if !ok {
98+
return fmt.Errorf("pod %s/%s is not assigned security labels", sub.Namespace, sub.Name)
99+
}
100+
101+
objIdentity, err := getPodIdentity(ctx, dynamicClient, obj.Namespace, obj.Name)
102+
if err != nil {
103+
return err
104+
}
105+
106+
objResource, err := dynamicClient.Resource(gvrIdentity).Get(ctx, strconv.Itoa(int(objIdentity)), metav1.GetOptions{})
107+
if err != nil {
108+
return err
109+
}
110+
111+
objLabels, ok, err := unstructured.NestedStringMap(objResource.Object, "security-labels")
112+
if err != nil {
113+
return err
114+
}
115+
if !ok {
116+
return fmt.Errorf("pod %s/%s is not assigned security labels", obj.Namespace, obj.Name)
117+
}
118+
119+
policyName := manifestGenerateOptions.name
120+
if policyName == "" {
121+
direction := "egress"
122+
policy := "allow"
123+
if ingress {
124+
direction = "ingress"
125+
}
126+
if deny {
127+
policy = "deny"
128+
}
129+
policyName = fmt.Sprintf("%s-%s-%d-%d", direction, policy, subIdentity, objIdentity)
130+
}
131+
132+
var manifest unstructured.Unstructured
133+
manifest.SetGroupVersionKind(gvkNetworkPolicy)
134+
manifest.SetNamespace(sub.Namespace)
135+
manifest.SetName(policyName)
136+
err = unstructured.SetNestedStringMap(manifest.Object, subLabels, "spec", "endpointSelector", "matchLabels")
137+
if err != nil {
138+
return err
139+
}
140+
141+
objMap := make(map[string]any)
142+
for k, v := range objLabels {
143+
objMap[k] = v
144+
}
145+
146+
var section, field string
147+
switch {
148+
case egress && allow:
149+
section = "egress"
150+
field = "toEndpoints"
151+
case egress && deny:
152+
section = "egressDeny"
153+
field = "toEndpoints"
154+
case ingress && allow:
155+
section = "ingress"
156+
field = "fromEndpoints"
157+
case ingress && deny:
158+
section = "ingressDeny"
159+
field = "fromEndpoints"
160+
}
161+
162+
err = unstructured.SetNestedField(manifest.Object, []any{
163+
map[string]any{
164+
field: []any{
165+
map[string]any{
166+
"matchLabels": objMap,
167+
},
168+
},
169+
},
170+
}, "spec", section)
171+
if err != nil {
172+
return err
173+
}
174+
175+
data, err := yaml.Marshal(manifest.Object)
176+
if err != nil {
177+
return err
178+
}
179+
if _, err := fmt.Fprintf(w, "%s", string(data)); err != nil {
180+
return err
181+
}
182+
return nil
183+
}

cmd/npv/app/manifest_range.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package app
2+
3+
import (
4+
"context"
5+
"errors"
6+
"io"
7+
"sort"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var manifestRangeOptions struct {
14+
from string
15+
to string
16+
}
17+
18+
func init() {
19+
manifestRangeCmd.Flags().StringVar(&manifestRangeOptions.from, "from", "", "egress pod")
20+
manifestRangeCmd.Flags().StringVar(&manifestRangeOptions.to, "to", "", "ingress pod")
21+
manifestCmd.AddCommand(manifestRangeCmd)
22+
}
23+
24+
var manifestRangeCmd = &cobra.Command{
25+
Use: "range",
26+
Short: "List affected pods of a generated manifest",
27+
Long: `List affected pods of a generated manifest`,
28+
29+
Args: cobra.ExactArgs(0),
30+
RunE: func(cmd *cobra.Command, args []string) error {
31+
return runManifestRange(context.Background(), cmd.OutOrStdout())
32+
},
33+
}
34+
35+
type manifestRangeEntry struct {
36+
Part string `json:"part"`
37+
Namespace string `json:"namespace"`
38+
Name string `json:"name"`
39+
}
40+
41+
func lessManifestRangeEntry(x, y *manifestRangeEntry) bool {
42+
ret := strings.Compare(x.Part, y.Part)
43+
if ret == 0 {
44+
ret = strings.Compare(x.Namespace, y.Namespace)
45+
}
46+
if ret == 0 {
47+
ret = strings.Compare(x.Name, y.Name)
48+
}
49+
return ret < 0
50+
}
51+
52+
func runManifestRange(ctx context.Context, w io.Writer) error {
53+
if manifestRangeOptions.from == "" || manifestRangeOptions.to == "" {
54+
return errors.New("--from and --to options are required")
55+
}
56+
57+
from, err := parseNamespacedName(manifestRangeOptions.from)
58+
if err != nil {
59+
return errors.New("--from and --to should be specified as NAMESPACE/POD")
60+
}
61+
62+
to, err := parseNamespacedName(manifestRangeOptions.to)
63+
if err != nil {
64+
return errors.New("--from and --to should be specified as NAMESPACE/POD")
65+
}
66+
67+
_, dynamicClient, err := createK8sClients()
68+
if err != nil {
69+
return err
70+
}
71+
72+
fromIdentity, err := getPodIdentity(ctx, dynamicClient, from.Namespace, from.Name)
73+
if err != nil {
74+
return err
75+
}
76+
77+
toIdentity, err := getPodIdentity(ctx, dynamicClient, to.Namespace, to.Name)
78+
if err != nil {
79+
return err
80+
}
81+
82+
idEndpoints, err := getIdentityEndpoints(ctx, dynamicClient)
83+
if err != nil {
84+
return err
85+
}
86+
87+
arr := make([]manifestRangeEntry, 0)
88+
sort.Slice(arr, func(i, j int) bool { return lessManifestRangeEntry(&arr[i], &arr[j]) })
89+
90+
for _, ep := range idEndpoints[int(fromIdentity)] {
91+
entry := manifestRangeEntry{
92+
Part: "From",
93+
Namespace: ep.GetNamespace(),
94+
Name: ep.GetName(),
95+
}
96+
arr = append(arr, entry)
97+
}
98+
for _, ep := range idEndpoints[int(toIdentity)] {
99+
entry := manifestRangeEntry{
100+
Part: "To",
101+
Namespace: ep.GetNamespace(),
102+
Name: ep.GetName(),
103+
}
104+
arr = append(arr, entry)
105+
}
106+
return writeSimpleOrJson(w, arr, []string{"PART", "NAMESPACE", "NAME"}, len(arr), func(index int) []any {
107+
ep := arr[index]
108+
return []any{ep.Part, ep.Namespace, ep.Name}
109+
})
110+
}

e2e/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ CACHE_DIR := $(shell pwd)/../cache
77
POLICY_VIEWER := $(BIN_DIR)/npv
88
HELM := helm --repository-cache $(CACHE_DIR)/helm/repository --repository-config $(CACHE_DIR)/helm/repositories.yaml
99

10+
DEPLOYMENT_REPLICAS ?= 1
11+
1012
##@ Basic
1113

1214
.PHONY: help
@@ -41,14 +43,15 @@ run-test-pod-%:
4143
@echo Hello | yq > /dev/null
4244
cat testdata/template/ubuntu.yaml | \
4345
yq '.metadata.name = "$*"' | \
46+
yq '.spec.replicas = $(DEPLOYMENT_REPLICAS)' | \
4447
yq '.spec.selector.matchLabels = {"test": "$*"}' | \
4548
yq '.spec.template.metadata.labels = {"test": "$*", "group": "test"}' | \
4649
kubectl apply -f -
4750

4851
.PHONY: install-test-pod
4952
install-test-pod:
5053
$(MAKE) --no-print-directory run-test-pod-self
51-
$(MAKE) --no-print-directory run-test-pod-l3-ingress-explicit-allow-all
54+
$(MAKE) --no-print-directory DEPLOYMENT_REPLICAS=2 run-test-pod-l3-ingress-explicit-allow-all
5255
$(MAKE) --no-print-directory run-test-pod-l3-ingress-implicit-deny-all
5356
$(MAKE) --no-print-directory run-test-pod-l3-ingress-explicit-deny-all
5457
$(MAKE) --no-print-directory run-test-pod-l3-egress-implicit-deny-all

0 commit comments

Comments
 (0)