apps: app repository
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)
+}