blocky/service/service.go

114 lines
3.4 KiB
Go

// Package service exposes types to abstract services from the networking.
//
// The idea is that we build a set of services and a set of network endpoints (Listener).
// The services are then assigned to endpoints based on the address(es) they were configured for.
//
// Actual service to endpoint binding is not handled by the abstractions in this package as it is
// protocol specific.
// The general pattern is to make a "server" that wraps a service, and can then be started on an
// endpoint using a `Serve` method, similar to `http.Server`.
//
// To support exposing multiple compatible services on a single endpoint (example: DoH + metrics on a single port),
// services can implement `Merger`.
package service
import (
"fmt"
"slices"
"strings"
"github.com/0xERR0R/blocky/util"
)
// Service is a network exposed service.
//
// It contains only the logic and user configured addresses it should be exposed on.
// Is is meant to be associated to one or more sockets via those addresses.
// Actual association with a socket is protocol specific.
type Service interface {
fmt.Stringer
// ServiceName returns the user friendly name of the service.
ServiceName() string
// ExposeOn returns the set of endpoints the service should be exposed on.
//
// They can be used to find listener(s) with matching configuration.
ExposeOn() []Endpoint
}
func svcString(s Service) string {
endpoints := util.ForEach(s.ExposeOn(), func(e Endpoint) string { return e.String() })
return fmt.Sprintf("%s on %s", s.ServiceName(), strings.Join(endpoints, ", "))
}
// Info can be embedded in structs to help implement Service.
type Info struct {
name string
endpoints []Endpoint
}
func NewInfo(name string, endpoints []Endpoint) Info {
return Info{
name: name,
endpoints: endpoints,
}
}
func (i *Info) ServiceName() string { return i.name }
func (i *Info) ExposeOn() []Endpoint { return i.endpoints }
func (i *Info) String() string { return svcString(i) }
// GroupByListener returns a map of listener and services grouped by configured address.
//
// Each input listener is a key in the map. The corresponding value is a service
// merged from all services with a matching address.
func GroupByListener(services []Service, listeners []Listener) (map[Listener]Service, error) {
res := make(map[Listener]Service, len(listeners))
unused := slices.Clone(services)
for _, listener := range listeners {
services := findAllCompatible(services, listener.Exposes())
if len(services) == 0 {
return nil, fmt.Errorf("found no compatible services for listener %s", listener)
}
svc, err := MergeAll(services)
if err != nil {
return nil, fmt.Errorf("cannot merge services configured for listener %s: %w", listener, err)
}
res[listener] = svc
for _, svc := range services {
if i := slices.Index(unused, svc); i != -1 {
unused = slices.Delete(unused, i, i+1)
}
}
}
if len(unused) != 0 {
return nil, fmt.Errorf("found no compatible listener for services: %v", unused)
}
return res, nil
}
// findAllCompatible returns the subset of services that use the given Listener.
func findAllCompatible(services []Service, endpoint Endpoint) []Service {
res := make([]Service, 0, len(services))
for _, svc := range services {
if isExposedOn(svc, endpoint) {
res = append(res, svc)
}
}
return res
}
func isExposedOn(svc Service, endpoint Endpoint) bool {
return slices.Index(svc.ExposeOn(), endpoint) != -1
}