diff --git a/Makefile b/Makefile index f5cda7ad..1e6e284f 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,8 @@ deps: clean-deps github.com/hashicorp/consul/api \ github.com/spf13/cobra \ github.com/laher/goxc \ - github.com/gin-contrib/cors + github.com/gin-contrib/cors \ + github.com/lxc/lxd clean-dist: rm -rf ./dist/${VERSION} @@ -84,10 +85,10 @@ dist: rm ./debian -rf @echo Done. -build-container-latest: build +build-container-latest: build @echo Building docker container LATEST docker build -t yyyar/gobetween . -build-container-tagged: build +build-container-tagged: build @echo Building docker container ${VERSION} docker build -t yyyar/gobetween:${VERSION} . diff --git a/config/gobetween.toml b/config/gobetween.toml index 686d6594..65be5d0d 100644 --- a/config/gobetween.toml +++ b/config/gobetween.toml @@ -236,3 +236,8 @@ protocol = "udp" # consul_tls_key_path = "/path/to/key.pem" # consul_tls_cacert_path = "/path/to/cacert.pem" # +# # -- lxd -- # +# kind = "lxd" +# lxd_container_label = "myservice" # (required) Label to filter containers +# lxd_container_private_port = "80" # (required) Private port of container to use +# lxd_container_interface = "eth1" # (optional) Container interface to communicate on. by default "eth0" diff --git a/src/config/config.go b/src/config/config.go index a2cccc45..19b09e2e 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -32,7 +32,7 @@ type ApiConfig struct { Bind string `toml:"bind" json:"bind"` BasicAuth *ApiBasicAuthConfig `toml:"basic_auth" json:"basic_auth"` Tls *ApiTlsConfig `toml:"tls" json:"tls"` - Cors bool `toml:"cors" json:"cors"` + Cors bool `toml:"cors" json:"cors"` } /** @@ -170,6 +170,7 @@ type DiscoveryConfig struct { *ExecDiscoveryConfig *PlaintextDiscoveryConfig *ConsulDiscoveryConfig + *LXDDiscoveryConfig } type StaticDiscoveryConfig struct { @@ -228,6 +229,12 @@ type ConsulDiscoveryConfig struct { ConsulTlsCacertPath string `toml:"consul_tls_cacert_path" json:"consul_tls_cacert_path"` } +type LXDDiscoveryConfig struct { + LXDContainerLabel string `toml:"lxd_container_label" json:"lxd_container_label"` + LXDContainerPrivatePort string `toml:"lxd_container_private_port" json:"lxd_container_private_port"` + LXDContainerInterface string `toml:"lxd_container_interface" json:"lxd_container_interface"` +} + /** * Healthcheck configuration */ diff --git a/src/discovery/discovery.go b/src/discovery/discovery.go index 590e47da..035ce581 100644 --- a/src/discovery/discovery.go +++ b/src/discovery/discovery.go @@ -29,6 +29,7 @@ func init() { registry["exec"] = NewExecDiscovery registry["plaintext"] = NewPlaintextDiscovery registry["consul"] = NewConsulDiscovery + registry["lxd"] = NewLXDDiscovery } /** diff --git a/src/discovery/lxd.go b/src/discovery/lxd.go new file mode 100644 index 00000000..ffc170ea --- /dev/null +++ b/src/discovery/lxd.go @@ -0,0 +1,155 @@ +/** + * lxd.go - LXD API discovery implementation + * + */ + +package discovery + +import ( + "../config" + "../core" + "../logging" + "../utils" + "fmt" + "time" + + "github.com/lxc/lxd" +) + +const ( + lxdRetryWaitDuration = 2 * time.Second + lxdTimeout = 5 * time.Second +) + +/** + * Create new Discovery with LXD fetch func + */ +func NewLXDDiscovery(cfg config.DiscoveryConfig) interface{} { + + d := Discovery{ + opts: DiscoveryOpts{lxdRetryWaitDuration}, + fetch: lxdFetch, + cfg: cfg, + } + + return &d +} + +/** + * Fetch backends from LXD API + */ +func lxdFetch(cfg config.DiscoveryConfig) (*[]core.Backend, error) { + + log := logging.For("lxdFetch") + + log.Info("Fetching unix.socket") + + // Create a simple LXD client that connects to the LXD server over the local + // unix socket. + config := lxd.Config{ + Remotes: map[string]lxd.RemoteConfig{ + "local": lxd.RemoteConfig{ + Addr: "unix:/var/lib/lxd/unix.socket", + }, + }, + } + client, err := lxd.NewClient(&config, "local") + if err != nil { + return nil, err + } + + // Validate the client config and connectivity + if _, err := client.GetServerConfig(); err != nil { + return nil, err + } + + client.Http.Timeout = utils.ParseDurationOrDefault(cfg.Timeout, lxdTimeout) + + /* Fetch containers */ + containers, err := client.ListContainers() + if err != nil { + return nil, err + } + + /* Create backends from response */ + + backends := []core.Backend{} + + for _, container := range containers { + config := container.Config + if v, ok := config["user.gobetween.label"]; ok { + // label is an arbitrary label to describe the service. + label := v + + // Don't add the backend if the label wasn't requested + // for this service. + if cfg.LXDContainerLabel != "" && label != cfg.LXDContainerLabel { + continue + } + + // private_port is the port on the LXD container. + private_port, ok := config["user.gobetween.private_port"] + if !ok { + msg := fmt.Sprintf("No user.gobetween.private_port set for container %s. Skipping", + container.Name) + log.Warn(msg) + continue + } + + // iface is the container interface to get an IP address. + iface := "eth0" + if v, ok := config["user.gobetween.interface"]; ok { + iface = v + } + + ip, err := lxdDetermineContainerIP(client, container.Name, iface) + if err != nil { + return nil, err + } + + var sni string + if v, ok := config["user.gobetween.sni"]; ok { + sni = v + } + + backends = append(backends, core.Backend{ + Target: core.Target{ + Host: ip, + Port: private_port, + }, + Priority: 1, + Weight: 1, + Stats: core.BackendStats{ + Live: true, + }, + Sni: sni, + }) + } + } + + return &backends, nil +} + +func lxdDetermineContainerIP(client *lxd.Client, container string, iface string) (string, error) { + var containerIP string + cstate, err := client.ContainerState(container) + if err != nil { + return "", err + } + + for i, network := range cstate.Network { + if i == iface { + for _, ip := range network.Addresses { + if ip.Family == "inet" { + containerIP = ip.Address + } + } + } + } + + if containerIP == "" { + return "", fmt.Errorf("Unable to determine IP address for LXD container %s", container) + } + + return containerIP, nil +}