202 lines
5.3 KiB
Go
202 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/goccy/go-yaml"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"slices"
|
|
)
|
|
|
|
var PREDEFINED_APPS = []string{"apiserver"}
|
|
|
|
func validateCIDR(cidr string) error {
|
|
_, _, err := net.ParseCIDR(cidr)
|
|
return err
|
|
}
|
|
|
|
type CIDR string
|
|
|
|
// UnmarshalYAML implements the yaml.Unmarshaler interface
|
|
func (c *CIDR) UnmarshalYAML(value []byte) error {
|
|
// Get the string value from the node
|
|
var s string
|
|
if err := yaml.Unmarshal(value, &s); err != nil {
|
|
return err
|
|
}
|
|
if err := validateCIDR(s); err != nil {
|
|
return err
|
|
}
|
|
*c = CIDR(s)
|
|
return nil
|
|
}
|
|
|
|
// MarshalYAML implements the yaml.Marshaler interface
|
|
func (c CIDR) MarshalYAML() ([]byte, error) {
|
|
// Do any custom processing here before marshalling
|
|
return []byte(string(c)), nil
|
|
}
|
|
|
|
type Port struct {
|
|
Port string `yaml:"port"`
|
|
Protocol string `yaml:"protocol,omitempty" validate:"omitempty,oneof=TCP UDP"`
|
|
}
|
|
|
|
// Network represents each network entry in the YAML
|
|
type Network struct {
|
|
Name string `yaml:"name" validate:"required"`
|
|
CIDR CIDR `yaml:"cidr"`
|
|
Except []CIDR `yaml:"except,omitempty" validate:"dive,required"`
|
|
Ports []Port `yaml:"ports,omitempty" validate:"dive,required"`
|
|
}
|
|
|
|
type Application struct {
|
|
Name string `yaml:"name"`
|
|
Ports []Port `yaml:"ports,omitempty"`
|
|
MatchLabels map[string]string `yaml:"matchLabels"`
|
|
Namespace *Namespace `yaml:"-" validate:"-"`
|
|
}
|
|
|
|
type Namespace struct {
|
|
Name string `yaml:"name"`
|
|
Open bool `yaml:"open"`
|
|
Capabilities []string `yaml:"capabilities"`
|
|
Applications []*Application `yaml:"applications" validate:"dive,required"`
|
|
}
|
|
|
|
type Communication struct {
|
|
From []string `yaml:"from" validate:"dive,required"`
|
|
To []string `yaml:"to" validate:"dive,required"`
|
|
Ports []Port `yaml:"ports" validate:"dive,required"`
|
|
}
|
|
|
|
// Config represents the top-level YAML structure
|
|
type Config struct {
|
|
Networks []*Network `yaml:"networks,omitempty" validate:"dive,required"`
|
|
Namespaces []*Namespace `yaml:"namespaces,omitempty" validate:"dive,required"`
|
|
Communications []*Communication `yaml:"communications,omitempty" validate:"dive,required"`
|
|
}
|
|
|
|
func (c *Config) Update(config *Config) {
|
|
c.Namespaces = append(c.Namespaces, config.Namespaces...)
|
|
c.Networks = append(c.Networks, config.Networks...)
|
|
c.Communications = append(c.Communications, config.Communications...)
|
|
}
|
|
|
|
func (c Config) ValidateSchema() error {
|
|
validate := validator.New(validator.WithRequiredStructEnabled())
|
|
return validate.Struct(c)
|
|
}
|
|
|
|
func (c Config) Validate() error {
|
|
|
|
errs := make([]error, 0)
|
|
|
|
// namesapaces must be unique
|
|
ns := make(map[string]bool)
|
|
for _, namespace := range c.Namespaces {
|
|
if ns[namespace.Name] {
|
|
errs = append(errs, fmt.Errorf("Duplicate namespace name %s", namespace.Name))
|
|
}
|
|
ns[namespace.Name] = true
|
|
}
|
|
|
|
// network names mus tbe unique
|
|
networks := make(map[string]bool)
|
|
for nn, network := range c.Networks {
|
|
log.Printf("Network %+v %v %v\n", network, nn, c.Networks)
|
|
if networks[network.Name] {
|
|
errs = append(errs, fmt.Errorf("Duplicate network name %s", network.Name))
|
|
}
|
|
networks[network.Name] = true
|
|
}
|
|
|
|
// application names must be unique and may not conflict with predefined applications
|
|
apps := make(map[string]bool)
|
|
for _, predefined := range PREDEFINED_APPS {
|
|
apps[predefined] = 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] {
|
|
errs = append(errs, fmt.Errorf("Duplicate application %s (%s)", app.Name, namespace.Name))
|
|
}
|
|
apps[app.Name] = true
|
|
}
|
|
}
|
|
|
|
// applications mentioned in a communication must exist
|
|
for _, communication := range c.Communications {
|
|
for _, from := range communication.From {
|
|
if !apps[from] {
|
|
errs = append(errs, fmt.Errorf("Application does not exist: %s referenced in a communication (%+v)", from, communication))
|
|
}
|
|
}
|
|
for _, to := range communication.To {
|
|
if !apps[to] {
|
|
errs = append(errs, fmt.Errorf("Application does not exist: %s referenced in a communication (%+v)", to, communication))
|
|
}
|
|
}
|
|
}
|
|
|
|
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(),
|
|
yaml.UseOrderedMap(),
|
|
yaml.Strict(),
|
|
)
|
|
var config Config
|
|
err = dec.Decode(&config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error parsing YAML: %v", err)
|
|
}
|
|
|
|
// every application must have its namespace field set
|
|
for _, ns := range config.Namespaces {
|
|
for _, app := range ns.Applications {
|
|
app.Namespace = ns
|
|
}
|
|
}
|
|
|
|
return &config, nil
|
|
}
|