diff --git a/DESIGN.md b/DESIGN.md index c1c64c0..bdcd7c3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -81,9 +81,7 @@ communications: Handling of capabilities: -1. capabilities at namespace level apply to each individual pod in the - namespace -2. a capability is a list of templates. +1. capabilities at namespace level is defined a template that gets the namespace name. Ingress template diff --git a/cmd/policygen/config.go b/cmd/policygen/config.go index af9c18f..da52a97 100644 --- a/cmd/policygen/config.go +++ b/cmd/policygen/config.go @@ -1,12 +1,17 @@ package main import ( + "bytes" "errors" "fmt" "github.com/goccy/go-yaml" "net" + "os" + "slices" ) +var PREDEFINED_APPS = []string{"apiserver"} + func validateCIDR(cidr string) error { _, _, err := net.ParseCIDR(cidr) return err @@ -36,21 +41,24 @@ func (c CIDR) MarshalYAML() ([]byte, error) { // Network represents each network entry in the YAML type Network struct { - Name string `yaml:"name"` - CIDR CIDR `yaml:"cidr"` - Except []CIDR `yaml:"except,omitempty"` + Name string `yaml:"name"` + CIDR CIDR `yaml:"cidr"` + Except []CIDR `yaml:"except,omitempty"` + Ports []string `yaml:"ports,omitempty"` } type Application struct { Name string `yaml:"name"` Ports []string `yaml:"ports,omitempty"` MatchLabels map[string]string `yaml:"matchLabels"` + Namespace string `yaml:"-"` } type Namespace struct { - Name string `yaml:"name"` - Capabilities []string `yaml:"capabilities"` - Applications []Application `yaml:"applications"` + Name string `yaml:"name"` + Open bool `yaml:"open"` + Capabilities []string `yaml:"capabilities"` + Applications []*Application `yaml:"applications"` } type Communication struct { @@ -61,9 +69,9 @@ type Communication struct { // Config represents the top-level YAML structure type Config struct { - Networks []Network `yaml:"networks,omitempty"` - Namespaces []Namespace `yaml:"namespaces,omitempty"` - Communications []Communication `yaml:"communications,omitempty"` + Networks []*Network `yaml:"networks,omitempty"` + Namespaces []*Namespace `yaml:"namespaces,omitempty"` + Communications []*Communication `yaml:"communications,omitempty"` } func (c Config) Validate() error { @@ -88,8 +96,15 @@ func (c Config) Validate() error { networks[network.Name] = true } - // application names must be unique - apps := make(map[string]bool) + // application names must be unique and may not conflict with predefined applications + apps := map[string]bool{ + "apiserver": true, + } + // application names may also not conflict with network names. + for _, network := range c.Networks { + apps[network.Name] = true + } + for _, namespace := range c.Namespaces { for _, app := range namespace.Applications { if apps[app.Name] { @@ -115,3 +130,54 @@ func (c Config) Validate() error { return errors.Join(errs...) } + +func (c Config) GetApplication(name string) (*Application, *Network, string) { + if slices.Contains(PREDEFINED_APPS, name) { + return nil, nil, name + } + for _, network := range c.Networks { + if name == network.Name { + return nil, network, "" + } + } + for _, ns := range c.Namespaces { + for _, app := range ns.Applications { + if app.Name == name { + return app, nil, "" + } + } + } + return nil, nil, "" +} + +func LoadConfig(file string) (*Config, error) { + fmt.Fprintf(os.Stderr, "Reading config %s\n", file) + yamlFile, err := os.ReadFile(file) + if err != nil { + return nil, fmt.Errorf("Error reading YAML file: %v", err) + } + + // Parse the YAML content + dec := yaml.NewDecoder(bytes.NewReader(yamlFile), + yaml.UseJSONUnmarshaler(), + yaml.DisallowUnknownField(), + ) + var config Config + err = dec.Decode(&config) + if err != nil { + return nil, fmt.Errorf("Error parsing YAML: %v", err) + } + err = config.Validate() + if err != nil { + return nil, err + } + + // every application must have its namespace field set + for _, ns := range config.Namespaces { + for _, app := range ns.Applications { + app.Namespace = ns.Name + } + } + + return &config, nil +} diff --git a/cmd/policygen/generator.go b/cmd/policygen/generator.go new file mode 100644 index 0000000..2341572 --- /dev/null +++ b/cmd/policygen/generator.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" +) + +type Generator interface { + GenerateNamespace(writer io.Writer, namespace *Namespace) error + GenerateCommunicationRule(writer io.Writer, app *Application, ingress *Ingress, egress *Egress) error +} + +type Peer struct { + Applications []*Application + Networks []*Network + Predefined []string +} + +func (p *Peer) append(app *Application, network *Network, predefined string) { + if app != nil { + p.Applications = append(p.Applications, app) + } + if network != nil { + p.Networks = append(p.Networks, network) + } + if predefined != "" { + p.Predefined = append(p.Predefined, predefined) + } +} + +func (p *Peer) Empty() bool { + return len(p.Applications)+len(p.Networks)+len(p.Predefined) == 0 +} + +func (p Peer) String() string { + res := "" + for _, app := range p.Applications { + res += "app:" + app.Name + " " + } + for _, net := range p.Networks { + res += "net:" + net.Name + " " + } + for _, pre := range p.Predefined { + res += "pre:" + pre + " " + } + return res +} + +type Ingress struct { + Peer +} +type Egress struct { + Peer +} + +func Generate(writer io.Writer, generator Generator, config *Config) error { + + log.Printf("CONFIG %+v", config) + for _, ns := range config.Namespaces { + err := generator.GenerateNamespace(os.Stdout, ns) + if err != nil { + return err + } + } + + // Loop over all applications and gather the ingress and egress for each application + var applications = make(map[string]*Application) + var ingresses = make(map[string]*Ingress) + var egresses = make(map[string]*Egress) + for _, ns := range config.Namespaces { + for _, app := range ns.Applications { + applications[app.Name] = app + if ingresses[app.Name] == nil { + ingresses[app.Name] = &Ingress{} + } + if egresses[app.Name] == nil { + egresses[app.Name] = &Egress{} + } + } + } + + for _, communication := range config.Communications { + for _, from := range communication.From { + appFrom, networkFrom, predefinedFrom := config.GetApplication(from) + for _, to := range communication.To { + appTo, networkTo, predefinedTo := config.GetApplication(to) + if appFrom != nil { + // we have an egress + egress := egresses[from] + egress.append(appTo, networkTo, predefinedTo) + } + if appTo != nil { + // we have an ingress + ingress := ingresses[to] + ingress.append(appFrom, networkFrom, predefinedFrom) + } + } + } + } + + // loop over all apps and configure them + for app, ingress := range ingresses { + egress := egresses[app] + if !ingress.Empty() || !egress.Empty() { + fmt.Fprintf(os.Stderr, "RULE %s\n", app) + fmt.Fprintf(os.Stderr, " IN %s\n", ingress) + fmt.Fprintf(os.Stderr, " OUT %s\n", egress) + generator.GenerateCommunicationRule(writer, applications[app], ingress, egress) + } + } + + return nil +} diff --git a/cmd/policygen/main.go b/cmd/policygen/main.go index c1e5d8a..8f9f7fc 100644 --- a/cmd/policygen/main.go +++ b/cmd/policygen/main.go @@ -1,10 +1,10 @@ package main import ( - "bytes" "fmt" "github.com/goccy/go-yaml" "github.com/spf13/cobra" + "log" "os" ) @@ -18,32 +18,39 @@ func execute(files []string, options *Options) error { return fmt.Errorf("File expected") } for _, file := range files { - fmt.Fprintf(os.Stderr, "Reading config %s\n", file) - yamlFile, err := os.ReadFile(file) - if err != nil { - return fmt.Errorf("Error reading YAML file: %v", err) - } - - // Parse the YAML content - dec := yaml.NewDecoder(bytes.NewReader(yamlFile), - yaml.UseJSONUnmarshaler(), - yaml.DisallowUnknownField(), - ) - var config Config - err = dec.Decode(&config) - if err != nil { - return fmt.Errorf("Error parsing YAML: %v", err) - } - err = config.Validate() + config, err := LoadConfig(file) if err != nil { return err } fmt.Printf("PARSED %+v\n", config) + + policyTemplates, err := NewPolicyTemplates() + if err != nil { + return err + } + var generator Generator + generator = NetworkPolicyGenerrator{ + config: config, + policyTemplates: policyTemplates, + } + Generate(os.Stdout, generator, config) + } return nil } func main() { + + val := map[string]string{ + "abc": "1", + } + data, err := yaml.Marshal(val) + if err != nil { + panic(err) + } + log.Printf("val %s", string(data)) + //os.Exit(1) + options := Options{ cni: "cilium", policyType: "netpol", diff --git a/cmd/policygen/netpol_generator.go b/cmd/policygen/netpol_generator.go new file mode 100644 index 0000000..2f6077c --- /dev/null +++ b/cmd/policygen/netpol_generator.go @@ -0,0 +1,61 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" +) + +type NetworkPolicyGenerrator struct { + config *Config + policyTemplates *PolicyTemplates +} + +func (g NetworkPolicyGenerrator) GenerateNamespace(writer io.Writer, namespace *Namespace) error { + fmt.Fprintf(os.Stderr, "Namespace %s\n", namespace.Name) + + templates := g.policyTemplates.NamespaceTemplates("netpol", namespace.Capabilities) + log.Printf("Got %d templates", len(templates)) + + for _, template := range templates { + err := template.Execute(writer, &namespace) + if err != nil { + return fmt.Errorf("Error using template %s: %w", template.Name(), err) + } + } + return nil +} + +func (g NetworkPolicyGenerrator) GenerateCommunicationRule( + writer io.Writer, + app *Application, + ingress *Ingress, + egress *Egress) error { + + if len(ingress.Applications)+ + len(ingress.Networks)+ + len(egress.Applications)+ + len(egress.Networks) > 0 { + // non-trivial regular network policy + + tmpl := g.policyTemplates.ApplicationTemplate("netpol") + log.Printf("Found template %v for pod %s", tmpl, app.Name) + if tmpl != nil { + + log.Printf("EXECUTING %s", app.Namespace) + err := tmpl.Execute(writer, map[string]any{ + "app": app, + "ingress": ingress, + "egress": egress, + "labels": map[string]string{ + "policy-generator": "1", + }, + }) + if err != nil { + return err + } + } + } + return nil +} diff --git a/cmd/policygen/templates.go b/cmd/policygen/templates.go index 7f23905..9ce5b61 100644 --- a/cmd/policygen/templates.go +++ b/cmd/policygen/templates.go @@ -2,10 +2,10 @@ package main import ( "fmt" - "html/template" "io/fs" "os" "strings" + "text/template" ) import "embed" import sprig "github.com/Masterminds/sprig/v3" // This provides most Helm functions @@ -66,20 +66,24 @@ func showContents(files fs.FS) { } } -func loadTemplates() (*template.Template, error) { +type PolicyTemplates struct { + templates *template.Template +} + +func NewPolicyTemplates() (*PolicyTemplates, error) { showContents(templateFS) // Parse all templates at once from the embedded FS tmpl := NewTemplate() - err := loadTemplatesGlob(tmpl) + err := loadTemplatesAll(tmpl) if err != nil { return nil, err } - return tmpl, err + return &PolicyTemplates{templates: tmpl}, err } -func loadTemplatesGlob(tmpl *template.Template) error { +func loadTemplatesAll(tmpl *template.Template) error { return fs.WalkDir(templateFS, ".", func(path string, d os.DirEntry, err error) error { if strings.HasSuffix(path, ".yaml") { data, err := fs.ReadFile(templateFS, path) @@ -87,8 +91,36 @@ func loadTemplatesGlob(tmpl *template.Template) error { return err } fmt.Fprintf(os.Stderr, "Loading template %s\n", path) - tmpl.New(path).Parse(string(data)) + _, err = tmpl.New(path).Option("missingkey=error").Parse(string(data)) + if err != nil { + return err + } } return nil }) } + +func (t *PolicyTemplates) NamespaceTemplates(policyType string, capabilities []string) []*template.Template { + res := make([]*template.Template, 0) + tmpl := t.templates.Lookup(fmt.Sprintf("templates/%s/namespace/namespace.yaml", policyType)) + if tmpl != nil { + res = append(res, tmpl) + } + for _, capability := range capabilities { + tmpl := t.templates.Lookup(fmt.Sprintf("templates/%s/namespace/%s.yaml", policyType, capability)) + if tmpl != nil { + res = append(res, tmpl) + } + } + return res +} + +func (t *PolicyTemplates) ApplicationTemplate(policyType string) *template.Template { + tmpl := t.templates.Lookup(fmt.Sprintf("templates/%s/pod/pod.yaml", policyType)) + return tmpl +} + +func (t *PolicyTemplates) PredefineApplicationPolicyTemplate(policyType string, predefined string) *template.Template { + tmpl := t.templates.Lookup(fmt.Sprintf("templates/pod/%s/%s.yaml", policyType, predefined)) + return tmpl +} diff --git a/cmd/policygen/templates/linkerd/dummy.yaml b/cmd/policygen/templates/linkerd/dummy.yaml new file mode 100644 index 0000000..e69de29 diff --git a/cmd/policygen/templates/netpol/apiserver/cilium/egress.yaml b/cmd/policygen/templates/netpol/apiserver/cilium/egress.yaml deleted file mode 100644 index b36c1d1..0000000 --- a/cmd/policygen/templates/netpol/apiserver/cilium/egress.yaml +++ /dev/null @@ -1,15 +0,0 @@ -kind: CiliumNetworkPolicy -apiVersion: cilium.io/v2 -metadata: - name: {{.name}} - namespace: {{.namespace}} -spec: - endpointSelector: - {{ .selector }} - egress: - - toEntities: - - kube-apiserver - - toPorts: - - ports: - - port: "6443" - protocol: TCP diff --git a/cmd/policygen/templates/netpol/egress.yaml b/cmd/policygen/templates/netpol/egress.yaml deleted file mode 100644 index 793dfd9..0000000 --- a/cmd/policygen/templates/netpol/egress.yaml +++ /dev/null @@ -1,14 +0,0 @@ -kind: NetworkPolicy -apiVersion: networking.k8s.io/v1 -metadata: - name: "{{.name}}" - namespace: "{{.namespace}}" -spec: - policyTypes: - - Egress - podSelector: - {{.selector}} - egress: - {{- range $from := .from }} - - {{ $from | nindent 4 }} - {{- end }} \ No newline at end of file diff --git a/cmd/policygen/templates/netpol/ingress.yaml b/cmd/policygen/templates/netpol/ingress.yaml deleted file mode 100644 index 5cd8b77..0000000 --- a/cmd/policygen/templates/netpol/ingress.yaml +++ /dev/null @@ -1,14 +0,0 @@ -kind: NetworkPolicy -apiVersion: networking.k8s.io/v1 -metadata: - name: "{{.name}}" - namespace: "{{.namespace}}" -spec: - policyTypes: - - Ingress - podSelector: - {{.selector}} - ingress: - {{- range $from := .from }} - - {{ $from | nindent 4 }} - {{- end }} \ No newline at end of file diff --git a/cmd/policygen/templates/netpol/namespace/linkerd.yaml b/cmd/policygen/templates/netpol/namespace/linkerd.yaml new file mode 100644 index 0000000..684665c --- /dev/null +++ b/cmd/policygen/templates/netpol/namespace/linkerd.yaml @@ -0,0 +1,6 @@ +--- +#################################################################################### +# LINKERD NETPOL TBD +#################################################################################### + + diff --git a/cmd/policygen/templates/netpol/namespace/namespace.yaml b/cmd/policygen/templates/netpol/namespace/namespace.yaml new file mode 100644 index 0000000..2c1e013 --- /dev/null +++ b/cmd/policygen/templates/netpol/namespace/namespace.yaml @@ -0,0 +1,14 @@ + +{{- if not .Open }} +--- +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: default-allow-nothing + namespace: "{{.Name}}" +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +{{ end }} \ No newline at end of file diff --git a/cmd/policygen/templates/netpol/apiserver/cilium/ingress.yaml b/cmd/policygen/templates/netpol/pod/cilium.yaml similarity index 53% rename from cmd/policygen/templates/netpol/apiserver/cilium/ingress.yaml rename to cmd/policygen/templates/netpol/pod/cilium.yaml index a81376f..a3c5713 100644 --- a/cmd/policygen/templates/netpol/apiserver/cilium/ingress.yaml +++ b/cmd/policygen/templates/netpol/pod/cilium.yaml @@ -3,11 +3,24 @@ apiVersion: cilium.io/v2 metadata: name: {{.name}} namespace: {{.namespace}} + labels: "{{ .labels | toYaml | nindent 4 }}" spec: endpointSelector: {{ .selector }} + {{- if .from }} ingress: - fromEntities: - kube-apiserver # See https://github.com/cilium/cilium/issues/35401 - remote-node + {{- end }} + {{- if .to }} + egress: + - toEntities: + - kube-apiserver + - toPorts: + - ports: + - port: "6443" + protocol: TCP + {{- end }} + diff --git a/cmd/policygen/templates/netpol/pod/pod.yaml b/cmd/policygen/templates/netpol/pod/pod.yaml new file mode 100644 index 0000000..c606036 --- /dev/null +++ b/cmd/policygen/templates/netpol/pod/pod.yaml @@ -0,0 +1,56 @@ +--- +{{- define "peer" }} + - podSelector: + matchLabels: {{ .MatchLabels | toYaml | nindent 12 }} + namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Namespace }} + {{- if .Ports }} + ports: + # TODO: add protocol + {{- range $port := .Ports }} + - port: {{ $port }} + {{- end }} + {{- end }} +{{- end }} +- + {{- define "ports" }} + {{- range $port := . }} + PORT {{ $port }} + {{- end }} + {{- end }} + + +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + name: "{{.app.Name}}" + namespace: "{{.app.Namespace}}" + labels: {{ .labels | toYaml | nindent 4 }} +spec: + podSelector: {{ .app.MatchLabels | toYaml | nindent 4 }} + policyTypes: + {{- if or .ingress.Applications .ingress.Networks }} + - Ingress + {{- end }} + {{- if or .egress.Applications .egress.Networks }} + - Egress + {{- end }} + + {{- if or .ingress.Applications .ingress.Networks }} + ingress: + from: + {{- range $ingress := .ingress.Applications }} + {{- template "peer" $ingress }} + {{- template "ports" $ingress.Ports }} + {{- end }} + {{- range $ingress := .ingress.Networks }} + - ipBlock: + cidr: {{ $ingress.CIDR}} + except: + {{- range $except := $ingress.Except }} + - {{ $except }} + {{- end }} + {{- end }} + {{- end }} + THEEND \ No newline at end of file diff --git a/example/config.yaml b/example/config.yaml index 246e0eb..a5cc0a6 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -12,6 +12,7 @@ networks: namespaces: - name: wamblee-org + open: true capabilities: - linkerd applications: @@ -29,22 +30,24 @@ namespaces: - name: httpd-wamblee-org matchLabels: app: wamblee-org + ports: + - 1000 communications: - from: # can we support both string and list of strings? - httpd-wamblee-org + - internet to: - nexus-server - - wamblee-static - - wamblee-safe - - # or limiting ports further - - from: - - httpd-wamblee-org - to: - - nexus-server - ports: - - 8081 - - 8082 + + +# # or limiting ports further +# - from: +# - httpd-wamblee-org +# to: +# - nexus-server +# ports: +# - 8081 +# - 8082