AppManager: Add API endpoint to install dodo app

Refactors cue definitions.

Next steps:
* Needs some cleanup, namespace is hard coded ...
* Maybe merge with regular install API
* Support exposing ports across clusters

Change-Id: Ibfc3c3f742b61f2c5874012fe6c77b958eae81d9
diff --git a/apps/app-runner/Dockerfile.golang.1.24.0 b/apps/app-runner/Dockerfile.golang.1.24.0
new file mode 100644
index 0000000..692818b
--- /dev/null
+++ b/apps/app-runner/Dockerfile.golang.1.24.0
@@ -0,0 +1,5 @@
+FROM golang:1.24.0-alpine3.21
+
+ARG TARGETARCH
+
+COPY app-runner_${TARGETARCH} /usr/bin/app-runner
diff --git a/apps/app-runner/Makefile b/apps/app-runner/Makefile
index 9c76326..8f11f8b 100644
--- a/apps/app-runner/Makefile
+++ b/apps/app-runner/Makefile
@@ -1,6 +1,7 @@
 repo_name ?= giolekva
 podman ?= docker
 ifeq ($(podman), podman)
+manifest_dest_golang_1_24_0=docker://docker.io/$(repo_name)/app-runner:golang-1.24.0
 manifest_dest_golang_1_22_0=docker://docker.io/$(repo_name)/app-runner:golang-1.22.0
 manifest_dest_golang_1_20_0=docker://docker.io/$(repo_name)/app-runner:golang-1.20.0
 manifest_dest_hugo_latest=docker://docker.io/$(repo_name)/app-runner:hugo-latest
@@ -26,6 +27,21 @@
 build_amd64:
 	/usr/local/go/bin/go build -o app-runner_amd64 *.go
 
+# Golang 1.24.0
+
+push_golang_1_24_0_arm64: clean build_arm64
+	$(podman) build --platform linux/arm64 --tag=$(repo_name)/app-runner:golang-1.24.0-arm64 -f Dockerfile.golang.1.24.0 .
+	$(podman) push $(repo_name)/app-runner:golang-1.24.0-arm64
+
+push_golang_1_24_0_amd64: clean build_amd64
+	$(podman) build --platform linux/amd64 --tag=$(repo_name)/app-runner:golang-1.24.0-amd64 -f Dockerfile.golang.1.24.0 .
+	$(podman) push $(repo_name)/app-runner:golang-1.24.0-amd64
+
+push_golang_1_24_0: push_golang_1_24_0_arm64 push_golang_1_24_0_amd64
+	$(podman) manifest create $(repo_name)/app-runner:golang-1.24.0 $(repo_name)/app-runner:golang-1.24.0-arm64 $(repo_name)/app-runner:golang-1.24.0-amd64
+	$(podman) manifest push $(repo_name)/app-runner:golang-1.24.0 $(manifest_dest_golang_1_24_0)
+	$(podman) manifest rm $(repo_name)/app-runner:golang-1.24.0
+
 # Golang 1.22.0
 
 push_golang_1_22_0_arm64: clean build_arm64
diff --git a/apps/app-runner/main.go b/apps/app-runner/main.go
index 9296749..2f4f5a8 100644
--- a/apps/app-runner/main.go
+++ b/apps/app-runner/main.go
@@ -21,6 +21,7 @@
 var appId = flag.String("app-id", "", "Application ID")
 var repoAddr = flag.String("repo-addr", "", "Git repository address")
 var branch = flag.String("branch", "", "Name of the branch to process")
+var rootDir = flag.String("root-dir", "/", "Path to the app code")
 var sshKey = flag.String("ssh-key", "", "Private SSH key to access Git repository")
 var appDir = flag.String("app-dir", "", "Path to store application repository locally")
 var runCfg = flag.String("run-cfg", "", "Run configuration")
@@ -32,11 +33,19 @@
 	Env  []string `json:"env"`
 }
 
