blob: 5da26ef749cefa376b0f0763f2564d04466a3951 [file] [log] [blame]
package welcome
import (
"context"
"embed"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/Masterminds/sprig/v3"
"github.com/gorilla/mux"
"github.com/giolekva/pcloud/core/installer"
"github.com/giolekva/pcloud/core/installer/tasks"
)
//go:embed appmanager-tmpl/*
var appTmpls embed.FS
type AppManagerServer struct {
port int
m *installer.AppManager
r installer.AppRepository
reconciler tasks.Reconciler
h installer.HelmReleaseMonitor
tasks map[string]tasks.Task
tmpl tmplts
}
type tmplts struct {
index *template.Template
app *template.Template
}
func parseTemplatesAppManager(fs embed.FS) (tmplts, error) {
base, err := template.New("base.html").Funcs(template.FuncMap(sprig.FuncMap())).ParseFS(fs, "appmanager-tmpl/base.html")
if err != nil {
return tmplts{}, err
}
parse := func(path string) (*template.Template, error) {
if b, err := base.Clone(); err != nil {
return nil, err
} else {
return b.ParseFS(fs, path)
}
}
index, err := parse("appmanager-tmpl/index.html")
if err != nil {
return tmplts{}, err
}
app, err := parse("appmanager-tmpl/app.html")
if err != nil {
return tmplts{}, err
}
return tmplts{index, app}, nil
}
func NewAppManagerServer(
port int,
m *installer.AppManager,
r installer.AppRepository,
reconciler tasks.Reconciler,
h installer.HelmReleaseMonitor,
) (*AppManagerServer, error) {
tmpl, err := parseTemplatesAppManager(appTmpls)
if err != nil {
return nil, err
}
return &AppManagerServer{
port: port,
m: m,
r: r,
reconciler: reconciler,
h: h,
tasks: make(map[string]tasks.Task),
tmpl: tmpl,
}, nil
}
type cachingHandler struct {
h http.Handler
}
func (h cachingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=604800")
h.h.ServeHTTP(w, r)
}
func (s *AppManagerServer) Start() error {
r := mux.NewRouter()
r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
r.HandleFunc("/api/app-repo", s.handleAppRepo)
r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
r.HandleFunc("/api/app/{slug}", s.handleApp).Methods(http.MethodGet)
r.HandleFunc("/api/instance/{slug}", s.handleInstance).Methods(http.MethodGet)
r.HandleFunc("/api/instance/{slug}/update", s.handleAppUpdate).Methods(http.MethodPost)
r.HandleFunc("/api/instance/{slug}/remove", s.handleAppRemove).Methods(http.MethodPost)
r.HandleFunc("/", s.handleIndex).Methods(http.MethodGet)
r.HandleFunc("/not-installed", s.handleNotInstalledApps).Methods(http.MethodGet)
r.HandleFunc("/installed", s.handleInstalledApps).Methods(http.MethodGet)
r.HandleFunc("/app/{slug}", s.handleAppUI).Methods(http.MethodGet)
r.HandleFunc("/instance/{slug}", s.handleInstanceUI).Methods(http.MethodGet)
fmt.Printf("Starting HTTP server on port: %d\n", s.port)
return http.ListenAndServe(fmt.Sprintf(":%d", s.port), r)
}
type app struct {
Name string `json:"name"`
Icon template.HTML `json:"icon"`
ShortDescription string `json:"shortDescription"`
Slug string `json:"slug"`
Instances []installer.AppInstanceConfig `json:"instances,omitempty"`
}
func (s *AppManagerServer) handleAppRepo(w http.ResponseWriter, r *http.Request) {
all, err := s.r.GetAll()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := make([]app, len(all))
for i, a := range all {
resp[i] = app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *AppManagerServer) handleApp(w http.ResponseWriter, r *http.Request) {
slug, ok := mux.Vars(r)["slug"]
if !ok {
http.Error(w, "empty slug", http.StatusBadRequest)
return
}
a, err := s.r.Find(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
instances, err := s.m.FindAllAppInstances(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *AppManagerServer) handleInstance(w http.ResponseWriter, r *http.Request) {
slug, ok := mux.Vars(r)["slug"]
if !ok {
http.Error(w, "empty slug", http.StatusBadRequest)
return
}
instance, err := s.m.FindInstance(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a, err := s.r.Find(instance.AppId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := app{a.Name(), a.Icon(), a.Description(), a.Slug(), []installer.AppInstanceConfig{*instance}}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *AppManagerServer) handleAppInstall(w http.ResponseWriter, r *http.Request) {
slug, ok := mux.Vars(r)["slug"]
if !ok {
http.Error(w, "empty slug", http.StatusBadRequest)
return
}
contents, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var values map[string]any
if err := json.Unmarshal(contents, &values); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Values: %+v\n", values)
a, err := installer.FindEnvApp(s.r, slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Found application: %s\n", slug)
env, err := s.m.Config()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Configuration: %+v\n", env)
suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
suffix, err := suffixGen.Generate()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
instanceId := a.Slug() + suffix
appDir := fmt.Sprintf("/apps/%s", instanceId)
namespace := fmt.Sprintf("%s%s%s", env.NamespacePrefix, a.Namespace(), suffix)
rr, err := s.m.Install(a, instanceId, appDir, namespace, values)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
go s.reconciler.Reconcile(ctx)
if _, ok := s.tasks[instanceId]; ok {
panic("MUST NOT REACH!")
}
t := tasks.NewMonitorRelease(s.h, rr)
t.OnDone(func(err error) {
delete(s.tasks, instanceId)
})
s.tasks[instanceId] = t
go t.Start()
if _, err := fmt.Fprintf(w, "/instance/%s", instanceId); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *AppManagerServer) handleAppUpdate(w http.ResponseWriter, r *http.Request) {
slug, ok := mux.Vars(r)["slug"]
if !ok {
http.Error(w, "empty slug", http.StatusBadRequest)
return
}
appConfig, err := s.m.AppConfig(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
contents, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var values map[string]any
if err := json.Unmarshal(contents, &values); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a, err := installer.FindEnvApp(s.r, appConfig.AppId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if _, ok := s.tasks[slug]; ok {
http.Error(w, "Update already in progress", http.StatusBadRequest)
return
}
rr, err := s.m.Update(a, slug, values)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
go s.reconciler.Reconcile(ctx)
t := tasks.NewMonitorRelease(s.h, rr)
t.OnDone(func(err error) {
delete(s.tasks, slug)
})
s.tasks[slug] = t
go t.Start()
if _, err := fmt.Fprintf(w, "/instance/%s", slug); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *AppManagerServer) handleAppRemove(w http.ResponseWriter, r *http.Request) {
slug, ok := mux.Vars(r)["slug"]
if !ok {
http.Error(w, "empty slug", http.StatusBadRequest)
return
}
if err := s.m.Remove(slug); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ctx, _ := context.WithTimeout(context.Background(), 2*time.Minute)
go s.reconciler.Reconcile(ctx)
if _, err := fmt.Fprint(w, "/"); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type PageData struct {
Apps []app
CurrentPage string
}
func (s *AppManagerServer) handleIndex(w http.ResponseWriter, r *http.Request) {
all, err := s.r.GetAll()
if err != nil {
log.Printf("all apps: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := make([]app, 0)
for _, a := range all {
instances, err := s.m.FindAllAppInstances(a.Slug())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
}
data := PageData{
Apps: resp,
CurrentPage: "ALL",
}
if err := s.tmpl.index.Execute(w, data); err != nil {
log.Printf("executing template: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *AppManagerServer) handleNotInstalledApps(w http.ResponseWriter, r *http.Request) {
all, err := s.r.GetAll()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := make([]app, 0)
for _, a := range all {
instances, err := s.m.FindAllAppInstances(a.Slug())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(instances) == 0 {
resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), nil})
}
}
data := PageData{
Apps: resp,
CurrentPage: "NOT_INSTALLED",
}
if err := s.tmpl.index.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *AppManagerServer) handleInstalledApps(w http.ResponseWriter, r *http.Request) {
all, err := s.r.GetAll()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
resp := make([]app, 0)
for _, a := range all {
instances, err := s.m.FindAllAppInstances(a.Slug())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(instances) != 0 {
resp = append(resp, app{a.Name(), a.Icon(), a.Description(), a.Slug(), instances})
}
}
data := PageData{
Apps: resp,
CurrentPage: "INSTALLED",
}
if err := s.tmpl.index.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
type appPageData struct {
App installer.EnvApp
Instance *installer.AppInstanceConfig
Instances []installer.AppInstanceConfig
AvailableNetworks []installer.Network
Task tasks.Task
CurrentPage string
}
func (s *AppManagerServer) handleAppUI(w http.ResponseWriter, r *http.Request) {
global, err := s.m.Config()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
slug, ok := mux.Vars(r)["slug"]
if !ok {
http.Error(w, "empty slug", http.StatusBadRequest)
return
}
a, err := installer.FindEnvApp(s.r, slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
instances, err := s.m.FindAllAppInstances(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
data := appPageData{
App: a,
Instances: instances,
AvailableNetworks: installer.CreateNetworks(global),
CurrentPage: a.Name(),
}
if err := s.tmpl.app.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func (s *AppManagerServer) handleInstanceUI(w http.ResponseWriter, r *http.Request) {
global, err := s.m.Config()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
slug, ok := mux.Vars(r)["slug"]
if !ok {
http.Error(w, "empty slug", http.StatusBadRequest)
return
}
instance, err := s.m.FindInstance(slug)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
a, err := installer.FindEnvApp(s.r, instance.AppId)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
instances, err := s.m.FindAllAppInstances(a.Slug())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
t := s.tasks[slug]
data := appPageData{
App: a,
Instance: instance,
Instances: instances,
AvailableNetworks: installer.CreateNetworks(global),
Task: t,
CurrentPage: instance.Id,
}
if err := s.tmpl.app.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}