apps: app repository
diff --git a/apps/app-repository/Dockerfile b/apps/app-repository/Dockerfile
new file mode 100644
index 0000000..d546bbd
--- /dev/null
+++ b/apps/app-repository/Dockerfile
@@ -0,0 +1,8 @@
+FROM alpine:latest
+
+ARG TARGETARCH
+
+COPY apps /pcloud/apps
+
+COPY server_${TARGETARCH} /usr/bin/app-repository
+RUN chmod +x /usr/bin/app-repository
diff --git a/apps/app-repository/Makefile b/apps/app-repository/Makefile
new file mode 100644
index 0000000..74cecbc
--- /dev/null
+++ b/apps/app-repository/Makefile
@@ -0,0 +1,30 @@
+clean:
+ rm -f server_*
+
+build_arm64: export CGO_ENABLED=0
+build_arm64: export GO111MODULE=on
+build_arm64: export GOOS=linux
+build_arm64: export GOARCH=arm64
+build_arm64:
+ go build -o server_arm64 cmd/*.go
+
+build_amd64: export CGO_ENABLED=0
+build_amd64: export GO111MODULE=on
+build_amd64: export GOOS=linux
+build_amd64: export GOARCH=amd64
+build_amd64:
+ go build -o server_amd64 cmd/*.go
+
+push_arm64: clean build_arm64
+ podman build --platform linux/arm64 --tag=giolekva/app-repository:arm64 .
+ podman push giolekva/app-repository:arm64
+
+push_amd64: clean build_amd64
+ podman build --platform linux/amd64 --tag=giolekva/app-repository:amd64 .
+ podman push giolekva/app-repository:amd64
+
+
+push: push_arm64 push_amd64
+ podman manifest create giolekva/app-repository:latest giolekva/app-repository:arm64 giolekva/app-repository:amd64
+ podman manifest push giolekva/app-repository:latest docker://docker.io/giolekva/app-repository:latest
+ podman manifest rm giolekva/app-repository:latest
diff --git a/apps/app-repository/app.go b/apps/app-repository/app.go
new file mode 100644
index 0000000..c59ef2b
--- /dev/null
+++ b/apps/app-repository/app.go
@@ -0,0 +1,78 @@
+package apprepo
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "sigs.k8s.io/yaml"
+)
+
+type App interface {
+ Name() string
+ Version() string
+ Reader() (io.ReadCloser, error)
+}
+
+type Server struct {
+ schemeWithHost string
+ port int
+ apps []App
+}
+
+func NewServer(schemeWithHost string, port int, apps []App) *Server {
+ return &Server{schemeWithHost, port, apps}
+}
+
+func (s *Server) Start() error {
+ r := mux.NewRouter()
+ r.Path("/").Methods("GET").HandlerFunc(s.allApps)
+ r.Path("/app/{name}/{version}.tar.gz").Methods("GET").HandlerFunc(s.app)
+ http.Handle("/", r)
+ return http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil)
+}
+
+func (s *Server) allApps(w http.ResponseWriter, r *http.Request) {
+ entries := make(map[string][]map[string]any)
+ for _, a := range s.apps {
+ e, ok := entries[a.Name()]
+ if !ok {
+ e = make([]map[string]any, 0)
+ }
+ e = append(e, map[string]any{
+ "version": a.Version(),
+ "urls": []string{fmt.Sprintf("%s/%s/%s.tar.gz", s.schemeWithHost, a.Name(), a.Version())},
+ })
+ entries[a.Name()] = e
+ }
+ resp := map[string]any{
+ "apiVersion": "v1",
+ "entries": entries,
+ }
+ b, err := yaml.Marshal(resp)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Write(b)
+}
+
+func (s *Server) app(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ name := vars["name"]
+ version := vars["version"]
+ for _, a := range s.apps {
+ if a.Name() == name && a.Version() == version {
+ r, err := a.Reader()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ defer r.Close()
+ io.Copy(w, r)
+ return
+ }
+ }
+ http.Error(w, "Not found", http.StatusNotFound)
+}
diff --git a/apps/app-repository/apps/pihole-0.0.1.tar.gz b/apps/app-repository/apps/pihole-0.0.1.tar.gz
new file mode 100644
index 0000000..57a1957
--- /dev/null
+++ b/apps/app-repository/apps/pihole-0.0.1.tar.gz
Binary files differ
diff --git a/apps/app-repository/apps/rpuppy-0.0.1.tar.gz b/apps/app-repository/apps/rpuppy-0.0.1.tar.gz
new file mode 100644
index 0000000..e00d21f
--- /dev/null
+++ b/apps/app-repository/apps/rpuppy-0.0.1.tar.gz
Binary files differ
diff --git a/apps/app-repository/apps/soft-serve-0.0.1.tar.gz b/apps/app-repository/apps/soft-serve-0.0.1.tar.gz
new file mode 100644
index 0000000..0b238f1
--- /dev/null
+++ b/apps/app-repository/apps/soft-serve-0.0.1.tar.gz
Binary files differ
diff --git a/apps/app-repository/apps/vaultwarden-0.0.1.tar.gz b/apps/app-repository/apps/vaultwarden-0.0.1.tar.gz
new file mode 100644
index 0000000..783e6b2
--- /dev/null
+++ b/apps/app-repository/apps/vaultwarden-0.0.1.tar.gz
Binary files differ
diff --git a/apps/app-repository/cmd/main.go b/apps/app-repository/cmd/main.go
new file mode 100644
index 0000000..f9a5f95
--- /dev/null
+++ b/apps/app-repository/cmd/main.go
@@ -0,0 +1,24 @@
+package main
+
+import (
+ "flag"
+ "log"
+ "os"
+
+ "github.com/giolekva/pcloud/apps/apprepo"
+)
+
+var port = flag.Int("port", 8080, "Port to listen on")
+var appsDir = flag.String("apps-dir", "./apps", "Directory listing application archives")
+var schemeWithHost = flag.String("scheme-with-host", "", "http://localhost:8080")
+
+func main() {
+ flag.Parse()
+ l := apprepo.NewFSLoader(os.DirFS(*appsDir))
+ apps, err := l.Load()
+ if err != nil {
+ log.Fatal(err)
+ }
+ s := apprepo.NewServer(*schemeWithHost, *port, apps)
+ log.Fatal(s.Start())
+}
diff --git a/apps/app-repository/go.mod b/apps/app-repository/go.mod
new file mode 100644
index 0000000..894321a
--- /dev/null
+++ b/apps/app-repository/go.mod
@@ -0,0 +1,9 @@
+module github.com/giolekva/pcloud/apps/apprepo
+
+go 1.18
+
+require (
+ github.com/gorilla/mux v1.8.1
+ golang.org/x/mod v0.14.0
+ sigs.k8s.io/yaml v1.4.0
+)
diff --git a/apps/app-repository/go.sum b/apps/app-repository/go.sum
new file mode 100644
index 0000000..a02ab5b
--- /dev/null
+++ b/apps/app-repository/go.sum
@@ -0,0 +1,10 @@
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/apps/app-repository/loader.go b/apps/app-repository/loader.go
new file mode 100644
index 0000000..0da3bbd
--- /dev/null
+++ b/apps/app-repository/loader.go
@@ -0,0 +1,64 @@
+package apprepo
+
+import (
+ "io"
+ "io/fs"
+ "log"
+ "strings"
+
+ "golang.org/x/mod/semver"
+)
+
+type Loader interface {
+ Load() ([]App, error)
+}
+
+type fsApp struct {
+ name string
+ version string
+ fs fs.FS
+ path string
+}
+
+func (a *fsApp) Name() string {
+ return a.name
+}
+
+func (a *fsApp) Version() string {
+ return a.version
+}
+
+func (a *fsApp) Reader() (io.ReadCloser, error) {
+ return a.fs.Open(a.path)
+}
+
+type fsLoader struct {
+ fs fs.FS
+}
+
+func NewFSLoader(fs fs.FS) Loader {
+ return &fsLoader{fs}
+}
+
+func (l *fsLoader) Load() ([]App, error) {
+ entries, err := fs.ReadDir(l.fs, ".")
+ if err != nil {
+ return nil, err
+ }
+ apps := make([]App, 0)
+ for _, e := range entries {
+ log.Println(e.Name())
+ if !e.IsDir() && strings.HasSuffix(e.Name(), ".tar.gz") {
+ items := strings.Split(strings.TrimSuffix(e.Name(), ".tar.gz"), "-")
+ if len(items) <= 1 {
+ continue
+ }
+ version := items[len(items)-1]
+ if semver.IsValid(version) || semver.IsValid("v"+version) {
+ name := strings.Join(items[:len(items)-1], "-")
+ apps = append(apps, &fsApp{name, strings.TrimPrefix(version, "v"), l.fs, e.Name()})
+ }
+ }
+ }
+ return apps, nil
+}
diff --git a/apps/app-repository/server b/apps/app-repository/server
new file mode 100755
index 0000000..e454167
--- /dev/null
+++ b/apps/app-repository/server
Binary files differ
diff --git a/charts/app-repository/.helmignore b/charts/app-repository/.helmignore
new file mode 100644
index 0000000..0e8a0eb
--- /dev/null
+++ b/charts/app-repository/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/charts/app-repository/Chart.yaml b/charts/app-repository/Chart.yaml
new file mode 100644
index 0000000..63ac303
--- /dev/null
+++ b/charts/app-repository/Chart.yaml
@@ -0,0 +1,6 @@
+apiVersion: v2
+name: app-repository
+description: A Helm chart for PCloud App Repository
+type: application
+version: 0.0.1
+appVersion: "0.0.1"
diff --git a/charts/app-repository/templates/install.yaml b/charts/app-repository/templates/install.yaml
new file mode 100644
index 0000000..9d592bb
--- /dev/null
+++ b/charts/app-repository/templates/install.yaml
@@ -0,0 +1,73 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: app-repository
+ namespace: {{ .Release.Namespace }}
+spec:
+ type: ClusterIP
+ selector:
+ app: app-repository
+ ports:
+ - name: http
+ port: 80
+ targetPort: http
+ protocol: TCP
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress
+ namespace: {{ .Release.Namespace }}
+ {{- if .Values.certificateIssuer }}
+ annotations:
+ acme.cert-manager.io/http01-edit-in-place: "true"
+ cert-manager.io/cluster-issuer: {{ .Values.certificateIssuer }}
+ {{- end }}
+spec:
+ ingressClassName: {{ .Values.ingressClassName }}
+ {{- if .Values.certificateIssuer }}
+ tls:
+ - hosts:
+ - {{ .Values.domain }}
+ secretName: cert-app-repository
+ {{- end }}
+ rules:
+ - host: {{ .Values.domain }}
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: app-repository
+ port:
+ name: http
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: app-repository
+ namespace: {{ .Release.Namespace }}
+spec:
+ selector:
+ matchLabels:
+ app: app-repository
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ app: app-repository
+ spec:
+ containers:
+ - name: app-repository
+ image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: 8080
+ protocol: TCP
+ command:
+ - app-repository
+ - --port=8080
+ - --apps-dir={{ .Values.appsDir }}
+ - --scheme-with-host=https://{{ .Values.domain }}
diff --git a/charts/app-repository/values.yaml b/charts/app-repository/values.yaml
new file mode 100644
index 0000000..1439898
--- /dev/null
+++ b/charts/app-repository/values.yaml
@@ -0,0 +1,8 @@
+image:
+ repository: giolekva/app-repository
+ tag: latest
+ pullPolicy: Always
+domain: example.com
+appsDir: /pcloud/apps
+certificateIssuer: ""
+ingressClassName: ""