installer-env: automate dns update with registrar
diff --git a/core/installer/kube.go b/core/installer/kube.go
index bc57805..b7fd9a4 100644
--- a/core/installer/kube.go
+++ b/core/installer/kube.go
@@ -22,8 +22,13 @@
Create(name string) error
}
+type ZoneInfo struct {
+ Zone string
+ Records string
+}
+
type ZoneStatusFetcher interface {
- Fetch(namespace, name string) (error, bool, string)
+ Fetch(namespace, name string) (error, bool, ZoneInfo)
}
type realNamespaceCreator struct {
@@ -50,22 +55,22 @@
clientset dynamic.Interface
}
-func (f *realZoneStatusFetcher) Fetch(namespace, name string) (error, bool, string) {
+func (f *realZoneStatusFetcher) Fetch(namespace, name string) (error, bool, ZoneInfo) {
dnsZoneRes := schema.GroupVersionResource{Group: "dodo.cloud.dodo.cloud", Version: "v1", Resource: "dnszones"}
zoneUnstr, err := f.clientset.Resource(dnsZoneRes).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
fmt.Printf("%+v %+v\n", zoneUnstr, err)
if err != nil {
- return err, false, ""
+ return err, false, ZoneInfo{}
}
var contents bytes.Buffer
if err := json.NewEncoder(&contents).Encode(zoneUnstr.Object); err != nil {
- return err, false, ""
+ return err, false, ZoneInfo{}
}
var zone dnsv1.DNSZone
if err := json.NewDecoder(&contents).Decode(&zone); err != nil {
- return err, false, ""
+ return err, false, ZoneInfo{}
}
- return nil, zone.Status.Ready, zone.Status.RecordsToPublish
+ return nil, zone.Status.Ready, ZoneInfo{zone.Spec.Zone, zone.Status.RecordsToPublish}
}
func NewNamespaceCreator(kubeconfig string) (NamespaceCreator, error) {
diff --git a/core/installer/tasks/dns.go b/core/installer/tasks/dns.go
index 4a33670..02d8fee 100644
--- a/core/installer/tasks/dns.go
+++ b/core/installer/tasks/dns.go
@@ -123,6 +123,7 @@
}
check := func(check Check) error {
addrs, err := net.LookupIP(name)
+ fmt.Printf("DNS LOOKUP: %+v\n", addrs)
if err == nil && gotExpectedIPs(addrs) {
return err
}
diff --git a/core/installer/welcome/dns.go b/core/installer/welcome/dns.go
new file mode 100644
index 0000000..943881e
--- /dev/null
+++ b/core/installer/welcome/dns.go
@@ -0,0 +1,67 @@
+package welcome
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "strings"
+ "time"
+
+ "github.com/libdns/gandi"
+ "github.com/libdns/libdns"
+)
+
+type DNSUpdater interface {
+ Update(zone string, records []string) error
+}
+
+func ParseRecords(zone string, records []string) ([]libdns.Record, error) {
+ var rrs []libdns.Record
+ for _, r := range records {
+ if r == "" {
+ continue
+ }
+ fmt.Println(r)
+ var name string
+ var ttl time.Duration
+ var tmp string
+ var t string
+ l := strings.NewReader(r)
+ if _, err := fmt.Fscanf(l, "%s %d %s %s", &name, &ttl, &tmp, &t); err != nil {
+ return nil, err
+ }
+ var value strings.Builder
+ if _, err := io.Copy(&value, l); err != nil {
+ return nil, err
+ }
+ val := strings.TrimSpace(value.String())
+ fmt.Printf("%s -- %d -- %s -- %s\n", name, ttl, t, val)
+ rrs = append(rrs, libdns.Record{
+ Type: t,
+ Name: libdns.RelativeName(name, zone),
+ Value: val,
+ TTL: ttl * time.Second,
+ })
+ }
+ return rrs, nil
+}
+
+type gandiUpdater struct {
+ provider libdns.RecordSetter
+}
+
+func NewGandiUpdater(apiToken string) *gandiUpdater {
+ return &gandiUpdater{
+ provider: &gandi.Provider{APIToken: apiToken},
+ }
+}
+
+func (u *gandiUpdater) Update(zone string, records []string) error {
+ if rrs, err := ParseRecords(zone, records); err != nil {
+ return err
+ } else {
+ fmt.Printf("%+v\n", rrs)
+ _, err := u.provider.SetRecords(context.TODO(), zone, rrs)
+ return err
+ }
+}
diff --git a/core/installer/welcome/dns_test.go b/core/installer/welcome/dns_test.go
new file mode 100644
index 0000000..4eec16b
--- /dev/null
+++ b/core/installer/welcome/dns_test.go
@@ -0,0 +1,25 @@
+package welcome
+
+import (
+ "strings"
+ "testing"
+)
+
+const rec = `
+t40.lekva.me. 3600 IN DS 43870 13 2 9ADA4E046EC0473383035B7BDB6443B8D869A9C8B35D000B8038ABF3F3864621
+ns1.t40.lekva.me. 10800 IN A 135.181.48.180
+ns2.t40.lekva.me. 10800 IN A 65.108.39.172
+t40.lekva.me. 10800 IN NS ns1.t40.lekva.me.
+t40.lekva.me. 10800 IN NS ns2.t40.lekva.me.
+`
+
+func TestParse(t *testing.T) {
+ zone := "lekva.me."
+ recs, err := ParseRecords(zone, strings.Split(rec, "\n"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(recs) != 5 {
+ t.Fatalf("Expected 5 records, got %d", len(recs))
+ }
+}
diff --git a/core/installer/welcome/env-created.html b/core/installer/welcome/env-created.html
index 5e54fb3..c50457e 100644
--- a/core/installer/welcome/env-created.html
+++ b/core/installer/welcome/env-created.html
@@ -20,7 +20,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{{ if not (or (eq .Root.Status 2) (eq .Root.Status 3))}}
- <meta http-equiv="refresh" content="10">
+ <meta http-equiv="refresh" content="60">
{{ end }}
</head>
<body>
@@ -35,22 +35,35 @@
</ul>
</nav>
<main class="container">
+ <article>
+ <ul>
+ {{ template "task" .Root.Subtasks }}
+ </ul>
+ </article>
{{ if .DNSRecords }}
- <form action="" method="POST">
- You will have to publish following DNS records via your domain registrar.
- <textarea disabled>{{ .DNSRecords }}</textarea>
- <label for="domain-registrar">Domain Registrar</label>
- <select id="domain-registrar" required>
- <option value="" selected>Select registrar</option>
- <option value="gandi">Gandi</option>
- <option value="namecheap">Namecheap</option>
- </select>
- <button type="submit" tabindex="1">Update</button>
- </form>
+ <div>
+ <form action="" method="POST">
+ You will have to publish following DNS records via your domain registrar.
+ <textarea rows="7">{{ .DNSRecords }}</textarea>
+ <label for="domain-registrar">Domain Registrar</label>
+ <select id="domain-registrar" required tabindex="1">
+ <option value="" selected>Select registrar</option>
+ <option value="gandi">Gandi</option>
+ <option value="namecheap">Namecheap</option>
+ </select>
+ <label for="api-token">API Token</label>
+ <input
+ type="text"
+ id="api-token"
+ name="api-token"
+ required
+ autofocus
+ tabindex="2"
+ />
+ <button type="submit" tabindex="3">Update</button>
+ </form>
+ </div>
{{ end }}
- <ul>
- {{ template "task" .Root.Subtasks }}
- </ul>
</main>
</body>
</html>
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 7cc3c1a..7514adf 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -11,6 +11,7 @@
"log"
"net"
"net/http"
+ "strings"
"github.com/gorilla/mux"
@@ -73,6 +74,7 @@
r := mux.NewRouter()
r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
r.Path("/env/{key}").Methods("GET").HandlerFunc(s.monitorTask)
+ r.Path("/env/{key}").Methods("POST").HandlerFunc(s.publishDNSRecords)
r.Path("/").Methods("GET").HandlerFunc(s.createEnvForm)
r.Path("/").Methods("POST").HandlerFunc(s.createEnv)
r.Path("/create-invitation").Methods("GET").HandlerFunc(s.createInvitation)
@@ -97,12 +99,12 @@
http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
return
}
- err, ready, records := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
+ err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
// TODO(gio): check error type
- if err != nil && (ready || len(records) > 0) {
+ if err != nil && (ready || len(info.Records) > 0) {
panic("!! SHOULD NOT REACH !!")
}
- if !ready && len(records) > 0 {
+ if !ready && len(info.Records) > 0 {
panic("!! SHOULD NOT REACH !!")
}
tmpl, err := htemplate.New("response").Parse(envCreatedHtml)
@@ -112,13 +114,48 @@
}
if err := tmpl.Execute(w, map[string]any{
"Root": t,
- "DNSRecords": records,
+ "DNSRecords": info.Records,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
+func (s *EnvServer) publishDNSRecords(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ key, ok := vars["key"]
+ if !ok {
+ http.Error(w, "Task key not provided", http.StatusBadRequest)
+ return
+ }
+ dnsRef, ok := s.dns[key]
+ if !ok {
+ http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
+ return
+ }
+ err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
+ // TODO(gio): check error type
+ if err != nil && (ready || len(info.Records) > 0) {
+ panic("!! SHOULD NOT REACH !!")
+ }
+ if !ready && len(info.Records) > 0 {
+ panic("!! SHOULD NOT REACH !!")
+ }
+ r.ParseForm()
+ if apiToken, err := getFormValue(r.PostForm, "api-token"); err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ } else {
+ p := NewGandiUpdater(apiToken)
+ zone := strings.Join(strings.Split(info.Zone, ".")[1:], ".") // TODO(gio): this is not gonna work with no subdomain case
+ if err := p.Update(zone, strings.Split(info.Records, "\n")); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ }
+ http.Redirect(w, r, "/env/foo", http.StatusSeeOther)
+}
+
func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
if _, err := w.Write(createEnvFormHtml); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
diff --git a/core/ns-controller/controllers/dnszone_controller.go b/core/ns-controller/controllers/dnszone_controller.go
index 8b32ecf..53db4a1 100644
--- a/core/ns-controller/controllers/dnszone_controller.go
+++ b/core/ns-controller/controllers/dnszone_controller.go
@@ -19,6 +19,7 @@
import (
"context"
"fmt"
+ "strings"
"time"
corev1 "k8s.io/api/core/v1"
@@ -127,7 +128,9 @@
}
resource.Status.Ready = true
if zoneConfig.DNSSec != nil {
- resource.Status.RecordsToPublish = string(zoneConfig.DNSSec.DS)
+ rrs := []string{string(zoneConfig.DNSSec.DS)}
+ rrs = append(rrs, GenerateNSRecords(zoneConfig)...)
+ resource.Status.RecordsToPublish = strings.Join(rrs, "\n")
}
if err := r.Status().Update(context.Background(), resource); err != nil {
return ctrl.Result{RequeueAfter: time.Minute}, err
diff --git a/core/ns-controller/controllers/store.go b/core/ns-controller/controllers/store.go
index 533e767..fd2d158 100644
--- a/core/ns-controller/controllers/store.go
+++ b/core/ns-controller/controllers/store.go
@@ -6,6 +6,7 @@
"io"
"io/fs"
"os"
+ "strings"
"text/template"
"github.com/Masterminds/sprig/v3"
@@ -34,6 +35,16 @@
DNSSec *DNSSecKey `json:"dnsSec,omitempty"`
}
+func GenerateNSRecords(z ZoneConfig) []string {
+ subdomain := strings.Split(z.Zone, ",")[0]
+ ret := make([]string, 0)
+ for i, ip := range z.Nameservers {
+ ret = append(ret, fmt.Sprintf("ns%d.%s 10800 IN A %s", i+1, z.Zone, ip))
+ ret = append(ret, fmt.Sprintf("%s. 10800 IN NS ns%d.%s.", subdomain, i+1, z.Zone))
+ }
+ return ret
+}
+
type ZoneStoreFactory interface {
ConfigPath() string
Create(zone ZoneConfig) (ZoneStore, error)