installer: env form
diff --git a/core/installer/welcome/index.html b/core/installer/welcome/create-admin-account.html
similarity index 100%
rename from core/installer/welcome/index.html
rename to core/installer/welcome/create-admin-account.html
diff --git a/core/installer/welcome/create-env.html b/core/installer/welcome/create-env.html
new file mode 100644
index 0000000..6633d65
--- /dev/null
+++ b/core/installer/welcome/create-env.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en" data-theme="light">
+	<head>
+        <link rel="stylesheet" href="/static/pico.min.css">
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1" />
+	</head>
+	<body>
+		<div style="display: contents">
+            <main class="container">
+              <article class="grid">
+                <div>
+                  <form action="/env" method="POST">
+                    <input
+                      type="text"
+                      name="name"
+                      placeholder="Name"
+                      required
+                    />
+                    <input
+                      type="test"
+                      name="domain"
+                      placeholder="Domain"
+                      required
+                      />
+                    <input
+                      type="email"
+                      name="contact-email"
+                      placeholder="Contact Email"
+                      required
+                      />
+                    <button type="submit" class="contrast">Create Environment</button>
+                  </form>
+                </div>
+              </article>
+            </main>
+        </div>
+	</body>
+</html>
diff --git a/core/installer/welcome/env-tmpl/config-kustomization.yaml b/core/installer/welcome/env-tmpl/config-kustomization.yaml
new file mode 100644
index 0000000..d76bf0f
--- /dev/null
+++ b/core/installer/welcome/env-tmpl/config-kustomization.yaml
@@ -0,0 +1,13 @@
+apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
+kind: Kustomization
+metadata:
+  name: {{ .Name }}
+  namespace: {{ .Name }}
+spec:
+  interval: 1m
+  path: "./"
+  sourceRef:
+    kind: GitRepository
+    name: {{ .Name }}
+    namespace: {{ .Name }}
+  prune: true
diff --git a/core/installer/welcome/env-tmpl/config-secret.yaml b/core/installer/welcome/env-tmpl/config-secret.yaml
new file mode 100644
index 0000000..3ea515b
--- /dev/null
+++ b/core/installer/welcome/env-tmpl/config-secret.yaml
@@ -0,0 +1,10 @@
+apiVersion: v1
+data:
+  identity: {{ .PrivateKey }}
+  identity.pub: {{ .PublicKey }}
+  known_hosts: {{ .KnownHosts }}
+kind: Secret
+metadata:
+  name: {{ .Name }}
+  namespace: {{ .Name }}
+type: Opaque
diff --git a/core/installer/welcome/env-tmpl/config-source.yaml b/core/installer/welcome/env-tmpl/config-source.yaml
new file mode 100644
index 0000000..113a0b4
--- /dev/null
+++ b/core/installer/welcome/env-tmpl/config-source.yaml
@@ -0,0 +1,14 @@
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+  name: {{ .Name }}
+  namespace: {{ .Name }}
+spec:
+  gitImplementation: go-git
+  interval: 1m0s
+  ref:
+    branch: master
+  secretRef:
+    name: {{ .Name }}
+  timeout: 60s
+  url: ssh://{{ .GitHost }}/{{ .Name }}
diff --git a/core/installer/welcome/env-tmpl/kustomization.yaml b/core/installer/welcome/env-tmpl/kustomization.yaml
new file mode 100644
index 0000000..70db25f
--- /dev/null
+++ b/core/installer/welcome/env-tmpl/kustomization.yaml
@@ -0,0 +1,7 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+- namespace.yaml
+- config-secret.yaml
+- config-source.yaml
+- config-kustomization.yaml
diff --git a/core/installer/welcome/env-tmpl/namespace.yaml b/core/installer/welcome/env-tmpl/namespace.yaml
new file mode 100644
index 0000000..0c14654
--- /dev/null
+++ b/core/installer/welcome/env-tmpl/namespace.yaml
@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{ .Name }}
+  labels:
+    pcloud-instance-id: {{ .Name }}
+  annotations:
+    helm.sh/resource-policy: keep
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
new file mode 100644
index 0000000..e871830
--- /dev/null
+++ b/core/installer/welcome/env.go
@@ -0,0 +1,304 @@
+package welcome
+
+import (
+	"embed"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"path"
+	"text/template"
+
+	"github.com/labstack/echo/v4"
+
+	"github.com/giolekva/pcloud/core/installer"
+	"github.com/giolekva/pcloud/core/installer/soft"
+)
+
+//go:embed env-tmpl
+var filesTmpls embed.FS
+
+//go:embed create-env.html
+var createEnvFormHtml string
+
+type EnvServer struct {
+	port int
+	ss   *soft.Client
+	repo installer.RepoIO
+}
+
+func NewEnvServer(port int, ss *soft.Client, repo installer.RepoIO) *EnvServer {
+	return &EnvServer{
+		port,
+		ss,
+		repo,
+	}
+}
+
+func (s *EnvServer) Start() {
+	e := echo.New()
+	e.StaticFS("/static", echo.MustSubFS(staticAssets, "static"))
+	e.GET("/env", s.createEnvForm)
+	e.POST("/env", s.createEnv)
+	log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
+}
+
+func (s *EnvServer) createEnvForm(c echo.Context) error {
+	return c.HTML(http.StatusOK, createEnvFormHtml)
+}
+
+type createEnvReq struct {
+	Name         string `json:"name"`
+	ContactEmail string `json:"contactEmail"`
+	Domain       string `json:"domain"`
+}
+
+func (s *EnvServer) createEnv(c echo.Context) error {
+	var req createEnvReq
+	if err := func() error {
+		var err error
+		f, err := c.FormParams()
+		if err != nil {
+			return err
+		}
+		if req.Name, err = getFormValue(f, "name"); err != nil {
+			return err
+		}
+		if req.Domain, err = getFormValue(f, "domain"); err != nil {
+			return err
+		}
+		if req.ContactEmail, err = getFormValue(f, "contact-email"); err != nil {
+			return err
+		}
+		return nil
+	}(); err != nil {
+		if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil {
+			return err
+		}
+	}
+	keys, err := installer.NewSSHKeyPair()
+	if err != nil {
+		return err
+	}
+	{
+		readme := fmt.Sprintf("# %s PCloud environment", req.Name)
+		if err := s.ss.AddRepository(req.Name, readme); err != nil {
+			return err
+		}
+		fluxUserName := fmt.Sprintf("flux-%s", req.Name)
+		if err := s.ss.AddUser(fluxUserName, keys.Public); err != nil {
+			return err
+		}
+		if err := s.ss.AddCollaborator(req.Name, fluxUserName); err != nil {
+			return err
+		}
+	}
+	{
+		repo, err := s.ss.GetRepo(req.Name)
+		if repo == nil {
+			return err
+		}
+		if err := initNewEnv(s.ss, installer.NewRepoIO(repo, s.ss.Signer), req); err != nil {
+			return err
+		}
+	}
+	{
+		repo, err := s.ss.GetRepo("pcloud")
+		if err != nil {
+			return err
+		}
+		ssPubKey, err := s.ss.GetPublicKey()
+		if err != nil {
+			return err
+		}
+		if err := addNewEnv(
+			installer.NewRepoIO(repo, s.ss.Signer),
+			req,
+			keys,
+			ssPubKey,
+		); err != nil {
+			return err
+		}
+	}
+	return c.String(http.StatusOK, "OK")
+}
+
+func initNewEnv(ss *soft.Client, r installer.RepoIO, req createEnvReq) error {
+	appManager, err := installer.NewAppManager(r)
+	if err != nil {
+		return err
+	}
+	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
+	// TODO(giolekva): env name and ip should come from pcloud repo config.yaml
+	// TODO(giolekva): private domain can be configurable as well
+	config := installer.Config{
+		Values: installer.Values{
+			PCloudEnvName:   "pcloud",
+			Id:              req.Name,
+			ContactEmail:    req.ContactEmail,
+			Domain:          req.Domain,
+			PrivateDomain:   fmt.Sprintf("p.%s", req.Domain),
+			PublicIP:        "46.49.35.44",
+			NamespacePrefix: fmt.Sprintf("%s-", req.Name),
+		},
+	}
+	if err := r.WriteYaml("config.yaml", config); err != nil {
+		return err
+	}
+	{
+		out, err := r.Writer("pcloud-charts.yaml")
+		if err != nil {
+			return err
+		}
+		defer out.Close()
+		_, err = out.Write([]byte(`
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: GitRepository
+metadata:
+  name: pcloud
+  namespace: lekva
+spec:
+  interval: 1m0s
+  url: https://github.com/giolekva/pcloud
+  ref:
+    branch: main
+`))
+		if err != nil {
+			return err
+		}
+	}
+	rootKust := installer.NewKustomization()
+	rootKust.AddResources("pcloud-charts.yaml", "apps")
+	if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
+		return err
+	}
+	appsKust := installer.NewKustomization()
+	if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
+		return err
+	}
+	r.CommitAndPush("initialize config")
+	{
+		app, err := appsRepo.Find("metallb-config-env")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"IngressPrivate": "10.1.0.1",
+			"Headscale":      "10.1.0.2",
+			"SoftServe":      "10.1.0.3",
+			"Rest": map[string]any{
+				"From": "10.1.0.100",
+				"To":   "10.1.0.255",
+			},
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("ingress-private")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("certificate-issuer-public")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("core-auth")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		app, err := appsRepo.Find("headscale")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"Subdomain": "headscale",
+		}); err != nil {
+			return err
+		}
+	}
+	{
+		keys, err := installer.NewSSHKeyPair()
+		if err != nil {
+			return err
+		}
+		user := fmt.Sprintf("%s-welcome", req.Name)
+		if err := ss.AddUser(user, keys.Public); err != nil {
+			return err
+		}
+		if err := ss.AddCollaborator(req.Name, user); err != nil {
+			return err
+		}
+		app, err := appsRepo.Find("welcome")
+		if err != nil {
+			return err
+		}
+		if err := appManager.Install(*app, map[string]any{
+			"RepoAddr":      ss.GetRepoAddress(req.Name),
+			"SSHPrivateKey": keys.Private,
+		}); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func addNewEnv(
+	repoIO installer.RepoIO,
+	req createEnvReq,
+	keys installer.KeyPair,
+	pcloudRepoPublicKey []byte,
+) error {
+	kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
+	if err != nil {
+		return err
+	}
+	kust.AddResources(req.Name)
+	tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
+	if err != nil {
+		return err
+	}
+	repoIP := "192.168.0.211" // TODO(giolekva): configure
+	for _, tmpl := range tmpls.Templates() {
+		dstPath := path.Join("environments", req.Name, tmpl.Name())
+		dst, err := repoIO.Writer(dstPath)
+		if err != nil {
+			return err
+		}
+		defer dst.Close()
+		if err := tmpl.Execute(dst, map[string]string{
+			"Name":       req.Name,
+			"PrivateKey": base64.StdEncoding.EncodeToString([]byte(keys.Private)),
+			"PublicKey":  base64.StdEncoding.EncodeToString([]byte(keys.Public)),
+			"GitHost":    repoIP,
+			"KnownHosts": base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s %s", repoIP, pcloudRepoPublicKey))),
+		}); err != nil {
+			return err
+		}
+	}
+	if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
+		return err
+	}
+	if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", req.Name)); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/core/installer/welcome/main.go b/core/installer/welcome/welcome.go
similarity index 98%
rename from core/installer/welcome/main.go
rename to core/installer/welcome/welcome.go
index 03a4047..04e08f5 100644
--- a/core/installer/welcome/main.go
+++ b/core/installer/welcome/welcome.go
@@ -13,7 +13,7 @@
 	"github.com/giolekva/pcloud/core/installer"
 )
 
-//go:embed index.html
+//go:embed create-admin-account.html
 var indexHtml string
 
 //go:embed static/*