feat: add support for external dns plugins

This commit is contained in:
Johan Siebens
2025-05-24 11:01:05 +02:00
committed by GitHub
parent f8b0eceae7
commit 57e8eb3a25
8 changed files with 427 additions and 37 deletions
+1
View File
@@ -219,6 +219,7 @@ type DNS struct {
type DNSProvider struct {
Name string `json:"name"`
PluginPath string `json:"plugin_path"`
Zone string `json:"zone"`
Configuration json.RawMessage `json:"config"`
}
+124
View File
@@ -0,0 +1,124 @@
package dns
import (
"context"
"encoding/json"
"fmt"
"github.com/hashicorp/go-plugin"
"github.com/jsiebens/ionscale/internal/util"
dnsplugin "github.com/jsiebens/libdns-plugin"
"github.com/libdns/libdns"
"go.uber.org/zap"
"os/exec"
"sync"
"time"
)
// pluginManager handles plugin lifecycle and resilience
type pluginManager struct {
pluginPath string
client *plugin.Client
instance dnsplugin.Provider
lock sync.RWMutex
logger *zap.Logger
zone string
config json.RawMessage
}
// NewPluginManager creates a new plugin manager
func newPluginManager(pluginPath string, zone string, config json.RawMessage) (*pluginManager, error) {
logger := zap.L().Named("dns").With(zap.String("plugin_path", pluginPath))
p := &pluginManager{
pluginPath: pluginPath,
logger: logger,
zone: zone,
config: config,
}
if err := p.ensureRunning(true); err != nil {
return nil, err
}
return p, nil
}
// ensureRunning makes sure the plugin is running
func (pm *pluginManager) ensureRunning(start bool) error {
pm.lock.RLock()
running := pm.client != nil && !pm.client.Exited()
instance := pm.instance
pm.lock.RUnlock()
if running && instance != nil {
return nil
}
// Need to restart
pm.lock.Lock()
defer pm.lock.Unlock()
if !start {
pm.logger.Info("Restarting DNS plugin")
}
if pm.client != nil {
pm.client.Kill()
}
// Create a new client
cmd := exec.Command(pm.pluginPath)
pm.client = plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: dnsplugin.Handshake,
Plugins: dnsplugin.PluginMap,
Cmd: cmd,
AllowedProtocols: []plugin.Protocol{
plugin.ProtocolNetRPC,
plugin.ProtocolGRPC,
},
Managed: true,
Logger: util.NewZapAdapter(pm.logger, "dns"),
})
// Connect via RPC
rpcClient, err := pm.client.Client()
if err != nil {
return fmt.Errorf("error creating plugin client: %w", err)
}
// Request the plugin
raw, err := rpcClient.Dispense(dnsplugin.ProviderPluginName)
if err != nil {
return fmt.Errorf("error dispensing plugin: %w", err)
}
// Convert to the interface
pm.instance = raw.(dnsplugin.Provider)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := pm.instance.Configure(ctx, pm.config); err != nil {
return fmt.Errorf("error configuring plugin: %w", err)
}
pm.logger.Info("DNS plugin started")
return nil
}
func (pm *pluginManager) SetRecord(ctx context.Context, recordType, recordName, value string) error {
if err := pm.ensureRunning(false); err != nil {
return err
}
_, err := pm.instance.SetRecords(ctx, pm.zone, []libdns.Record{{
Type: recordType,
Name: libdns.RelativeName(recordName, pm.zone),
Value: value,
TTL: 1 * time.Minute,
}})
return err
}
+17 -3
View File
@@ -11,6 +11,7 @@ import (
"github.com/libdns/googleclouddns"
"github.com/libdns/libdns"
"github.com/libdns/route53"
"go.uber.org/zap"
"strings"
"time"
)
@@ -33,16 +34,24 @@ func NewProvider(config config.DNS) (Provider, error) {
return nil, nil
}
if p.Name == "" && p.PluginPath == "" {
return nil, fmt.Errorf("invalid dns provider configuration, either name or plugin_path should be set")
}
if p.Name != "" && p.PluginPath != "" {
return nil, fmt.Errorf("invalid dns provider configuration, only one of name or plugin_path should be set")
}
if !strings.HasSuffix(config.MagicDNSSuffix, p.Zone) {
return nil, fmt.Errorf("invalid MagicDNS suffix [%s], not part of zone [%s]", config.MagicDNSSuffix, p.Zone)
}
factory, ok := factories[p.Name]
if !ok {
return nil, fmt.Errorf("unknown dns provider: %s", p.Name)
if ok {
return newProvider(p.Zone, p.Configuration, factory)
}
return newProvider(p.Zone, p.Configuration, factory)
return newPluginManager(p.PluginPath, fqdn(p.Zone), p.Configuration)
}
func newProvider(zone string, values json.RawMessage, factory func() libdns.RecordSetter) (Provider, error) {
@@ -54,22 +63,27 @@ func newProvider(zone string, values json.RawMessage, factory func() libdns.Reco
}
func azureProvider() libdns.RecordSetter {
zap.L().Warn("Builtin azure DNS plugin is deprecated and will be removed in a future release.")
return &azure.Provider{}
}
func cloudflareProvider() libdns.RecordSetter {
zap.L().Warn("Builtin cloudflare DNS plugin is deprecated and will be removed in a future release.")
return &cloudflare.Provider{}
}
func digitalOceanProvider() libdns.RecordSetter {
zap.L().Warn("Builtin digitalocean DNS plugin is deprecated and will be removed in a future release.")
return &digitalocean.Provider{}
}
func googleCloudDNSProvider() libdns.RecordSetter {
zap.L().Warn("Builtin googleclouddns DNS plugin is deprecated and will be removed in a future release.")
return &googleclouddns.Provider{}
}
func route53Provider() libdns.RecordSetter {
zap.L().Warn("Builtin route53 DNS plugin is deprecated and will be removed in a future release.")
return &route53.Provider{}
}
+2
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"github.com/caddyserver/certmagic"
"github.com/hashicorp/go-plugin"
"github.com/jsiebens/ionscale/internal/auth"
"github.com/jsiebens/ionscale/internal/config"
"github.com/jsiebens/ionscale/internal/core"
@@ -221,6 +222,7 @@ func Start(ctx context.Context, c *config.Config) error {
go func() {
<-gCtx.Done()
logger.Sugar().Infow("Shutting down ionscale server")
plugin.CleanupClients()
shutdownHttpServer(metricsServer)
shutdownHttpServer(webServer)
_ = stunServer.Shutdown()
+152
View File
@@ -0,0 +1,152 @@
package util
import (
"fmt"
"io"
"log"
"strings"
"github.com/hashicorp/go-hclog"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
type ZapHCLogAdapter struct {
zapLogger *zap.Logger
name string
}
func NewZapAdapter(zapLogger *zap.Logger, name string) *ZapHCLogAdapter {
return &ZapHCLogAdapter{
zapLogger: zapLogger.WithOptions(zap.AddCallerSkip(2)),
name: name,
}
}
func (z *ZapHCLogAdapter) Log(level hclog.Level, msg string, args ...interface{}) {
fields := make([]zap.Field, 0, len(args)/2)
for i := 0; i < len(args); i += 2 {
if i+1 < len(args) {
key, ok := args[i].(string)
if !ok {
key = fmt.Sprintf("%v", args[i])
}
fields = append(fields, zap.Any(key, args[i+1]))
}
}
switch level {
case hclog.Trace:
z.zapLogger.Debug(msg, fields...)
case hclog.Debug:
z.zapLogger.Debug(msg, fields...)
case hclog.Info:
z.zapLogger.Info(msg, fields...)
case hclog.Warn:
z.zapLogger.Warn(msg, fields...)
case hclog.Error:
z.zapLogger.Error(msg, fields...)
}
}
func (z *ZapHCLogAdapter) Trace(msg string, args ...interface{}) {
z.Log(hclog.Trace, msg, args...)
}
func (z *ZapHCLogAdapter) Debug(msg string, args ...interface{}) {
z.Log(hclog.Debug, msg, args...)
}
func (z *ZapHCLogAdapter) Info(msg string, args ...interface{}) {
z.Log(hclog.Info, msg, args...)
}
func (z *ZapHCLogAdapter) Warn(msg string, args ...interface{}) {
z.Log(hclog.Warn, msg, args...)
}
func (z *ZapHCLogAdapter) Error(msg string, args ...interface{}) {
z.Log(hclog.Error, msg, args...)
}
func (z *ZapHCLogAdapter) IsTrace() bool {
return z.zapLogger.Core().Enabled(zapcore.DebugLevel)
}
func (z *ZapHCLogAdapter) IsDebug() bool {
return z.zapLogger.Core().Enabled(zapcore.DebugLevel)
}
func (z *ZapHCLogAdapter) IsInfo() bool {
return z.zapLogger.Core().Enabled(zapcore.InfoLevel)
}
func (z *ZapHCLogAdapter) IsWarn() bool {
return z.zapLogger.Core().Enabled(zapcore.WarnLevel)
}
func (z *ZapHCLogAdapter) IsError() bool {
return z.zapLogger.Core().Enabled(zapcore.ErrorLevel)
}
func (z *ZapHCLogAdapter) ImpliedArgs() []interface{} {
return nil
}
func (z *ZapHCLogAdapter) With(args ...interface{}) hclog.Logger {
if len(args) == 0 {
return z
}
fields := make([]zap.Field, 0, len(args)/2)
for i := 0; i < len(args); i += 2 {
if i+1 < len(args) {
key, ok := args[i].(string)
if !ok {
key = fmt.Sprintf("%v", args[i])
}
fields = append(fields, zap.Any(key, args[i+1]))
}
}
newLogger := z.zapLogger.With(fields...)
return &ZapHCLogAdapter{
zapLogger: newLogger,
}
}
func (z *ZapHCLogAdapter) Name() string {
return z.name
}
func (z *ZapHCLogAdapter) Named(name string) hclog.Logger {
return &ZapHCLogAdapter{zapLogger: z.zapLogger.Named(name), name: name}
}
func (z *ZapHCLogAdapter) ResetNamed(name string) hclog.Logger {
return &ZapHCLogAdapter{
zapLogger: z.zapLogger,
name: name,
}
}
func (z *ZapHCLogAdapter) SetLevel(level hclog.Level) {
}
func (z *ZapHCLogAdapter) GetLevel() hclog.Level {
return hclog.LevelFromString(z.zapLogger.Level().String())
}
func (z *ZapHCLogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
return log.New(z, "", log.LstdFlags)
}
func (z *ZapHCLogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
return z
}
func (z *ZapHCLogAdapter) Write(p []byte) (n int, err error) {
s := strings.TrimSpace(string(p))
z.Info(s)
return len(p), nil
}