env-manager: dynamically generate cidr for new env (#85)

* env-manager: allocate env cidrs dynamically

* fix: net.IP to netip.Addr conversion

* bootstrapper: generate empty env-cidrs.yaml

* fix: net.IP to netip.Addr conversion for IP pool

* infra: expose provided startIP subnet via tailscale proxy

* headscale: pass private network ip subnet to expose to api service

* dns: make ingress IP configurable

---------

Co-authored-by: Giorgi Lekveishvili <lekva@gl-mbp-m1-max.local>
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 10a2ade..5436702 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -11,6 +11,7 @@
 	"log"
 	"net"
 	"net/http"
+	"net/netip"
 	"strings"
 
 	"github.com/gorilla/mux"
@@ -293,6 +294,10 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	if err := s.repo.CommitAndPush(fmt.Sprintf("Allocate CIDR for %s", req.Name)); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	// if err := s.acceptInvitation(req.SecretToken); err != nil {
 	// 	http.Error(w, err.Error(), http.StatusInternalServerError)
 	// 	return
@@ -303,6 +308,27 @@
 	} else {
 		req.Name = name
 	}
+	var cidrs installer.EnvCIDRs
+	cidrsR, err := s.repo.Reader("env-cidrs.yaml")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	defer cidrsR.Close()
+	if err := installer.ReadYaml(cidrsR, &cidrs); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	startIP, err := findNextStartIP(cidrs)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	cidrs = append(cidrs, installer.EnvCIDR{req.Name, startIP})
+	if err := s.repo.WriteYaml("env-cidrs.yaml", cidrs); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	t, dns := tasks.NewCreateEnvTask(
 		tasks.Env{
 			PCloudEnvName:  env.Name,
@@ -315,6 +341,7 @@
 			net.ParseIP("135.181.48.180"),
 			net.ParseIP("65.108.39.172"),
 		},
+		startIP,
 		s.nsCreator,
 		s.repo,
 	)
@@ -331,3 +358,33 @@
 	go t.Start()
 	http.Redirect(w, r, fmt.Sprintf("/env/%s", key), http.StatusSeeOther)
 }
+
+func findNextStartIP(cidrs installer.EnvCIDRs) (net.IP, error) {
+	m, err := netip.ParseAddr("10.0.0.0")
+	if err != nil {
+		return nil, err
+	}
+	for _, cidr := range cidrs {
+		i, err := netip.ParseAddr(cidr.IP.String())
+		if err != nil {
+			return nil, err
+		}
+		if i.Compare(m) > 0 {
+			m = i
+		}
+	}
+	sl := m.AsSlice()
+	sl[2]++
+	if sl[2] == 0b11111111 {
+		sl[2] = 0
+		sl[1]++
+	}
+	if sl[1] == 0b11111111 {
+		return nil, fmt.Errorf("Can not allocate")
+	}
+	ret, ok := netip.AddrFromSlice(sl)
+	if !ok {
+		return nil, fmt.Errorf("Must not reach")
+	}
+	return net.ParseIP(ret.String()), nil
+}