Skip to content

Commit 57e8eb3

Browse files
authored
feat: add support for external dns plugins
1 parent f8b0ece commit 57e8eb3

File tree

8 files changed

+427
-37
lines changed

8 files changed

+427
-37
lines changed

go.mod

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ require (
1616
github.com/golang-jwt/jwt/v4 v4.5.2
1717
github.com/hashicorp/go-bexpr v0.1.14
1818
github.com/hashicorp/go-getter v1.7.8
19+
github.com/hashicorp/go-hclog v1.6.3
1920
github.com/hashicorp/go-multierror v1.1.1
21+
github.com/hashicorp/go-plugin v1.6.3
2022
github.com/jsiebens/go-edit v0.1.0
23+
github.com/jsiebens/libdns-plugin v0.1.0
2124
github.com/jsiebens/mockoidc v0.1.0-rc2
2225
github.com/klauspost/compress v1.18.0
2326
github.com/labstack/echo-contrib v0.17.3
@@ -26,7 +29,7 @@ require (
2629
github.com/libdns/cloudflare v0.1.1
2730
github.com/libdns/digitalocean v0.0.0-20230728223659-4f9064657aea
2831
github.com/libdns/googleclouddns v1.1.0
29-
github.com/libdns/libdns v0.2.2
32+
github.com/libdns/libdns v0.2.3
3033
github.com/libdns/route53 v1.3.3
3134
github.com/mitchellh/go-homedir v1.1.0
3235
github.com/mitchellh/pointerstructure v1.2.1
@@ -43,10 +46,10 @@ require (
4346
github.com/travisjeffery/certmagic-sqlstorage v1.1.1
4447
github.com/xhit/go-str2duration/v2 v2.1.0
4548
go.uber.org/zap v1.27.0
46-
golang.org/x/crypto v0.37.0
47-
golang.org/x/net v0.39.0
49+
golang.org/x/crypto v0.38.0
50+
golang.org/x/net v0.40.0
4851
golang.org/x/oauth2 v0.29.0
49-
golang.org/x/sync v0.13.0
52+
golang.org/x/sync v0.14.0
5053
google.golang.org/protobuf v1.36.6
5154
gopkg.in/yaml.v2 v2.4.0
5255
gopkg.in/yaml.v3 v3.0.1
@@ -121,6 +124,7 @@ require (
121124
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
122125
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
123126
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
127+
github.com/fatih/color v1.18.0 // indirect
124128
github.com/felixge/httpsnoop v1.0.4 // indirect
125129
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
126130
github.com/gaissmai/bart v0.11.1 // indirect
@@ -137,6 +141,7 @@ require (
137141
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
138142
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
139143
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
144+
github.com/golang/protobuf v1.5.4 // indirect
140145
github.com/google/btree v1.1.2 // indirect
141146
github.com/google/go-cmp v0.7.0 // indirect
142147
github.com/google/go-querystring v1.1.0 // indirect
@@ -154,6 +159,7 @@ require (
154159
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
155160
github.com/hashicorp/go-safetemp v1.0.0 // indirect
156161
github.com/hashicorp/go-version v1.7.0 // indirect
162+
github.com/hashicorp/yamux v0.1.2 // indirect
157163
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
158164
github.com/illarion/gonotify/v2 v2.0.3 // indirect
159165
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -187,6 +193,7 @@ require (
187193
github.com/mtibben/percent v0.2.1 // indirect
188194
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
189195
github.com/ncruces/go-strftime v0.1.9 // indirect
196+
github.com/oklog/run v1.1.0 // indirect
190197
github.com/opencontainers/go-digest v1.0.0 // indirect
191198
github.com/opencontainers/image-spec v1.1.0 // indirect
192199
github.com/opencontainers/runc v1.2.3 // indirect
@@ -240,18 +247,18 @@ require (
240247
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
241248
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
242249
golang.org/x/mod v0.22.0 // indirect
243-
golang.org/x/sys v0.32.0 // indirect
244-
golang.org/x/term v0.31.0 // indirect
245-
golang.org/x/text v0.24.0 // indirect
250+
golang.org/x/sys v0.33.0 // indirect
251+
golang.org/x/term v0.32.0 // indirect
252+
golang.org/x/text v0.25.0 // indirect
246253
golang.org/x/time v0.11.0 // indirect
247254
golang.org/x/tools v0.29.0 // indirect
248255
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
249256
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
250257
google.golang.org/api v0.230.0 // indirect
251258
google.golang.org/genproto v0.0.0-20250425173222-7b384671a197 // indirect
252259
google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197 // indirect
253-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 // indirect
254-
google.golang.org/grpc v1.72.0 // indirect
260+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9 // indirect
261+
google.golang.org/grpc v1.72.1 // indirect
255262
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect
256263
modernc.org/libc v1.50.3 // indirect
257264
modernc.org/mathutil v1.6.0 // indirect

go.sum

Lines changed: 36 additions & 18 deletions
Large diffs are not rendered by default.

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ type DNS struct {
219219

220220
type DNSProvider struct {
221221
Name string `json:"name"`
222+
PluginPath string `json:"plugin_path"`
222223
Zone string `json:"zone"`
223224
Configuration json.RawMessage `json:"config"`
224225
}

internal/dns/plugin.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package dns
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"github.com/hashicorp/go-plugin"
8+
"github.com/jsiebens/ionscale/internal/util"
9+
dnsplugin "github.com/jsiebens/libdns-plugin"
10+
"github.com/libdns/libdns"
11+
"go.uber.org/zap"
12+
"os/exec"
13+
"sync"
14+
"time"
15+
)
16+
17+
// pluginManager handles plugin lifecycle and resilience
18+
type pluginManager struct {
19+
pluginPath string
20+
client *plugin.Client
21+
instance dnsplugin.Provider
22+
lock sync.RWMutex
23+
logger *zap.Logger
24+
25+
zone string
26+
config json.RawMessage
27+
}
28+
29+
// NewPluginManager creates a new plugin manager
30+
func newPluginManager(pluginPath string, zone string, config json.RawMessage) (*pluginManager, error) {
31+
logger := zap.L().Named("dns").With(zap.String("plugin_path", pluginPath))
32+
33+
p := &pluginManager{
34+
pluginPath: pluginPath,
35+
logger: logger,
36+
zone: zone,
37+
config: config,
38+
}
39+
40+
if err := p.ensureRunning(true); err != nil {
41+
return nil, err
42+
}
43+
44+
return p, nil
45+
}
46+
47+
// ensureRunning makes sure the plugin is running
48+
func (pm *pluginManager) ensureRunning(start bool) error {
49+
pm.lock.RLock()
50+
running := pm.client != nil && !pm.client.Exited()
51+
instance := pm.instance
52+
pm.lock.RUnlock()
53+
54+
if running && instance != nil {
55+
return nil
56+
}
57+
58+
// Need to restart
59+
pm.lock.Lock()
60+
defer pm.lock.Unlock()
61+
62+
if !start {
63+
pm.logger.Info("Restarting DNS plugin")
64+
}
65+
66+
if pm.client != nil {
67+
pm.client.Kill()
68+
}
69+
70+
// Create a new client
71+
cmd := exec.Command(pm.pluginPath)
72+
pm.client = plugin.NewClient(&plugin.ClientConfig{
73+
HandshakeConfig: dnsplugin.Handshake,
74+
Plugins: dnsplugin.PluginMap,
75+
Cmd: cmd,
76+
AllowedProtocols: []plugin.Protocol{
77+
plugin.ProtocolNetRPC,
78+
plugin.ProtocolGRPC,
79+
},
80+
Managed: true,
81+
Logger: util.NewZapAdapter(pm.logger, "dns"),
82+
})
83+
84+
// Connect via RPC
85+
rpcClient, err := pm.client.Client()
86+
if err != nil {
87+
return fmt.Errorf("error creating plugin client: %w", err)
88+
}
89+
90+
// Request the plugin
91+
raw, err := rpcClient.Dispense(dnsplugin.ProviderPluginName)
92+
if err != nil {
93+
return fmt.Errorf("error dispensing plugin: %w", err)
94+
}
95+
96+
// Convert to the interface
97+
pm.instance = raw.(dnsplugin.Provider)
98+
99+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
100+
defer cancel()
101+
102+
if err := pm.instance.Configure(ctx, pm.config); err != nil {
103+
return fmt.Errorf("error configuring plugin: %w", err)
104+
}
105+
106+
pm.logger.Info("DNS plugin started")
107+
108+
return nil
109+
}
110+
111+
func (pm *pluginManager) SetRecord(ctx context.Context, recordType, recordName, value string) error {
112+
if err := pm.ensureRunning(false); err != nil {
113+
return err
114+
}
115+
116+
_, err := pm.instance.SetRecords(ctx, pm.zone, []libdns.Record{{
117+
Type: recordType,
118+
Name: libdns.RelativeName(recordName, pm.zone),
119+
Value: value,
120+
TTL: 1 * time.Minute,
121+
}})
122+
123+
return err
124+
}

internal/dns/provider.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/libdns/googleclouddns"
1212
"github.com/libdns/libdns"
1313
"github.com/libdns/route53"
14+
"go.uber.org/zap"
1415
"strings"
1516
"time"
1617
)
@@ -33,16 +34,24 @@ func NewProvider(config config.DNS) (Provider, error) {
3334
return nil, nil
3435
}
3536

37+
if p.Name == "" && p.PluginPath == "" {
38+
return nil, fmt.Errorf("invalid dns provider configuration, either name or plugin_path should be set")
39+
}
40+
41+
if p.Name != "" && p.PluginPath != "" {
42+
return nil, fmt.Errorf("invalid dns provider configuration, only one of name or plugin_path should be set")
43+
}
44+
3645
if !strings.HasSuffix(config.MagicDNSSuffix, p.Zone) {
3746
return nil, fmt.Errorf("invalid MagicDNS suffix [%s], not part of zone [%s]", config.MagicDNSSuffix, p.Zone)
3847
}
3948

4049
factory, ok := factories[p.Name]
41-
if !ok {
42-
return nil, fmt.Errorf("unknown dns provider: %s", p.Name)
50+
if ok {
51+
return newProvider(p.Zone, p.Configuration, factory)
4352
}
4453

45-
return newProvider(p.Zone, p.Configuration, factory)
54+
return newPluginManager(p.PluginPath, fqdn(p.Zone), p.Configuration)
4655
}
4756

4857
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
5463
}
5564

5665
func azureProvider() libdns.RecordSetter {
66+
zap.L().Warn("Builtin azure DNS plugin is deprecated and will be removed in a future release.")
5767
return &azure.Provider{}
5868
}
5969

6070
func cloudflareProvider() libdns.RecordSetter {
71+
zap.L().Warn("Builtin cloudflare DNS plugin is deprecated and will be removed in a future release.")
6172
return &cloudflare.Provider{}
6273
}
6374

6475
func digitalOceanProvider() libdns.RecordSetter {
76+
zap.L().Warn("Builtin digitalocean DNS plugin is deprecated and will be removed in a future release.")
6577
return &digitalocean.Provider{}
6678
}
6779

6880
func googleCloudDNSProvider() libdns.RecordSetter {
81+
zap.L().Warn("Builtin googleclouddns DNS plugin is deprecated and will be removed in a future release.")
6982
return &googleclouddns.Provider{}
7083
}
7184

7285
func route53Provider() libdns.RecordSetter {
86+
zap.L().Warn("Builtin route53 DNS plugin is deprecated and will be removed in a future release.")
7387
return &route53.Provider{}
7488
}
7589

internal/server/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"github.com/caddyserver/certmagic"
9+
"github.com/hashicorp/go-plugin"
910
"github.com/jsiebens/ionscale/internal/auth"
1011
"github.com/jsiebens/ionscale/internal/config"
1112
"github.com/jsiebens/ionscale/internal/core"
@@ -221,6 +222,7 @@ func Start(ctx context.Context, c *config.Config) error {
221222
go func() {
222223
<-gCtx.Done()
223224
logger.Sugar().Infow("Shutting down ionscale server")
225+
plugin.CleanupClients()
224226
shutdownHttpServer(metricsServer)
225227
shutdownHttpServer(webServer)
226228
_ = stunServer.Shutdown()

0 commit comments

Comments
 (0)