chore: use yaml-to-json library, giving better support for configuring third-party libs like libdns providers

This commit is contained in:
Johan Siebens
2025-03-10 08:40:17 +01:00
parent 978b0ecf4f
commit 828e0c920b
7 changed files with 124 additions and 120 deletions
+2 -3
View File
@@ -7,7 +7,6 @@ require (
github.com/a-h/templ v0.2.663
github.com/apparentlymart/go-cidr v1.1.0
github.com/bufbuild/connect-go v1.10.0
github.com/caarlos0/env/v6 v6.10.1
github.com/caddyserver/certmagic v0.20.0
github.com/coreos/go-oidc/v3 v3.10.0
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
@@ -18,7 +17,6 @@ require (
github.com/hashicorp/go-bexpr v0.1.14
github.com/hashicorp/go-getter v1.7.4
github.com/hashicorp/go-multierror v1.1.1
github.com/imdario/mergo v0.3.16
github.com/jsiebens/go-edit v0.1.0
github.com/jsiebens/mockoidc v0.1.0-rc2
github.com/klauspost/compress v1.17.11
@@ -36,6 +34,7 @@ require (
github.com/nleeper/goment v1.4.4
github.com/ory/dockertest/v3 v3.10.0
github.com/prometheus/client_golang v1.19.1
github.com/puzpuzpuz/xsync/v3 v3.5.1
github.com/rodaine/table v1.2.0
github.com/sony/sonyflake v1.2.0
github.com/spf13/cobra v1.8.1
@@ -55,6 +54,7 @@ require (
gorm.io/gorm v1.25.9
gorm.io/plugin/prometheus v0.1.0
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a
sigs.k8s.io/yaml v1.4.0
tailscale.com v1.80.0
)
@@ -191,7 +191,6 @@ require (
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/safchain/ethtool v0.3.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
+2 -4
View File
@@ -295,8 +295,6 @@ github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJR
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
github.com/caarlos0/env/v6 v6.10.1 h1:t1mPSxNpei6M5yAeu1qtRdPAK29Nbcf/n3G7x+b3/II=
github.com/caarlos0/env/v6 v6.10.1/go.mod h1:hvp/ryKXKipEkcuYjs9mI4bBCg+UI0Yhgm5Zu0ddvwc=
github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE=
github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
@@ -590,8 +588,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
@@ -1619,6 +1615,8 @@ modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
tailscale.com v1.80.0 h1:7joWtDtdHEHJvGmOag10RNITKp1I4Ts7Hrn6pU33/1I=
+89 -57
View File
@@ -2,17 +2,18 @@ package config
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/caddyserver/certmagic"
"github.com/jsiebens/ionscale/internal/domain"
"github.com/jsiebens/ionscale/internal/key"
"github.com/jsiebens/ionscale/internal/util"
"github.com/mitchellh/go-homedir"
"gopkg.in/yaml.v3"
"net/url"
"os"
"path/filepath"
"regexp"
"sigs.k8s.io/yaml"
"strings"
"tailscale.com/tailcfg"
tkey "tailscale.com/types/key"
@@ -88,7 +89,7 @@ func LoadConfig(path string) (*Config, error) {
}
}
keepAliveInterval = cfg.PollNet.KeepAliveInterval
keepAliveInterval = time.Duration(cfg.PollNet.KeepAliveInterval)
magicDNSSuffix = cfg.DNS.MagicDNSSuffix
if cfg.DNS.Provider.Zone != "" {
@@ -116,7 +117,7 @@ func defaultConfig() *Config {
AcmeCA: certmagic.LetsEncryptProductionCA,
},
PollNet: PollNet{
KeepAliveInterval: defaultKeepAliveInterval,
KeepAliveInterval: Duration(defaultKeepAliveInterval),
},
DNS: DNS{
MagicDNSSuffix: defaultMagicDNSSuffix,
@@ -142,21 +143,21 @@ type ServerKeys struct {
}
type Config struct {
ListenAddr string `yaml:"listen_addr,omitempty"`
StunListenAddr string `yaml:"stun_listen_addr,omitempty"`
MetricsListenAddr string `yaml:"metrics_listen_addr,omitempty"`
PublicAddr string `yaml:"public_addr,omitempty"`
StunPublicAddr string `yaml:"stun_public_addr,omitempty"`
Tls Tls `yaml:"tls,omitempty"`
PollNet PollNet `yaml:"poll_net,omitempty"`
Keys Keys `yaml:"keys,omitempty"`
Database Database `yaml:"database,omitempty"`
Auth Auth `yaml:"auth,omitempty"`
DNS DNS `yaml:"dns,omitempty"`
DERP DERP `yaml:"derp,omitempty"`
Logging Logging `yaml:"logging,omitempty"`
ListenAddr string `json:"listen_addr,omitempty"`
StunListenAddr string `json:"stun_listen_addr,omitempty"`
MetricsListenAddr string `json:"metrics_listen_addr,omitempty"`
PublicAddr string `json:"public_addr,omitempty"`
StunPublicAddr string `json:"stun_public_addr,omitempty"`
Tls Tls `json:"tls,omitempty"`
PollNet PollNet `json:"poll_net,omitempty"`
Keys Keys `json:"keys,omitempty"`
Database Database `json:"database,omitempty"`
Auth Auth `json:"auth,omitempty"`
DNS DNS `json:"dns,omitempty"`
DERP DERP `json:"derp,omitempty"`
Logging Logging `json:"logging,omitempty"`
PublicUrl *url.URL `yaml:"-"`
PublicUrl *url.URL `json:"-"`
stunHost string
stunPort int
@@ -165,79 +166,79 @@ type Config struct {
}
type Tls struct {
Disable bool `yaml:"disable"`
ForceHttps bool `yaml:"force_https"`
CertFile string `yaml:"cert_file,omitempty"`
KeyFile string `yaml:"key_file,omitempty"`
AcmeEnabled bool `yaml:"acme,omitempty"`
AcmeEmail string `yaml:"acme_email,omitempty"`
AcmeCA string `yaml:"acme_ca,omitempty"`
Disable bool `json:"disable"`
ForceHttps bool `json:"force_https"`
CertFile string `json:"cert_file,omitempty"`
KeyFile string `json:"key_file,omitempty"`
AcmeEnabled bool `json:"acme,omitempty"`
AcmeEmail string `json:"acme_email,omitempty"`
AcmeCA string `json:"acme_ca,omitempty"`
}
type PollNet struct {
KeepAliveInterval time.Duration `yaml:"keep_alive_interval"`
KeepAliveInterval Duration `json:"keep_alive_interval"`
}
type Logging struct {
Level string `yaml:"level,omitempty"`
Format string `yaml:"format,omitempty"`
File string `yaml:"file,omitempty"`
Level string `json:"level,omitempty"`
Format string `json:"format,omitempty"`
File string `json:"file,omitempty"`
}
type Database struct {
Type string `yaml:"type,omitempty"`
Url string `yaml:"url,omitempty"`
MaxOpenConns int `yaml:"max_open_conns,omitempty"`
MaxIdleConns int `yaml:"max_idle_conns,omitempty"`
ConnMaxLifetime time.Duration `yaml:"conn_max_life_time,omitempty"`
ConnMaxIdleTime time.Duration `yaml:"conn_max_idle_time,omitempty"`
Type string `json:"type,omitempty"`
Url string `json:"url,omitempty"`
MaxOpenConns int `json:"max_open_conns,omitempty"`
MaxIdleConns int `json:"max_idle_conns,omitempty"`
ConnMaxLifetime Duration `json:"conn_max_life_time,omitempty"`
ConnMaxIdleTime Duration `json:"conn_max_idle_time,omitempty"`
}
type Keys struct {
ControlKey string `yaml:"control_key,omitempty"`
LegacyControlKey string `yaml:"legacy_control_key,omitempty"`
SystemAdminKey string `yaml:"system_admin_key,omitempty"`
ControlKey string `json:"control_key,omitempty"`
LegacyControlKey string `json:"legacy_control_key,omitempty"`
SystemAdminKey string `json:"system_admin_key,omitempty"`
}
type Auth struct {
Provider AuthProvider `yaml:"provider,omitempty"`
SystemAdminPolicy SystemAdminPolicy `yaml:"system_admins"`
Provider AuthProvider `json:"provider,omitempty"`
SystemAdminPolicy SystemAdminPolicy `json:"system_admins"`
}
type AuthProvider struct {
Issuer string `yaml:"issuer"`
ClientID string `yaml:"client_id"`
ClientSecret string `yaml:"client_secret"`
Scopes []string `yaml:"additional_scopes" `
Issuer string `json:"issuer"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Scopes []string `json:"additional_scopes" `
}
type DNS struct {
MagicDNSSuffix string `yaml:"magic_dns_suffix"`
Provider DNSProvider `yaml:"provider,omitempty"`
MagicDNSSuffix string `json:"magic_dns_suffix"`
Provider DNSProvider `json:"provider,omitempty"`
}
type DNSProvider struct {
Name string `yaml:"name"`
Zone string `yaml:"zone"`
Configuration map[string]string `yaml:"config"`
Name string `json:"name"`
Zone string `json:"zone"`
Configuration json.RawMessage `json:"config"`
}
type SystemAdminPolicy struct {
Subs []string `yaml:"subs,omitempty"`
Emails []string `yaml:"emails,omitempty"`
Filters []string `yaml:"filters,omitempty"`
Subs []string `json:"subs,omitempty"`
Emails []string `json:"emails,omitempty"`
Filters []string `json:"filters,omitempty"`
}
type DERP struct {
Server DERPServer `yaml:"server,omitempty"`
Sources []string `yaml:"sources,omitempty"`
Server DERPServer `json:"server,omitempty"`
Sources []string `json:"sources,omitempty"`
}
type DERPServer struct {
Disabled bool `yaml:"disabled,omitempty"`
RegionID int `yaml:"region_id,omitempty"`
RegionCode string `yaml:"region_code,omitempty"`
RegionName string `yaml:"region_name,omitempty"`
Disabled bool `json:"disabled,omitempty"`
RegionID int `json:"region_id,omitempty"`
RegionCode string `json:"region_code,omitempty"`
RegionName string `json:"region_name,omitempty"`
}
func (c *Config) Validate() (*Config, error) {
@@ -356,6 +357,37 @@ func (c *Config) DefaultDERPMap() *tailcfg.DERPMap {
}
}
type Duration time.Duration
func (d Duration) Std() time.Duration {
return time.Duration(d)
}
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Duration(d).String())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
*d = Duration(value)
return nil
case string:
tmp, err := time.ParseDuration(value)
if err != nil {
return err
}
*d = Duration(tmp)
return nil
default:
return fmt.Errorf("invalid duration")
}
}
// Match ${VAR:default} syntax for variables with default values
var optionalEnvRegex = regexp.MustCompile(`\${([a-zA-Z0-9_]+):([^}]*)}`)
+1
View File
@@ -24,6 +24,7 @@ database:
type: ${DB_TYPE:sqlite}
url: ${DB_URL}
max_open_conns: ${DB_MAX_OPEN_CONNS:5}
conn_max_life_time: ${DB_CONN_MAX_LIFE_TIME:5s}
`
if _, err := tempFile.Write([]byte(yamlContent)); err != nil {
t.Fatalf("Failed to write to temp file: %v", err)
+2 -2
View File
@@ -39,8 +39,8 @@ func OpenDB(config *config.Database, logger *zap.Logger) (*sql.DB, domain.Reposi
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime)
sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime)
sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime.Std())
sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime.Std())
repository := domain.NewRepository(db)
+28 -40
View File
@@ -2,9 +2,9 @@ package dns
import (
"context"
"encoding/json"
"fmt"
"github.com/jsiebens/ionscale/internal/config"
"github.com/jsiebens/ionscale/internal/mapping"
"github.com/libdns/azure"
"github.com/libdns/cloudflare"
"github.com/libdns/digitalocean"
@@ -15,6 +15,14 @@ import (
"time"
)
var factories = map[string]func() libdns.RecordSetter{
"azure": azureProvider,
"cloudflare": cloudflareProvider,
"digitalocean": digitalOceanProvider,
"googleclouddns": googleCloudDNSProvider,
"route53": route53Provider,
}
type Provider interface {
SetRecord(ctx context.Context, recordType, recordName, value string) error
}
@@ -29,60 +37,40 @@ func NewProvider(config config.DNS) (Provider, error) {
return nil, fmt.Errorf("invalid MagicDNS suffix [%s], not part of zone [%s]", config.MagicDNSSuffix, p.Zone)
}
switch p.Name {
case "azure":
return configureAzureProvider(p.Zone, p.Configuration)
case "cloudflare":
return configureCloudflareProvider(p.Zone, p.Configuration)
case "digitalocean":
return configureDigitalOceanProvider(p.Zone, p.Configuration)
case "googleclouddns":
return configureGoogleCloudDNSProvider(p.Zone, p.Configuration)
case "route53":
return configureRoute53Provider(p.Zone, p.Configuration)
default:
factory, ok := factories[p.Name]
if !ok {
return nil, fmt.Errorf("unknown dns provider: %s", p.Name)
}
return newProvider(p.Zone, p.Configuration, factory)
}
func configureAzureProvider(zone string, values map[string]string) (Provider, error) {
p := &azure.Provider{}
if err := mapping.CopyViaJson(values, p); err != nil {
func newProvider(zone string, values json.RawMessage, factory func() libdns.RecordSetter) (Provider, error) {
p := factory()
if err := json.Unmarshal(values, p); err != nil {
return nil, err
}
return &externalProvider{zone: fqdn(zone), setter: p}, nil
}
func configureCloudflareProvider(zone string, values map[string]string) (Provider, error) {
p := &cloudflare.Provider{}
if err := mapping.CopyViaJson(values, p); err != nil {
return nil, err
}
return &externalProvider{zone: fqdn(zone), setter: p}, nil
func azureProvider() libdns.RecordSetter {
return &azure.Provider{}
}
func configureDigitalOceanProvider(zone string, values map[string]string) (Provider, error) {
p := &digitalocean.Provider{}
if err := mapping.CopyViaJson(values, p); err != nil {
return nil, err
}
return &externalProvider{zone: fqdn(zone), setter: p}, nil
func cloudflareProvider() libdns.RecordSetter {
return &cloudflare.Provider{}
}
func configureGoogleCloudDNSProvider(zone string, values map[string]string) (Provider, error) {
p := &googleclouddns.Provider{}
if err := mapping.CopyViaJson(values, p); err != nil {
return nil, err
}
return &externalProvider{zone: fqdn(zone), setter: p}, nil
func digitalOceanProvider() libdns.RecordSetter {
return &digitalocean.Provider{}
}
func configureRoute53Provider(zone string, values map[string]string) (Provider, error) {
p := &route53.Provider{}
if err := mapping.CopyViaJson(values, p); err != nil {
return nil, err
}
return &externalProvider{zone: fqdn(zone), setter: p}, nil
func googleCloudDNSProvider() libdns.RecordSetter {
return &googleclouddns.Provider{}
}
func route53Provider() libdns.RecordSetter {
return &route53.Provider{}
}
type externalProvider struct {
-14
View File
@@ -1,7 +1,6 @@
package mapping
import (
"encoding/json"
"fmt"
"github.com/jsiebens/ionscale/internal/config"
"github.com/jsiebens/ionscale/internal/domain"
@@ -15,19 +14,6 @@ import (
"time"
)
func CopyViaJson[F any, T any](f F, t T) error {
raw, err := json.Marshal(f)
if err != nil {
return err
}
if err := json.Unmarshal(raw, t); err != nil {
return err
}
return nil
}
func ToDNSConfig(m *domain.Machine, tailnet *domain.Tailnet, c *domain.DNSConfig) *tailcfg.DNSConfig {
certsEnabled := c.HttpsCertsEnabled && config.DNSProviderConfigured()