isle/go/daemon/daecommon/config.go

283 lines
7.1 KiB
Go

package daecommon
import (
"bytes"
"fmt"
"io"
"isle/bootstrap"
"isle/toolkit"
"isle/yamlutil"
"net"
"strconv"
_ "embed"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"gopkg.in/yaml.v3"
)
const (
// Network ID used when translating from the old single-network daemon
// config to the multi-network config.
DeprecatedNetworkID = "_" // DEPRECATED
)
//go:embed daemon.yml
var defaultConfigB []byte
type ConfigTun struct {
Device string `yaml:"device"`
}
type ConfigFirewall struct {
Outbound []ConfigFirewallRule `yaml:"outbound"`
Inbound []ConfigFirewallRule `yaml:"inbound"`
}
type ConfigFirewallRule struct {
Port string `yaml:"port,omitempty"`
Code string `yaml:"code,omitempty"`
Proto string `yaml:"proto,omitempty"`
Host string `yaml:"host,omitempty"`
Group string `yaml:"group,omitempty"`
Groups []string `yaml:"groups,omitempty"`
CIDR string `yaml:"cidr,omitempty"`
CASha string `yaml:"ca_sha,omitempty"`
CAName string `yaml:"ca_name,omitempty"`
}
// ConfigStorageAllocation describes the structure of each storage allocation
// within the daemon config file.
type ConfigStorageAllocation struct {
DataPath string `yaml:"data_path"`
MetaPath string `yaml:"meta_path"`
Capacity int `yaml:"capacity"`
S3APIPort int `yaml:"s3_api_port"`
RPCPort int `yaml:"rpc_port"`
AdminPort int `yaml:"admin_port"`
// Zone is a secret option which makes it easier to test garage bugs, but
// which we don't want users to otherwise know about.
Zone string `yaml:"zone"`
}
// Annotate implements the mctx.Annotator interface.
func (csa ConfigStorageAllocation) Annotate(aa mctx.Annotations) {
aa["allocDataPath"] = csa.DataPath
aa["allocMetaPath"] = csa.MetaPath
aa["allocCapacity"] = csa.Capacity
aa["allocS3APIPort"] = csa.S3APIPort
aa["allocRPCPort"] = csa.RPCPort
aa["allocAdminPort"] = csa.AdminPort
}
// NetworkConfig describes the configuration of a single network.
type NetworkConfig struct {
DNS struct {
Resolvers []string `yaml:"resolvers"`
} `yaml:"dns"`
VPN struct {
PublicAddr string `yaml:"public_addr"`
Firewall ConfigFirewall `yaml:"firewall"`
Tun ConfigTun `yaml:"tun"`
} `yaml:"vpn"`
Storage struct {
Allocations []ConfigStorageAllocation `yaml:"allocations"`
} `yaml:"storage"`
}
// NewNetworkConfig returns a new NetworkConfig populated with its default
// values. If a callback is given the NetworkConfig will be passed to it for
// modification prior to having defaults populated.
func NewNetworkConfig(fn func(*NetworkConfig)) NetworkConfig {
var c NetworkConfig
if fn != nil {
fn(&c)
}
c.fillDefaults()
return c
}
func (c *NetworkConfig) fillDefaults() {
if c.DNS.Resolvers == nil {
c.DNS.Resolvers = []string{
"1.1.1.1",
"8.8.8.8",
}
}
if c.VPN.Firewall.Outbound == nil {
c.VPN.Firewall.Outbound = []ConfigFirewallRule{
{
Port: "any",
Proto: "any",
Host: "any",
},
}
}
if c.VPN.Firewall.Inbound == nil {
c.VPN.Firewall.Inbound = []ConfigFirewallRule{
{
Port: "any",
Proto: "icmp",
Host: "any",
},
}
}
if c.VPN.Tun.Device == "" {
// TODO if there are multiple Networks then each one needs a unique
// device name.
c.VPN.Tun.Device = "isle-tun"
}
var firewallGarageInbound []ConfigFirewallRule
for i := range c.Storage.Allocations {
if c.Storage.Allocations[i].RPCPort == 0 {
c.Storage.Allocations[i].RPCPort = 3900 + (i * 10)
}
if c.Storage.Allocations[i].S3APIPort == 0 {
c.Storage.Allocations[i].S3APIPort = 3901 + (i * 10)
}
if c.Storage.Allocations[i].AdminPort == 0 {
c.Storage.Allocations[i].AdminPort = 3902 + (i * 10)
}
alloc := c.Storage.Allocations[i]
firewallGarageInbound = append(
firewallGarageInbound,
ConfigFirewallRule{
Port: strconv.Itoa(alloc.S3APIPort),
Proto: "tcp",
Host: "any",
},
ConfigFirewallRule{
Port: strconv.Itoa(alloc.RPCPort),
Proto: "tcp",
Host: "any",
},
)
}
c.VPN.Firewall.Inbound = append(
c.VPN.Firewall.Inbound,
firewallGarageInbound...,
)
}
// UnmarshalYAML implements the yaml.Unmarshaler interface. It will attempt to
// fill in default values where it can.
func (c *NetworkConfig) UnmarshalYAML(n *yaml.Node) error {
type wrap NetworkConfig
if err := n.Decode((*wrap)(c)); err != nil {
return fmt.Errorf("decoding into %T: %w", c, err)
}
c.fillDefaults()
return nil
}
// Config describes the structure of the daemon config file.
type Config struct {
Networks map[string]NetworkConfig `yaml:"networks"`
}
// Validate asserts that the Config has no internal inconsistencies which would
// render it unusable.
func (c Config) Validate() error {
nebulaPorts := map[string]string{}
for id, network := range c.Networks {
if network.VPN.PublicAddr == "" {
continue
}
_, port, err := net.SplitHostPort(network.VPN.PublicAddr)
if err != nil {
return fmt.Errorf(
"invalid vpn.public_addr %q: %w", network.VPN.PublicAddr, err,
)
} else if otherID, ok := nebulaPorts[port]; ok {
return fmt.Errorf(
"two networks with the same vpn.public_addr: %q and %q",
id,
otherID,
)
}
nebulaPorts[port] = id
}
return nil
}
// CopyDefaultConfig copies the daemon config file embedded in the AppDir into
// the given io.Writer.
func CopyDefaultConfig(into io.Writer) error {
_, err := io.Copy(into, bytes.NewReader(defaultConfigB))
return err
}
// UnmarshalYAML implements the yaml.Unmarshaler interface. It will attempt to
// fill in default values where it can.
func (c *Config) UnmarshalYAML(n *yaml.Node) error {
{ // DEPRECATED
// We decode into a wrapped NetworkConfig, so that its UnmarshalYAML
// doesn't get invoked. This prevents fillDefaults from getting
// automatically called, so the IsZero check will work as intended. This
// means we need to call fillDefaults manually though.
type wrap NetworkConfig
var networkConfig NetworkConfig
_ = n.Decode((*wrap)(&networkConfig))
if !toolkit.IsZero(networkConfig) {
networkConfig.fillDefaults()
*c = Config{
Networks: map[string]NetworkConfig{
DeprecatedNetworkID: networkConfig,
},
}
return c.Validate()
}
}
type wrap Config
if err := n.Decode((*wrap)(c)); err != nil {
return fmt.Errorf("yaml unmarshaling back into Config struct: %w", err)
}
return c.Validate()
}
// LoadConfig loads the daemon config from userConfigPath.
//
// If userConfigPath is not given then the default is loaded and returned.
func LoadConfig(userConfigPath string) (Config, error) {
if userConfigPath == "" {
return Config{}, nil
}
var config Config
err := yamlutil.LoadYamlFile(&config, userConfigPath)
return config, err
}
// BootstrapGarageHostForAlloc returns the bootstrap.GarageHostInstance which
// corresponds with the given alloc from the daemon config. This will panic if
// no associated instance can be found.
func BootstrapGarageHostForAlloc(
host bootstrap.Host, alloc ConfigStorageAllocation,
) bootstrap.GarageHostInstance {
for _, inst := range host.Garage.Instances {
if inst.RPCPort == alloc.RPCPort {
return inst
}
}
panic(fmt.Sprintf("could not find alloc %+v in the bootstrap data", alloc))
}