From 066788b9be143e403119cb410831034bbd71ae34 Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Sun, 10 Sep 2023 18:50:38 +0000 Subject: [PATCH 1/8] vscode: add Lookuper, publicsuffix and libdns to the dictionary Signed-off-by: Alejandro Mery --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index b4287d2..a8459bc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,9 @@ "darvaza", "gofrs", "jpictl", + "libdns", + "Lookuper", + "publicsuffix", "Wrapf", "zerolog" ] From c397ca29acb64139b51f087ef3b96bb3177c6cd6 Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Tue, 12 Sep 2023 13:36:50 +0000 Subject: [PATCH 2/8] cluster: introduce Region interators Signed-off-by: Alejandro Mery --- pkg/cluster/regions.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pkg/cluster/regions.go b/pkg/cluster/regions.go index 134cb2e..c3b716d 100644 --- a/pkg/cluster/regions.go +++ b/pkg/cluster/regions.go @@ -1,5 +1,10 @@ package cluster +var ( + _ MachineIterator = (*Region)(nil) + _ ZoneIterator = (*Region)(nil) +) + // Region represents a group of zones geographically related type Region struct { m *Cluster @@ -9,6 +14,43 @@ type Region struct { Regions []string `json:",omitempty" yaml:",omitempty"` } +// ForEachRegion calls a function for each Region of the cluster +// until instructed to terminate the loop +func (m *Cluster) ForEachRegion(fn func(r *Region) bool) { + for i := range m.Regions { + r := &m.Regions[i] + if fn(r) { + return + } + } +} + +// ForEachMachine calls a function for each Machine in the region +// until instructed to terminate the loop +func (r *Region) ForEachMachine(fn func(*Machine) bool) { + r.ForEachZone(func(z *Zone) bool { + var term bool + + z.ForEachMachine(func(p *Machine) bool { + term = fn(p) + return term + }) + + return term + }) +} + +// ForEachZone calls a function for each Zone in the region +// until instructed to terminate the loop +func (r *Region) ForEachZone(fn func(*Zone) bool) { + for _, p := range r.zones { + if fn(p) { + // terminate + return + } + } +} + func (m *Cluster) initRegions(_ *ScanOptions) error { regions := make(map[string][]*Zone) From e0d8592dc1ef80bdd77bc423546e47e2ae3a025c Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Mon, 11 Sep 2023 23:26:47 +0000 Subject: [PATCH 3/8] dns: introduce AddrRecord{} to abstract A/AAAA entries Signed-off-by: Alejandro Mery --- go.mod | 1 + go.sum | 2 ++ pkg/dns/dns.go | 2 ++ pkg/dns/record.go | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 pkg/dns/dns.go create mode 100644 pkg/dns/record.go diff --git a/go.mod b/go.mod index da2402f..939924e 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( darvaza.org/slog/handlers/discard v0.4.5 github.com/gofrs/uuid/v5 v5.0.0 github.com/hack-pad/hackpadfs v0.2.1 + github.com/libdns/libdns v0.2.1 github.com/mgechev/revive v1.3.3 github.com/spf13/cobra v1.7.0 golang.org/x/crypto v0.12.0 diff --git a/go.sum b/go.sum index 3029c5c..b0a0a72 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMI github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= +github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go new file mode 100644 index 0000000..ca95946 --- /dev/null +++ b/pkg/dns/dns.go @@ -0,0 +1,2 @@ +// Package dns manages DNS entries for the cluster +package dns diff --git a/pkg/dns/record.go b/pkg/dns/record.go new file mode 100644 index 0000000..2f8810d --- /dev/null +++ b/pkg/dns/record.go @@ -0,0 +1,89 @@ +package dns + +import ( + "bytes" + "fmt" + "io" + "net/netip" + "sort" + "time" + + "darvaza.org/core" + "github.com/libdns/libdns" +) + +// SortAddrSlice sorts a slice of [netip.Addr] +func SortAddrSlice(s []netip.Addr) []netip.Addr { + sort.Slice(s, func(i, j int) bool { + return s[i].Less(s[j]) + }) + return s +} + +// SortAddrRecords sorts a slice of [AddrRecord] +// by Name and Address +func SortAddrRecords(s []AddrRecord) []AddrRecord { + sort.Slice(s, func(i, j int) bool { + return s[i].Name < s[j].Name + }) + + for _, p := range s { + SortAddrSlice(p.Addr) + } + + return s +} + +// AddrRecord represents an A or AAAA record +type AddrRecord struct { + Name string + Addr []netip.Addr +} + +// Sort sorts the addresses of the record +func (rr *AddrRecord) Sort() { + SortAddrSlice(rr.Addr) +} + +// Export converts the record into libdns.Record +func (rr *AddrRecord) Export() []libdns.Record { + out := make([]libdns.Record, len(rr.Addr)) + for i, addr := range rr.Addr { + out[i] = libdns.Record{ + Name: rr.Name, + TTL: time.Second * 1, + Type: core.IIf(addr.Is6(), "AAAA", "A"), + Value: addr.String(), + } + } + + return out +} + +// WriteTo writes the record in BIND notation +func (rr *AddrRecord) WriteTo(w io.Writer) (int64, error) { + var total int + for _, addr := range rr.Addr { + n, err := fmt.Fprint(w, + rr.Name, "\t", + 1, "\t", + core.IIf(addr.Is6(), "AAAA", "A"), "\t", + addr.String(), "\n") + + switch { + case err != nil: + return 0, err + case n > 0: + total += n + } + } + + return int64(total), nil +} + +// String converts the record into BIND entries +func (rr *AddrRecord) String() string { + var buf bytes.Buffer + _, _ = rr.WriteTo(&buf) + return buf.String() +} From 00aec477a4f9d48081d98a112c7c382b5cbf2ede Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Tue, 12 Sep 2023 14:46:49 +0000 Subject: [PATCH 4/8] dns: DefaultDNSProvider() using CLOUDFLARE_DNS_API_TOKEN Signed-off-by: Alejandro Mery --- go.mod | 1 + go.sum | 3 +++ pkg/dns/provider.go | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 pkg/dns/provider.go diff --git a/go.mod b/go.mod index 939924e..605a3a2 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( darvaza.org/slog/handlers/discard v0.4.5 github.com/gofrs/uuid/v5 v5.0.0 github.com/hack-pad/hackpadfs v0.2.1 + github.com/libdns/cloudflare v0.1.0 github.com/libdns/libdns v0.2.1 github.com/mgechev/revive v1.3.3 github.com/spf13/cobra v1.7.0 diff --git a/go.sum b/go.sum index b0a0a72..3cc377a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,9 @@ github.com/hack-pad/hackpadfs v0.2.1 h1:FelFhIhv26gyjujoA/yeFO+6YGlqzmc9la/6iKMI github.com/hack-pad/hackpadfs v0.2.1/go.mod h1:khQBuCEwGXWakkmq8ZiFUvUZz84ZkJ2KNwKvChs4OrU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/libdns/cloudflare v0.1.0 h1:93WkJaGaiXCe353LHEP36kAWCUw0YjFqwhkBkU2/iic= +github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8= +github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= diff --git a/pkg/dns/provider.go b/pkg/dns/provider.go new file mode 100644 index 0000000..6a53a24 --- /dev/null +++ b/pkg/dns/provider.go @@ -0,0 +1,36 @@ +package dns + +import ( + "fmt" + "os" + + "github.com/libdns/cloudflare" + "github.com/libdns/libdns" +) + +const ( + // CloudflareAPIToken is the environment variable + // containing the API Token + CloudflareAPIToken = "CLOUDFLARE_DNS_API_TOKEN" +) + +// Provider manages DNS entries +type Provider interface { + libdns.RecordGetter + libdns.RecordDeleter +} + +// DefaultDNSProvider returns a cloudflare DNS provider +// using an API Token from env [CloudflareAPIToken] +func DefaultDNSProvider() (*cloudflare.Provider, error) { + s := os.Getenv(CloudflareAPIToken) + if s == "" { + return nil, fmt.Errorf("%q: %s", CloudflareAPIToken, "not found") + } + + p := &cloudflare.Provider{ + APIToken: s, + } + + return p, nil +} From 357c85dc1a4e1560cd5948c98a5e58bafa648220 Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Tue, 12 Sep 2023 14:48:51 +0000 Subject: [PATCH 5/8] dns: SortRegions() by ISO3166 Signed-off-by: Alejandro Mery --- pkg/dns/record.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/dns/record.go b/pkg/dns/record.go index 2f8810d..4b8deeb 100644 --- a/pkg/dns/record.go +++ b/pkg/dns/record.go @@ -34,6 +34,32 @@ func SortAddrRecords(s []AddrRecord) []AddrRecord { return s } +// SortRegions sorts regions. first by length those 3-character +// or shorter, and then by length. It's mostly aimed at +// supporting ISO-3166 order +func SortRegions(regions []string) []string { + sort.Slice(regions, func(i, j int) bool { + r1, r2 := regions[i], regions[j] + + switch { + case len(r1) < 4: + switch { + case len(r1) < len(r2): + return true + case len(r1) > len(r2): + return false + default: + return r1 < r2 + } + case len(r2) < 4: + return false + default: + return r1 < r2 + } + }) + return regions +} + // AddrRecord represents an A or AAAA record type AddrRecord struct { Name string From f5ea72740c1b4c7189123a8b486d8e8b5c8747ba Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Tue, 12 Sep 2023 15:11:19 +0000 Subject: [PATCH 6/8] dns: introduce initial DNS Manager Signed-off-by: Alejandro Mery --- go.mod | 2 +- pkg/dns/dns.go | 36 ++++++++++ pkg/dns/manager.go | 173 +++++++++++++++++++++++++++++++++++++++++++++ pkg/dns/record.go | 80 +++++++++++++++++++++ 4 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 pkg/dns/manager.go diff --git a/go.mod b/go.mod index 605a3a2..f6f6f05 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/mgechev/revive v1.3.3 github.com/spf13/cobra v1.7.0 golang.org/x/crypto v0.12.0 + golang.org/x/net v0.14.0 gopkg.in/gcfg.v1 v1.2.3 gopkg.in/yaml.v3 v3.0.1 ) @@ -41,7 +42,6 @@ require ( github.com/rs/zerolog v1.30.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/tools v0.12.0 // indirect diff --git a/pkg/dns/dns.go b/pkg/dns/dns.go index ca95946..9fa1ffc 100644 --- a/pkg/dns/dns.go +++ b/pkg/dns/dns.go @@ -1,2 +1,38 @@ // Package dns manages DNS entries for the cluster package dns + +import ( + "fmt" + "net/netip" +) + +// Zone represents a set of hosts with high affinity +type Zone struct { + Name string + + Hosts map[int]*Host +} + +func (z *Zone) String() string { + if z == nil { + return "undetermined" + } + return z.Name +} + +// Host represents a member of the cluster +type Host struct { + zone *Zone + + ID int + Active bool + Addrs []netip.Addr +} + +func (p *Host) String() string { + if p == nil { + return "undetermined" + } + + return fmt.Sprintf("%s-%v", p.zone, p.ID) +} diff --git a/pkg/dns/manager.go b/pkg/dns/manager.go new file mode 100644 index 0000000..8e29252 --- /dev/null +++ b/pkg/dns/manager.go @@ -0,0 +1,173 @@ +package dns + +import ( + "context" + "errors" + "io/fs" + "net/netip" + "strings" + + "darvaza.org/core" + "darvaza.org/slog" + "git.jpi.io/amery/jpictl/pkg/cluster" + "golang.org/x/net/publicsuffix" +) + +// Manager is a DNS Manager instance +type Manager struct { + domain string + suffix string + zones map[string]*Zone + regions map[string][]string + + p Provider + l slog.Logger +} + +// ManagerOption configures a Manager +type ManagerOption func(*Manager) error + +func newErrorManagerOption(err error, hint string) ManagerOption { + return func(*Manager) error { + return core.Wrap(err, hint) + } +} + +// WithProvider attaches a libdns Provider to the Manager +func WithProvider(p Provider) ManagerOption { + var err error + + if p == nil { + p, err = DefaultDNSProvider() + } + + if err != nil { + return newErrorManagerOption(err, "WithProvider") + } + + return func(mgr *Manager) error { + mgr.p = p + return nil + } +} + +// WithLogger attaches a logger to the Manager +func WithLogger(log slog.Logger) ManagerOption { + if log == nil { + log = cluster.DefaultLogger() + } + + return func(mgr *Manager) error { + mgr.l = log + return nil + } +} + +func (mgr *Manager) setDefaults() error { + var opts []ManagerOption + + if mgr.l == nil { + opts = append(opts, WithLogger(nil)) + } + + if mgr.domain == "" || mgr.suffix == "" { + return errors.New("domain not specified") + } + + for _, opt := range opts { + if err := opt(mgr); err != nil { + return err + } + } + return nil +} + +// WithDomain specifies where the manager operates +func WithDomain(domain string) ManagerOption { + base, err := publicsuffix.EffectiveTLDPlusOne(domain) + if err != nil { + return newErrorManagerOption(err, "publicsuffix") + } + + suffix := strings.TrimSuffix(domain, base) + if suffix != "" { + suffix = "." + suffix[:len(suffix)-1] + } + + return func(mgr *Manager) error { + mgr.domain = base + mgr.suffix = suffix + return nil + } +} + +// NewManager creates a DNS manager with the +func NewManager(opts ...ManagerOption) (*Manager, error) { + mgr := &Manager{ + zones: make(map[string]*Zone), + regions: make(map[string][]string), + } + + for _, opt := range opts { + if err := opt(mgr); err != nil { + return nil, err + } + } + + if err := mgr.setDefaults(); err != nil { + return nil, err + } + return mgr, nil +} + +// AddHost registers a host +func (mgr *Manager) AddHost(_ context.Context, zone string, id int, + active bool, addrs ...netip.Addr) error { + // + if zone == "" || id <= 0 { + return fs.ErrInvalid + } + + z, ok := mgr.zones[zone] + if !ok { + z = &Zone{ + Name: zone, + Hosts: make(map[int]*Host), + } + + mgr.zones[zone] = z + } + + p := &Host{ + zone: z, + ID: id, + Active: active, + Addrs: SortAddrSlice(addrs), + } + z.Hosts[id] = p + + if log, ok := mgr.l.Debug().WithEnabled(); ok { + log.WithField("subsystem", "dns"). + WithField("zone", zone). + WithField("host", p.String()). + WithField("active", active). + Print() + } + + return nil +} + +// AddRegion specifies a new region and the zones it contains +func (mgr *Manager) AddRegion(_ context.Context, region string, zones ...string) error { + mgr.regions[region] = append(mgr.regions[region], zones...) + + if log, ok := mgr.l.Debug().WithEnabled(); ok { + for _, zoneName := range zones { + log.WithField("subsystem", "dns"). + WithField("region", region). + WithField("zone", zoneName).Print() + } + } + + return nil +} diff --git a/pkg/dns/record.go b/pkg/dns/record.go index 4b8deeb..407844f 100644 --- a/pkg/dns/record.go +++ b/pkg/dns/record.go @@ -113,3 +113,83 @@ func (rr *AddrRecord) String() string { _, _ = rr.WriteTo(&buf) return buf.String() } + +func (mgr *Manager) genRegionsSorted() []string { + regions := make([]string, 0, len(mgr.regions)) + for name := range mgr.regions { + regions = append(regions, name) + } + + return SortRegions(regions) +} + +func (mgr *Manager) genAllAddrRecords() []AddrRecord { + var out []AddrRecord + + cache := make(map[string][]netip.Addr) + + // zones + for _, z := range mgr.zones { + // hosts + s := mgr.genZoneHostRecords(z) + out = append(out, s...) + + // zone alias + addrs := mgr.genZoneAddresses(z) + name := z.Name + + out = append(out, AddrRecord{ + Name: name + mgr.suffix, + Addr: addrs, + }) + + // and cache for regions + cache[name] = addrs + } + + for _, name := range mgr.genRegionsSorted() { + var addrs []netip.Addr + + for _, z := range mgr.regions[name] { + addrs = append(addrs, cache[z]...) + } + + rec := AddrRecord{ + Name: name + mgr.suffix, + Addr: addrs, + } + rec.Sort() + + out = append(out, rec) + } + + return out +} + +func (*Manager) genZoneAddresses(z *Zone) []netip.Addr { + var out []netip.Addr + + for _, p := range z.Hosts { + if p.Active { + out = append(out, p.Addrs...) + } + } + + SortAddrSlice(out) + return out +} + +func (mgr *Manager) genZoneHostRecords(z *Zone) []AddrRecord { + out := make([]AddrRecord, 0, len(z.Hosts)) + + for _, p := range z.Hosts { + rec := AddrRecord{ + Name: p.String() + mgr.suffix, + Addr: p.Addrs, + } + out = append(out, rec) + } + + SortAddrRecords(out) + return out +} From 1a47985bd75d26178f86b1ba17f52a901066bab0 Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Tue, 12 Sep 2023 15:14:53 +0000 Subject: [PATCH 7/8] dns: Manager.WriteTo() generates BIND config, fully qualifies Signed-off-by: Alejandro Mery --- pkg/dns/record.go | 17 +++++++++++++ pkg/dns/write.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 pkg/dns/write.go diff --git a/pkg/dns/record.go b/pkg/dns/record.go index 407844f..be97701 100644 --- a/pkg/dns/record.go +++ b/pkg/dns/record.go @@ -12,6 +12,10 @@ import ( "github.com/libdns/libdns" ) +func (mgr *Manager) fqdn(name string) string { + return fmt.Sprintf("%s.%s.", name, mgr.domain) +} + // SortAddrSlice sorts a slice of [netip.Addr] func SortAddrSlice(s []netip.Addr) []netip.Addr { sort.Slice(s, func(i, j int) bool { @@ -193,3 +197,16 @@ func (mgr *Manager) genZoneHostRecords(z *Zone) []AddrRecord { SortAddrRecords(out) return out } + +func (mgr *Manager) genRegionAddressesCached(name string, + zones map[string][]netip.Addr) []netip.Addr { + // + var addrs []netip.Addr + + for _, zoneName := range mgr.regions[name] { + addrs = append(addrs, zones[zoneName]...) + } + + SortAddrSlice(addrs) + return addrs +} diff --git a/pkg/dns/write.go b/pkg/dns/write.go new file mode 100644 index 0000000..5fdb05b --- /dev/null +++ b/pkg/dns/write.go @@ -0,0 +1,62 @@ +package dns + +import ( + "bytes" + "fmt" + "io" + "net/netip" +) + +// WriteTo writes the DNS data for the cluster +func (mgr *Manager) WriteTo(w io.Writer) (int64, error) { + var buf bytes.Buffer + + cache := make(map[string][]netip.Addr) + + // zones + for _, z := range mgr.zones { + mgr.writeZoneHosts(&buf, z) + + // zone alias + addrs := mgr.genZoneAddresses(z) + zoneName := z.Name + + rr := AddrRecord{ + Name: mgr.fqdn(zoneName + mgr.suffix), + Addr: addrs, + } + _, _ = rr.WriteTo(&buf) + + // and cache for regions + cache[zoneName] = addrs + } + + // regions, sorted + for _, name := range mgr.genRegionsSorted() { + addrs := mgr.genRegionAddressesCached(name, cache) + + mgr.writeRegionAddresses(&buf, name, addrs) + } + + return buf.WriteTo(w) +} + +func (mgr *Manager) writeZoneHosts(w io.Writer, z *Zone) { + _, _ = fmt.Fprintf(w, ";\n; %s\n;\n", z.Name) + + for _, rr := range mgr.genZoneHostRecords(z) { + rr.Name = mgr.fqdn(rr.Name) + _, _ = rr.WriteTo(w) + } +} + +func (mgr *Manager) writeRegionAddresses(w io.Writer, name string, addrs []netip.Addr) { + _, _ = fmt.Fprintf(w, "; %s\n", name) + + rr := AddrRecord{ + Name: mgr.fqdn(name + mgr.suffix), + Addr: addrs, + } + + _, _ = rr.WriteTo(w) +} From eba0340e32f84db04f7db61788af805a4c8c5c35 Mon Sep 17 00:00:00 2001 From: Alejandro Mery Date: Tue, 12 Sep 2023 15:54:28 +0000 Subject: [PATCH 8/8] jpictl: introduce `jpictl dns write` command it renders BIND config to describe the public view of the cluster Signed-off-by: Alejandro Mery --- cmd/jpictl/dns.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 cmd/jpictl/dns.go diff --git a/cmd/jpictl/dns.go b/cmd/jpictl/dns.go new file mode 100644 index 0000000..6520bbf --- /dev/null +++ b/cmd/jpictl/dns.go @@ -0,0 +1,82 @@ +package main + +import ( + "context" + "os" + + "github.com/spf13/cobra" + + "git.jpi.io/amery/jpictl/pkg/cluster" + "git.jpi.io/amery/jpictl/pkg/dns" +) + +func newDNSManager(m *cluster.Cluster) (*dns.Manager, error) { + ctx := context.TODO() + + domain := m.Domain + if m.Name != "" { + domain = m.Name + "." + domain + } + + mgr, err := dns.NewManager(dns.WithDomain(domain), dns.WithLogger(log)) + if err != nil { + return nil, err + } + + m.ForEachZone(func(z *cluster.Zone) bool { + z.ForEachMachine(func(p *cluster.Machine) bool { + err = mgr.AddHost(ctx, z.Name, p.ID, true, p.PublicAddresses...) + return err != nil + }) + + return err != nil + }) + if err != nil { + return nil, err + } + + m.ForEachRegion(func(r *cluster.Region) bool { + r.ForEachZone(func(z *cluster.Zone) bool { + err = mgr.AddRegion(ctx, r.Name, z.Name) + return err != nil + }) + + return err != nil + }) + if err != nil { + return nil, err + } + + return mgr, nil +} + +// Command +var dnsCmd = &cobra.Command{ + Use: "dns", +} + +var dnsWriteCmd = &cobra.Command{ + Use: "write", + Short: "dns write generates public DNS records", + PreRun: setVerbosity, + RunE: func(_ *cobra.Command, _ []string) error { + m, err := cfg.LoadZones(true) + if err != nil { + return err + } + + mgr, err := newDNSManager(m) + if err != nil { + return err + } + + _, err = mgr.WriteTo(os.Stdout) + return err + }, +} + +func init() { + rootCmd.AddCommand(dnsCmd) + + dnsCmd.AddCommand(dnsWriteCmd) +}