mirror of
https://github.com/jsiebens/ionscale.git
synced 2026-03-31 15:07:49 +01:00
feat: add support for external dns plugins
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user