Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ spec:
- [x] DMI/BIOS discovery engine #2
- [x] Local file / HTTP discovery engine #3
- [x] Hostname / node name discovery engine #4
- [ ] Network config discovery engine #5
- [x] Network config discovery engine #5
- [ ] Taint management #6
- [ ] Prometheus Exporter #7
- [ ] Better RBAC / admission webhook #8
Expand Down Expand Up @@ -109,6 +109,11 @@ The `labelsTemplates` section defines which Kubernetes labels Topomatik will man

The [Sprig library](http://masterminds.github.io/sprig/) is available for advanced template operations, giving you access to string manipulation, regular expressions, and more.

> [!NOTE]
> Labels must be an empty string or consist of alphanumeric characters, "-", "\_" or ".", and must start and end with an alphanumeric character. Invalid characters will be replaced by "\_" and trailing non alpha-numeric characters will be removed.
>
> Example: "@@@foo+bar.foobar----." will be rendered as "foo_bar.foobar".

### Auto-Discovery Engine Configuration

Each auto-discovery engine has its own configuration section.
Expand Down Expand Up @@ -215,6 +220,24 @@ hostname:

Template variables are defined by the name of an file entry in the configuration. For instance, with the above configuration, this engine exposes `.hostname.zone`, `.hostname.region` and `.hostname.node`, but since the `pattern` in the configuration is optional, it also exposes the whole hostname as `.hostname.value`.

#### Network

Reads network information of the device in order to group machines by subnet.

##### Network configuration

| Name | Description | Default value |
| --------- | --------------------------------------------------------------------------------------- | ------------- |
| enabled | Enable or disable this auto discovery engine | false |
| interval | Polling interval of the hardware information | `<required>` |
| interface | Interface name to get the subnet from or auto to use any interface with default gateway | auto |

##### Available template variables from the network engine

| Name | Description |
| ---------------| ------------------------------------------------ |
| network.subnet | The subnet associated with the defined interface |

## 🤝 Contributing

Found a bug? Want to add support for another discovery engine? We welcome contributions! Just be sure your code is as clean as your commit messages.
Expand Down
4 changes: 4 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/enix/topomatik/internal/autodiscovery/hardware"
"github.com/enix/topomatik/internal/autodiscovery/hostname"
"github.com/enix/topomatik/internal/autodiscovery/lldp"
"github.com/enix/topomatik/internal/autodiscovery/network"
"github.com/enix/topomatik/internal/config"
"github.com/enix/topomatik/internal/controller"
"github.com/enix/topomatik/internal/schedulers"
Expand Down Expand Up @@ -74,5 +75,8 @@ func main() {
ctrl.Register("hostname", &hostname.HostnameDiscoveryEngine{Config: config.Hostname.Config})
}

if config.Network.Enabled {
ctrl.Register("network", &network.NetworkDiscoveryEngine{Config: config.Network.Config})
}
panic(ctrl.Start())
}
18 changes: 6 additions & 12 deletions internal/autodiscovery/lldp/lldp.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package lldp