-func CloneRepositoryBranch(addr, branch string, signer ssh.Signer, path string) error {
+func CloneRepositoryBranch(addr, branch, rootDir string, signer ssh.Signer, path string) error {
 	ref := fmt.Sprintf("refs/heads/%s", branch)
-	c, err := git.Clone(memory.NewStorage(), osfs.New(path, osfs.WithBoundOS()), &git.CloneOptions{
-		URL: addr,
-		Auth: &gitssh.PublicKeys{
+	opts := &git.CloneOptions{
+		URL:             addr,
+		RemoteName:      "origin",
+		ReferenceName:   plumbing.ReferenceName(ref),
+		SingleBranch:    true,
+		Depth:           1,
+		InsecureSkipTLS: true,
+		Progress:        os.Stdout,
+	}
+	if signer != nil {
+		opts.Auth = &gitssh.PublicKeys{
 			User:   "git",
 			Signer: signer,
 			HostKeyCallbackHelper: gitssh.HostKeyCallbackHelper{
@@ -46,14 +55,9 @@
 					return nil
 				},
 			},
-		},
-		RemoteName:      "origin",
-		ReferenceName:   plumbing.ReferenceName(ref),
-		SingleBranch:    true,
-		Depth:           1,
-		InsecureSkipTLS: true,
-		Progress:        os.Stdout,
-	})
+		}
+	}
+	c, err := git.Clone(memory.NewStorage(), osfs.New(path, osfs.WithBoundOS()), opts)
 	if err != nil {
 		return err
 	}
@@ -61,18 +65,22 @@
 	if err != nil {
 		return err
 	}
-	sb, err := wt.Submodules()
-	if err != nil {
-		return err
+	if wt == nil {
+		panic(wt)
 	}
-	if err := sb.Init(); err != nil {
-		return err
-	}
-	if err := sb.Update(&git.SubmoduleUpdateOptions{
-		Depth: 1,
-	}); err != nil {
-		return err
-	}
+	// TODO(gio): This should probably be removed.
+	// sb, err := wt.Submodules()
+	// if err != nil {
+	// 	return err
+	// }
+	// if err := sb.Init(); err != nil {
+	// 	return err
+	// }
+	// if err := sb.Update(&git.SubmoduleUpdateOptions{
+	// 	Depth: 1,
+	// }); err != nil {
+	// 	return err
+	// }
 	return err
 }
 
@@ -82,15 +90,18 @@
 	if !ok {
 		panic("no SELF_IP")
 	}
-	key, err := os.ReadFile(*sshKey)
-	if err != nil {
-		panic(err)
+	var signer ssh.Signer
+	if *sshKey != "" {
+		key, err := os.ReadFile(*sshKey)
+		if err != nil {
+			panic(err)
+		}
+		signer, err = ssh.ParsePrivateKey(key)
+		if err != nil {
+			panic(err)
+		}
 	}
-	signer, err := ssh.ParsePrivateKey(key)
-	if err != nil {
-		panic(err)
-	}
-	if err := CloneRepositoryBranch(*repoAddr, *branch, signer, *appDir); err != nil {
+	if err := CloneRepositoryBranch(*repoAddr, *branch, *rootDir, signer, *appDir); err != nil {
 		panic(err)
 	}
 	r, err := os.Open(*runCfg)
@@ -102,7 +113,7 @@
 	if err := json.NewDecoder(r).Decode(&cmds); err != nil {
 		panic(err)
 	}
-	s := NewServer(*port, *appId, *repoAddr, *branch, signer, *appDir, cmds, self, *managerAddr)
+	s := NewServer(*port, *appId, *repoAddr, *branch, *rootDir, signer, *appDir, cmds, self, *managerAddr)
 	if err := s.Start(); err != nil {
 		log.Fatal(err)
 	}
diff --git a/apps/app-runner/server.go b/apps/app-runner/server.go
index 1779a76..ad8c7bc 100644
--- a/apps/app-runner/server.go
+++ b/apps/app-runner/server.go
@@ -8,6 +8,7 @@
 	"net/http"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"sync"
 	"syscall"
 	"time"
@@ -23,6 +24,7 @@
 	cmd         *exec.Cmd
 	repoAddr    string
 	branch      string
+	rootDir     string
 	signer      ssh.Signer
 	appDir      string
 	runCommands []Command
@@ -32,7 +34,7 @@
 	currDir     string
 }
 
-func NewServer(port int, appId string, repoAddr, branch string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
+func NewServer(port int, appId string, repoAddr, branch, rootDir string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
 	return &Server{
 		l:           &sync.Mutex{},
 		port:        port,
@@ -40,6 +42,7 @@
 		appId:       appId,
 		repoAddr:    repoAddr,
 		branch:      branch,
+		rootDir:     rootDir,
 		signer:      signer,
 		appDir:      appDir,
 		runCommands: runCommands,
@@ -94,7 +97,7 @@
 	if err != nil {
 		return err
 	}
-	if err := CloneRepositoryBranch(s.repoAddr, s.branch, s.signer, newDir); err != nil {
+	if err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir); err != nil {
 		return err
 	}
 	logM := io.MultiWriter(os.Stdout, s.logs)
@@ -102,7 +105,7 @@
 		args := []string{c.Bin}
 		args = append(args, c.Args...)
 		cmd := &exec.Cmd{
-			Dir:    newDir,
+			Dir:    filepath.Join(newDir, s.rootDir),
 			Path:   c.Bin,
 			Args:   args,
 			Env:    append(os.Environ(), c.Env...),