From 828e0c920b045b95fc6bf3ef9a7f0db15d6fd369 Mon Sep 17 00:00:00 2001 From: Johan Siebens Date: Mon, 10 Mar 2025 08:40:17 +0100 Subject: [PATCH] chore: use yaml-to-json library, giving better support for configuring third-party libs like libdns providers --- go.mod | 5 +- go.sum | 6 +- internal/config/config.go | 146 ++++++++++++++++++++------------- internal/config/config_test.go | 1 + internal/database/database.go | 4 +- internal/dns/provider.go | 68 +++++++-------- internal/mapping/mapping.go | 14 ---- 7 files changed, 124 insertions(+), 120 deletions(-) diff --git a/go.mod b/go.mod index 9665d58..f9f3f9e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f327301..b22fde3 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/config/config.go b/internal/config/config.go index fb1f330..e259fd6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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_]+):([^}]*)}`) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7a96d73..be9f8cd 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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) diff --git a/internal/database/database.go b/internal/database/database.go index 2be454b..8e713ef 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -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) diff --git a/internal/dns/provider.go b/internal/dns/provider.go index dcf5562..98e6b30 100644 --- a/internal/dns/provider.go +++ b/internal/dns/provider.go @@ -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 { diff --git a/internal/mapping/mapping.go b/internal/mapping/mapping.go index 9a9059a..7426b50 100644 --- a/internal/mapping/mapping.go +++ b/internal/mapping/mapping.go @@ -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()