diff --git a/cmd/jpictl/dns.go b/cmd/jpictl/dns.go new file mode 100644 index 0000000..307d2ef --- /dev/null +++ b/cmd/jpictl/dns.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "os" + "time" + + "github.com/spf13/cobra" + + "git.jpi.io/amery/jpictl/pkg/cluster" + "git.jpi.io/amery/jpictl/pkg/dns" +) + +const ( + DNSTimeout = 10 * time.Second +) + +func newDNSManager(ctx context.Context, m *cluster.Cluster) (*dns.Manager, error) { + 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 + } + + return mgr, nil +} + +// Command +var dnsCmd = &cobra.Command{ + Use: "dns", +} + +var dnsWriteCmd = &cobra.Command{ + Use: "write", + PreRun: setVerbosity, + RunE: func(_ *cobra.Command, _ []string) error { + m, err := cfg.LoadZones(true) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), DNSTimeout) + defer cancel() + + mgr, err := newDNSManager(ctx, m) + if err != nil { + return err + } + + _, err = mgr.WriteTo(os.Stdout) + return err + }, +} + +func init() { + rootCmd.AddCommand(dnsCmd) + + dnsCmd.AddCommand(dnsWriteCmd) +} diff --git a/go.mod b/go.mod index 8e709ad..10b5f34 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,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 fe01d3b..0248d22 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,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/dns.go b/pkg/dns/dns.go index ca95946..f429b49 100644 --- a/pkg/dns/dns.go +++ b/pkg/dns/dns.go @@ -1,2 +1,30 @@ // Package dns manages DNS entries for the cluster package dns + +//// A Config defines a Region +//type Config struct { +// // Name is the identifier of this Region +// Name string +// // Regions are a list of (sub)regions that belong to this Region +// Regions []string +// // Zones are a list of Zones that directly belong to this Region +// Zones []string +//} +// +//type Region struct { +// Name string +//} +// +//type Zone struct { +// Name string +// +// Machines map[int]*Machine +//} +// +//type Machine struct { +// ID int +// +// Active bool +// Addrs []netip.Addr +//} +// diff --git a/pkg/dns/host.go b/pkg/dns/host.go new file mode 100644 index 0000000..c068836 --- /dev/null +++ b/pkg/dns/host.go @@ -0,0 +1,26 @@ +package dns + +//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, +// Machines: make(map[int]*Machine), +// } +// +// mgr.zones[zone] = z +// } +// +// z.Machines[id] = &Machine{ +// ID: id, +// Active: active, +// Addrs: addrs, +// } +// +// return nil +//} +// diff --git a/pkg/dns/manager.go b/pkg/dns/manager.go new file mode 100644 index 0000000..ccb000d --- /dev/null +++ b/pkg/dns/manager.go @@ -0,0 +1,116 @@ +package dns + +// // Manager is a DNS Manager instance +// type Manager struct { +// mu sync.Mutex +// +// domain string +// suffix string +// zones map[string]*Zone +// +// 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 { +// if hint != "" { +// err = core.Wrap(err, hint) +// } +// return err +// } +// } +// +// // 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.p == nil { +// opts = append(opts, WithProvider(nil)) +// } +// +// if mgr.l == nil { +// opts = append(opts, WithLogger(nil)) +// } +// +// if mgr.domain == "" || mgr.suffix == "" { +// return errors.New("domain not specified") +// } +// +// mgr.zones = make(map[string]*Zone) +// +// 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 := new(Manager) +// +// 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 +// } +// 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 +} diff --git a/pkg/dns/write.go b/pkg/dns/write.go new file mode 100644 index 0000000..d970482 --- /dev/null +++ b/pkg/dns/write.go @@ -0,0 +1,172 @@ +package dns + +// func (mgr *Manager) WriteTo(w io.Writer) (int64, error) { +// var buf bytes.Buffer +// +// var zoneNames sort.StringSlice +// +// // sort zones +// for name := range mgr.zones { +// zoneNames = append(zoneNames, name) +// } +// sort.Sort(zoneNames) +// +// // records +// zones := make(map[string][]libdns.Record) +// +// for _, name := range zoneNames { +// z := mgr.zones[name] +// +// if buf.Len() > 0 { +// _, _ = buf.WriteRune('\n') +// } +// +// // servers +// err := mgr.writeZoneMachinesToBuffer(&buf, name, z) +// if err != nil { +// return 0, err +// } +// +// // zone alias +// _, _ = fmt.Fprintf(&buf, "; %s\n", name) +// records := mgr.genAliasRecords(name, true, z) +// mgr.Sort(records) +// mgr.writeRecordsToBuffer(&buf, "", records...) +// +// zones[name] = records +// } +// +// mgr.writeRegionToBuffer(&buf, zones, "uk", "ssd-lon") +// mgr.writeRegionToBuffer(&buf, zones, "nl", "ssd-ams") +// mgr.writeRegionToBuffer(&buf, zones, "de", "htz-fsn") +// mgr.writeRegionToBuffer(&buf, zones, "eu", "ssd-ams", "htz-fsn") +// mgr.writeRegionToBuffer(&buf, zones, "europe", "ssd-ams", "ssd-lon", "htz-fsn") +// mgr.writeRegionToBuffer(&buf, zones, "earth", "ssd-ams", "ssd-lon", "htz-fsn") +// +// return buf.WriteTo(w) +// } +// +// func (mgr *Manager) writeRecordsToBuffer(buf *bytes.Buffer, rename string, records ...libdns.Record) { +// for _, rr := range records { +// name := core.IIf(rename != "", rename, rr.Name) +// fqdn := fmt.Sprintf("%s.%s.", name, mgr.domain) +// +// _, _ = fmt.Fprintf(buf, "%s\t%v\tIN\t%s\t%v\n", +// fqdn, int(rr.TTL/time.Second), rr.Type, rr.Value) +// } +// } +// +// func (mgr *Manager) writeRegionToBuffer(buf *bytes.Buffer, cache map[string][]libdns.Record, name string, zones ...string) { +// var alias string +// +// _, _ = fmt.Fprintf(buf, "\n; %s\n", name) +// +// switch { +// case mgr.suffix == "": +// // no suffix +// case strings.HasSuffix(name, mgr.suffix): +// // suffixed +// default: +// // unsuffixed +// alias = name +// name = name + mgr.suffix +// } +// +// records := []libdns.Record{} +// for _, zone := range zones { +// records = append(records, cache[zone]...) +// } +// mgr.Sort(records) +// mgr.writeRecordsToBuffer(buf, alias, records...) +// +// if alias != "" { +// rr := libdns.Record{ +// Name: alias, +// TTL: 3600 * time.Second, +// Type: "CNAME", +// Value: name + "." + mgr.domain + ".", +// } +// +// mgr.writeRecordsToBuffer(buf, alias, rr) +// } +// } +// +// func (mgr *Manager) writeZoneMachinesToBuffer(buf *bytes.Buffer, zone_name string, z *Zone) error { +// // title +// _, _ = fmt.Fprintf(buf, ";\n; %s\n;\n", zone_name) +// +// // machine records +// records := mgr.genMachineRecords(z) +// mgr.writeRecordsToBuffer(buf, "", records...) +// +// return nil +// } +// +// func (mgr *Manager) genMachineRecords(z *Zone) []libdns.Record { +// var out []libdns.Record +// +// for _, p := range z.Machines { +// pqdn := fmt.Sprintf("%s-%v%s", z.Name, p.ID, mgr.suffix) +// +// for _, addr := range p.Addrs { +// out = append(out, libdns.Record{ +// Name: pqdn, +// Type: core.IIf(addr.Is6(), "AAAA", "A"), +// TTL: time.Second, +// Value: addr.String(), +// }) +// } +// } +// +// mgr.Sort(out) +// return out +// } +// +// func (mgr *Manager) genAliasRecords(name string, activeOnly bool, zones ...*Zone) []libdns.Record { +// var out []libdns.Record +// +// fqdn := fmt.Sprintf("%s%s", name, mgr.suffix) +// for _, z := range zones { +// for _, p := range z.Machines { +// if p.Active || !activeOnly { +// for _, addr := range p.Addrs { +// out = append(out, libdns.Record{ +// Name: fqdn, +// Type: core.IIf(addr.Is6(), "AAAA", "A"), +// TTL: time.Second, +// Value: addr.String(), +// }) +// } +// } +// } +// } +// +// return out +// } +// +// func (mgr *Manager) Sort(records []libdns.Record) { +// sort.SliceStable(records, func(i, j int) bool { +// a := &records[i] +// b := &records[j] +// switch { +// case a.Name < b.Name: +// return true +// case a.Name > b.Name: +// return false +// case a.Type < b.Type: +// return true +// case a.Type > b.Type: +// return false +// case a.Type == "A" || a.Type == "AAAA": +// var aAddr, bAddr netip.Addr +// aAddr.UnmarshalText([]byte(a.Value)) +// bAddr.UnmarshalText([]byte(b.Value)) +// return aAddr.Less(bAddr) +// case a.Value < b.Value: +// return true +// default: +// return false +// } +// }) +// } +//