appmanager: fetch app configs from app-repository
diff --git a/core/installer/Makefile b/core/installer/Makefile
index ec56861..1762a41 100644
--- a/core/installer/Makefile
+++ b/core/installer/Makefile
@@ -21,7 +21,7 @@
./pcloud --kubeconfig=../../priv/kubeconfig install --ssh-key=/Users/lekva/.ssh/id_rsa --app=rpuppy --repo-addr=ssh://localhost:2222/lekva
appmanager:
- ./pcloud --kubeconfig=../../priv/kubeconfig appmanager --ssh-key=/Users/lekva/.ssh/id_rsa --repo-addr=ssh://192.168.0.211/qwe --port=9090
+ ./pcloud --kubeconfig=../../priv/kubeconfig appmanager --ssh-key=/Users/lekva/.ssh/id_rsa --repo-addr=ssh://192.168.100.210:22/rkcr --port=9090 --app-repo-addr=https://apprepo.dodo.cloud
welc:
./pcloud --kubeconfig=../../priv/kubeconfig welcome --ssh-key=/Users/lekva/.ssh/id_rsa --repo-addr=ssh://192.168.0.211/lekva --port=9090
diff --git a/core/installer/app.go b/core/installer/app.go
index 902e686..de294cb 100644
--- a/core/installer/app.go
+++ b/core/installer/app.go
@@ -1,15 +1,21 @@
package installer
import (
+ "archive/tar"
+ "compress/gzip"
"embed"
"encoding/json"
"fmt"
htemplate "html/template"
+ "io"
"log"
+ "net/http"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
+ "github.com/go-git/go-billy/v5"
+ "sigs.k8s.io/yaml"
)
//go:embed values-tmpl
@@ -19,6 +25,14 @@
Nam() string
}
+type appConfig struct {
+ Name string `json:"name"`
+ Version string `json:"version"`
+ Description string `json:"description"`
+ Namespaces []string `json:"namespaces"`
+ Icon htemplate.HTML `json:"icon"`
+}
+
type App struct {
Name string
Namespaces []string
@@ -58,8 +72,8 @@
apps []A
}
-func NewInMemoryAppRepository[A Named](apps []A) AppRepository[A] {
- return &InMemoryAppRepository[A]{
+func NewInMemoryAppRepository[A Named](apps []A) InMemoryAppRepository[A] {
+ return InMemoryAppRepository[A]{
apps,
}
}
@@ -559,3 +573,191 @@
tmpls.Lookup("headscale-controller.md"),
}
}
+
+type httpAppRepository struct {
+ apps []StoreApp
+}
+
+type appVersion struct {
+ Version string `json:"version"`
+ Urls []string `json:"urls"`
+}
+
+type allAppsResp struct {
+ ApiVersion string `json:"apiVersion"`
+ Entries map[string][]appVersion `json:"entries"`
+}
+
+func FetchAppsFromHTTPRepository(addr string, fs billy.Filesystem) error {
+ resp, err := http.Get(addr)
+ if err != nil {
+ return err
+ }
+ b, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ var apps allAppsResp
+ if err := yaml.Unmarshal(b, &apps); err != nil {
+ return err
+ }
+ for name, conf := range apps.Entries {
+ for _, version := range conf {
+ resp, err := http.Get(version.Urls[0])
+ if err != nil {
+ return err
+ }
+ nameVersion := fmt.Sprintf("%s-%s", name, version.Version)
+ if err := fs.MkdirAll(nameVersion, 0700); err != nil {
+ return err
+ }
+ sub, err := fs.Chroot(nameVersion)
+ if err != nil {
+ return err
+ }
+ if err := extractApp(resp.Body, sub); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func extractApp(archive io.Reader, fs billy.Filesystem) error {
+ uncompressed, err := gzip.NewReader(archive)
+ if err != nil {
+ return err
+ }
+ tarReader := tar.NewReader(uncompressed)
+ for true {
+ header, err := tarReader.Next()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return err
+ }
+ switch header.Typeflag {
+ case tar.TypeDir:
+ if err := fs.MkdirAll(header.Name, 0755); err != nil {
+ return err
+ }
+ case tar.TypeReg:
+ out, err := fs.Create(header.Name)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+ if _, err := io.Copy(out, tarReader); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("Uknown type: %s", header.Name)
+ }
+ }
+ return nil
+}
+
+type fsAppRepository struct {
+ InMemoryAppRepository[StoreApp]
+ fs billy.Filesystem
+}
+
+func NewFSAppRepository(fs billy.Filesystem) (AppRepository[StoreApp], error) {
+ all, err := fs.ReadDir(".")
+ if err != nil {
+ return nil, err
+ }
+ apps := make([]StoreApp, 0)
+ for _, e := range all {
+ if !e.IsDir() {
+ continue
+ }
+ appFS, err := fs.Chroot(e.Name())
+ if err != nil {
+ return nil, err
+ }
+ app, err := loadApp(appFS)
+ if err != nil {
+ log.Printf("Ignoring directory %s: %s", e.Name(), err)
+ continue
+ }
+ apps = append(apps, app)
+ }
+ return &fsAppRepository{
+ NewInMemoryAppRepository[StoreApp](apps),
+ fs,
+ }, nil
+}
+
+func loadApp(fs billy.Filesystem) (StoreApp, error) {
+ cfg, err := fs.Open("Chart.yaml")
+ if err != nil {
+ return StoreApp{}, err
+ }
+ defer cfg.Close()
+ b, err := io.ReadAll(cfg)
+ if err != nil {
+ return StoreApp{}, err
+ }
+ var appCfg appConfig
+ if err := yaml.Unmarshal(b, &appCfg); err != nil {
+ return StoreApp{}, err
+ }
+ rb, err := fs.Open("README.md")
+ if err != nil {
+ return StoreApp{}, err
+ }
+ defer rb.Close()
+ readme, err := io.ReadAll(rb)
+ if err != nil {
+ return StoreApp{}, err
+ }
+ readmeTmpl, err := template.New("README.md").Parse(string(readme))
+ if err != nil {
+ return StoreApp{}, err
+ }
+ sb, err := fs.Open("schema.json")
+ if err != nil {
+ return StoreApp{}, err
+ }
+ defer sb.Close()
+ schema, err := io.ReadAll(sb)
+ if err != nil {
+ return StoreApp{}, err
+ }
+ tFiles, err := fs.ReadDir("templates")
+ if err != nil {
+ return StoreApp{}, err
+ }
+ tmpls := make([]*template.Template, 0)
+ for _, t := range tFiles {
+ if !strings.HasSuffix(t.Name(), ".yaml") {
+ continue
+ }
+ inp, err := fs.Open(fs.Join("templates", t.Name()))
+ if err != nil {
+ return StoreApp{}, err
+ }
+ b, err := io.ReadAll(inp)
+ if err != nil {
+ return StoreApp{}, err
+ }
+ tmpl, err := template.New(t.Name()).Parse(string(b))
+ if err != nil {
+ return StoreApp{}, err
+ }
+ tmpls = append(tmpls, tmpl)
+ }
+ return StoreApp{
+ App: App{
+ Name: appCfg.Name,
+ Readme: readmeTmpl,
+ Schema: string(schema),
+ Namespaces: appCfg.Namespaces,
+ Templates: tmpls,
+ },
+ ShortDescription: appCfg.Description,
+ Icon: appCfg.Icon,
+ }, nil
+}
diff --git a/core/installer/cmd/app_manager.go b/core/installer/cmd/app_manager.go
index 769ea5a..cfb69a4 100644
--- a/core/installer/cmd/app_manager.go
+++ b/core/installer/cmd/app_manager.go
@@ -1,21 +1,24 @@
package main
import (
+ "log"
"os"
- "github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"github.com/giolekva/pcloud/core/installer"
"github.com/giolekva/pcloud/core/installer/soft"
"github.com/giolekva/pcloud/core/installer/welcome"
+
+ "github.com/go-git/go-billy/v5/memfs"
+ "github.com/spf13/cobra"
)
var appManagerFlags struct {
- sshKey string
- repoAddr string
- port int
- webAppAddr string
+ sshKey string
+ repoAddr string
+ port int
+ appRepoAddr string
}
func appManagerCmd() *cobra.Command {
@@ -42,8 +45,8 @@
"",
)
cmd.Flags().StringVar(
- &appManagerFlags.webAppAddr,
- "web-app-addr",
+ &appManagerFlags.appRepoAddr,
+ "app-repo-addr",
"",
"",
)
@@ -67,6 +70,7 @@
if err != nil {
return err
}
+ log.Println("Cloned repository")
kube, err := newNSCreator()
if err != nil {
return err
@@ -78,13 +82,25 @@
if err != nil {
return err
}
- r := installer.NewInMemoryAppRepository[installer.StoreApp](installer.CreateStoreApps())
+ log.Println("Creating repository")
+ var r installer.AppRepository[installer.StoreApp]
+ if appManagerFlags.appRepoAddr != "" {
+ fs := memfs.New()
+ err = installer.FetchAppsFromHTTPRepository(appManagerFlags.appRepoAddr, fs)
+ if err != nil {
+ return err
+ }
+ r, err = installer.NewFSAppRepository(fs)
+ if err != nil {
+ return err
+ }
+ } else {
+ r = installer.NewInMemoryAppRepository[installer.StoreApp](installer.CreateStoreApps())
+ }
s := welcome.NewAppManagerServer(
appManagerFlags.port,
- appManagerFlags.webAppAddr,
m,
r,
)
- s.Start()
- return nil
+ return s.Start()
}
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index 242faed..033e1db 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -28,27 +28,24 @@
var appHtmlTmpl string
type AppManagerServer struct {
- port int
- webAppAddr string
- m *installer.AppManager
- r installer.AppRepository[installer.StoreApp]
+ port int
+ m *installer.AppManager
+ r installer.AppRepository[installer.StoreApp]
}
func NewAppManagerServer(
port int,
- webAppAddr string,
m *installer.AppManager,
r installer.AppRepository[installer.StoreApp],
) *AppManagerServer {
return &AppManagerServer{
port,
- webAppAddr,
m,
r,
}
}
-func (s *AppManagerServer) Start() {
+func (s *AppManagerServer) Start() error {
e := echo.New()
e.StaticFS("/static", echo.MustSubFS(staticAssets, "static"))
e.GET("/api/app-repo", s.handleAppRepo)
@@ -62,7 +59,7 @@
e.GET("/app/:slug", s.handleAppUI)
e.GET("/instance/:slug", s.handleInstanceUI)
fmt.Printf("Starting HTTP server on port: %d\n", s.port)
- log.Fatal(e.Start(fmt.Sprintf(":%d", s.port)))
+ return e.Start(fmt.Sprintf(":%d", s.port))
}
type app struct {
@@ -223,18 +220,21 @@
if err := json.Unmarshal(contents, &values); err != nil {
return err
}
- fmt.Println(values)
+ log.Printf("Values: %+v\n", values)
a, err := s.r.Find(slug)
if err != nil {
return err
}
+ log.Printf("Found application: %s\n", slug)
config, err := s.m.Config()
if err != nil {
return err
}
+ log.Printf("Configuration: %+v\n", config)
nsGen := installer.NewPrefixGenerator(config.Values.NamespacePrefix)
suffixGen := installer.NewFixedLengthRandomSuffixGenerator(3)
if err := s.m.Install(a.App, nsGen, suffixGen, values); err != nil {
+ log.Printf("%s\n", err.Error())
return err
}
return c.String(http.StatusOK, "Installed")