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)