installer: generate and use invitations
diff --git a/core/installer/cmd/env_manager.go b/core/installer/cmd/env_manager.go
index 8e21c67..66bfb85 100644
--- a/core/installer/cmd/env_manager.go
+++ b/core/installer/cmd/env_manager.go
@@ -79,6 +79,7 @@
ss,
repoIO,
nsCreator,
+ installer.NewFixedLengthRandomNameGenerator(4),
)
log.Printf("Starting server\n")
s.Start()
diff --git a/core/installer/kube.go b/core/installer/kube.go
index 3fa253b..25a7103 100644
--- a/core/installer/kube.go
+++ b/core/installer/kube.go
@@ -2,7 +2,6 @@
import (
"context"
- "fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
diff --git a/core/installer/name.go b/core/installer/name.go
new file mode 100644
index 0000000..f0ba6a4
--- /dev/null
+++ b/core/installer/name.go
@@ -0,0 +1,29 @@
+package installer
+
+import (
+ "crypto/rand"
+)
+
+type NameGenerator interface {
+ Generate() (string, error)
+}
+
+type fixedLengthRandomNameGenerator struct {
+ len int
+}
+
+func NewFixedLengthRandomNameGenerator(len int) NameGenerator {
+ return &fixedLengthRandomNameGenerator{len}
+}
+
+func (g *fixedLengthRandomNameGenerator) Generate() (string, error) {
+ r := make([]byte, g.len)
+ if _, err := rand.Read(r); err != nil {
+ return "", err
+ }
+ ret := make([]rune, g.len)
+ for i, v := range r {
+ ret[i] += letters[v%26]
+ }
+ return string(ret), nil
+}
diff --git a/core/installer/welcome/create-env.html b/core/installer/welcome/create-env.html
index 6633d65..abaa197 100644
--- a/core/installer/welcome/create-env.html
+++ b/core/installer/welcome/create-env.html
@@ -12,12 +12,6 @@
<div>
<form action="/env" method="POST">
<input
- type="text"
- name="name"
- placeholder="Name"
- required
- />
- <input
type="test"
name="domain"
placeholder="Domain"
@@ -28,7 +22,12 @@
name="contact-email"
placeholder="Contact Email"
required
- />
+ />
+ <textarea
+ name="secret-token"
+ placeholder="Secret Token"
+ required
+ ></textarea>
<button type="submit" class="contrast">Create Environment</button>
</form>
</div>
diff --git a/core/installer/welcome/env-created.html b/core/installer/welcome/env-created.html
new file mode 100644
index 0000000..0a64116
--- /dev/null
+++ b/core/installer/welcome/env-created.html
@@ -0,0 +1,15 @@
+<!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">
+ Environment for {{ .Domain }} is being configured. In few minutes go to <a href="https://welcome.{{ .Domain }}">https://welcome.<b>{{ .Domain }}</b></a> to set up administrative account.
+ </main>
+ </div>
+ </body>
+</html>
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 20b4354..029f6df 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -5,6 +5,8 @@
"encoding/base64"
"encoding/json"
"fmt"
+ htemplate "html/template"
+ "io"
"log"
"net/http"
"path"
@@ -23,19 +25,37 @@
//go:embed create-env.html
var createEnvFormHtml string
-type EnvServer struct {
- port int
- ss *soft.Client
- repo installer.RepoIO
- nsCreator installer.NamespaceCreator
+//go:embed env-created.html
+var envCreatedHtml string
+
+type Status string
+
+const (
+ StatusActive Status = "ACTIVE"
+ StatusAccepted Status = "ACCEPTED"
+)
+
+// TODO(giolekva): add CreatedAt and ValidUntil
+type invitation struct {
+ Token string `json:"token"`
+ Status Status `json:"status"`
}
-func NewEnvServer(port int, ss *soft.Client, repo installer.RepoIO, nsCreator installer.NamespaceCreator) *EnvServer {
+type EnvServer struct {
+ port int
+ ss *soft.Client
+ repo installer.RepoIO
+ nsCreator installer.NamespaceCreator
+ nameGenerator installer.NameGenerator
+}
+
+func NewEnvServer(port int, ss *soft.Client, repo installer.RepoIO, nsCreator installer.NamespaceCreator, nameGenerator installer.NameGenerator) *EnvServer {
return &EnvServer{
port,
ss,
repo,
nsCreator,
+ nameGenerator,
}
}
@@ -44,21 +64,78 @@
r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
r.Path("/env").Methods("GET").HandlerFunc(s.createEnvForm)
r.Path("/env").Methods("POST").HandlerFunc(s.createEnv)
+ r.Path("/create-invitation").Methods("GET").HandlerFunc(s.createInvitation)
http.Handle("/", r)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
}
func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
- log.Printf("asdasd\n")
if _, err := w.Write([]byte(createEnvFormHtml)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
+func (s *EnvServer) createInvitation(w http.ResponseWriter, r *http.Request) {
+ invitations, err := s.readInvitations()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ token, err := installer.NewFixedLengthRandomNameGenerator(100).Generate() // TODO(giolekva): use cryptographic tokens
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+
+ }
+ invitations = append(invitations, invitation{token, StatusActive})
+ if err := s.writeInvitations(invitations); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if _, err := w.Write([]byte("OK")); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}
+
type createEnvReq struct {
- Name string `json:"name"`
+ Name string
ContactEmail string `json:"contactEmail"`
Domain string `json:"domain"`
+ SecretToken string `json:"secretToken"`
+}
+
+func (s *EnvServer) readInvitations() ([]invitation, error) {
+ r, err := s.repo.Reader("invitations")
+ if err != nil {
+ return nil, err
+ }
+ defer r.Close()
+ dec := json.NewDecoder(r)
+ invitations := make([]invitation, 0)
+ for {
+ var i invitation
+ if err := dec.Decode(&i); err == io.EOF {
+ break
+ }
+ invitations = append(invitations, i)
+ }
+ return invitations, nil
+}
+
+func (s *EnvServer) writeInvitations(invitations []invitation) error {
+ w, err := s.repo.Writer("invitations")
+ if err != nil {
+ return err
+ }
+ defer w.Close()
+ enc := json.NewEncoder(w)
+ for _, i := range invitations {
+ if err := enc.Encode(i); err != nil {
+ return err
+ }
+ }
+ return s.repo.CommitAndPush("Generated new invitation")
}
func (s *EnvServer) createEnv(w http.ResponseWriter, r *http.Request) {
@@ -68,7 +145,7 @@
if err = r.ParseForm(); err != nil {
return err
}
- if req.Name, err = getFormValue(r.PostForm, "name"); err != nil {
+ if req.SecretToken, err = getFormValue(r.PostForm, "secret-token"); err != nil {
return err
}
if req.Domain, err = getFormValue(r.PostForm, "domain"); err != nil {
@@ -84,6 +161,33 @@
return
}
}
+ invitations, err := s.readInvitations()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ found := false
+ for _, i := range invitations {
+ if i.Token == req.SecretToken && i.Status == StatusActive {
+ i.Status = StatusAccepted
+ found = true
+ break
+ }
+ }
+ if !found {
+ http.Error(w, "Invalid invitation", http.StatusNotFound)
+ return
+ }
+ if err := s.writeInvitations(invitations); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if name, err := s.nameGenerator.Generate(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ } else {
+ req.Name = name
+ }
fluxUserName := fmt.Sprintf("flux-%s", req.Name)
keys, err := installer.NewSSHKeyPair(fluxUserName)
if err != nil {
@@ -143,7 +247,14 @@
return
}
}
- if _, err := w.Write([]byte("OK")); err != nil {
+ tmpl, err := htemplate.New("response").Parse(envCreatedHtml)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if err := tmpl.Execute(w, map[string]any{
+ "Domain": req.Domain,
+ }); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}