import (
"errors"
"fmt"

"github.com/enix/topomatik/internal/autodiscovery/network"
"github.com/google/gopacket"
"github.com/google/gopacket/afpacket"
"github.com/google/gopacket/layers"
Expand Down Expand Up @@ -85,21 +85,15 @@ func (l *LLDPDiscoveryEngine) Watch(callback func(data map[string]string, err er
}

func getDefaultRouteInterfaceName() (string, error) {
routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
defaultRoute, err := network.GetDefaultRoute()
if err != nil {
return "", err
}

for _, route := range routes {
ones, _ := route.Dst.Mask.Size()
if route.Dst == nil || (route.Dst.IP.IsUnspecified() && ones == 0) {
link, err := netlink.LinkByIndex(route.LinkIndex)
if err != nil {
return "", err
}
return link.Attrs().Name, nil
}
link, err := netlink.LinkByIndex(defaultRoute.LinkIndex)
if err != nil {
return "", err
}

return "", errors.New("default route not found")
return link.Attrs().Name, nil
}
94 changes: 94 additions & 0 deletions internal/autodiscovery/network/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package network

import (
"errors"
"fmt"
"time"

"github.com/vishvananda/netlink"
)

type Config struct {
Interval time.Duration `yaml:"interval" validate:"required"`
Interface string `yaml:"interface"`
}

type NetworkDiscoveryEngine struct {
Config
}

func (n *NetworkDiscoveryEngine) Setup() (err error) {
return
}

func (n *NetworkDiscoveryEngine) Watch(callback func(data map[string]string, err error)) {
var route *netlink.Route
var err error

ticker := time.NewTicker(n.Interval)

for {
if n.Interface == "" {
route, err = GetDefaultRoute()
} else {
route, err = getRouteByInterfaceName(n.Interface)
}

if err != nil {
callback(nil, err)
return
}

if route.Family != netlink.FAMILY_V4 {
callback(nil, fmt.Errorf("unsupported IP family (code: %d), must be IPV4", route.Family))
return
}

ones, _ := route.Src.DefaultMask().Size()
callback(map[string]string{
"subnet": fmt.Sprintf("%s/%d", route.Src.Mask(route.Src.DefaultMask()), ones),
}, nil)

<-ticker.C
}
}

func GetDefaultRoute() (*netlink.Route, error) {
routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
if err != nil {
return nil, err
}

for _, route := range routes {
if route.Dst == nil {
return &route, nil
}

ones, _ := route.Dst.Mask.Size()
if route.Dst.IP.IsUnspecified() && ones == 0 {
return &route, nil
}
}

return nil, errors.New("default route not found")
}

func getRouteByInterfaceName(interfaceName string) (*netlink.Route, error) {
routes, err := netlink.RouteList(nil, netlink.FAMILY_ALL)
if err != nil {
return nil, err
}

for _, route := range routes {
link, err := netlink.LinkByIndex(route.LinkIndex)
if err != nil {
return nil, err
}

if link.Attrs().Name == interfaceName {
return &route, nil
}
}

return nil, fmt.Errorf("route with interface name \"%s\" not found", interfaceName)
}
5 changes: 5 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/enix/topomatik/internal/autodiscovery/hardware"
"github.com/enix/topomatik/internal/autodiscovery/hostname"
"github.com/enix/topomatik/internal/autodiscovery/lldp"
"github.com/enix/topomatik/internal/autodiscovery/network"
"github.com/go-playground/validator/v10"
"gopkg.in/yaml.v2"
)
Expand All @@ -23,6 +24,7 @@ type Config struct {
Files files.Config `yaml:"files" validate:"dive"`
Hardware EngineConfig[hardware.Config] `yaml:"hardware"`
Hostname EngineConfig[hostname.Config] `yaml:"hostname"`
Network EngineConfig[network.Config] `yaml:"network"`
}

type EngineConfig[T any] struct {
Expand Down Expand Up @@ -79,6 +81,9 @@ func Load(path string) (*Config, error) {
if !config.Hostname.Enabled {
ignoredEngines = append(ignoredEngines, "Hostname")
}
if !config.Network.Enabled {
ignoredEngines = append(ignoredEngines, "Network")
}

if err := validate.StructExcept(config, ignoredEngines...); err != nil {
if errs, ok := err.(validator.ValidationErrors); ok {
Expand Down
14 changes: 11 additions & 3 deletions internal/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
"text/template"

"github.com/Masterminds/sprig"
Expand All @@ -17,6 +19,8 @@ import (
"k8s.io/client-go/kubernetes"
)

var labelRegexp = regexp.MustCompile(`[^A-Za-z0-9._-]`)

type ReconciliationScheduler interface {
Trigger()
C() <-chan struct{}
Expand Down Expand Up @@ -121,9 +125,13 @@ func (c *Controller) reconcileNode() error {
if err != nil {
fmt.Printf("could not render template for %s: %s\n", label, err.Error())
} else {
if node.Labels[label] != value.String() {
labels[label] = value.String()
fmt.Printf("%s: %s\n", label, value)
sanitizedValue := labelRegexp.ReplaceAllString(value.String(), "_")
sanitizedValue = strings.TrimFunc(sanitizedValue, func(r rune) bool {
return strings.Contains("_.-", string(r))
})
if node.Labels[label] != sanitizedValue {
labels[label] = sanitizedValue
fmt.Printf("%s: %s\n", label, sanitizedValue)
}
}
}
Expand Down