mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 07:41:27 +00:00
feat: build-on-node — git checkouts, remote image builds, image transport (#60)
Git-sourced deploys never materialized a checkout: sourcePath was build_path resolved against the server cwd, so builds consumed the backend's own directory. Remote placement could only run registry-pulled images. - internal/source: per-deployment checkout (init/fetch/checkout) at branch or commit; GIT_ASKPASS keeps tokens out of argv, .git/config, and the build context; dumb-transport and SHA-refusal fallbacks; build_path validated inside the checkout. - BuildManager.PackageContext: produces the same tar context Build ships to the daemon — generated Dockerfile at tar root, .git excluded. Fixes nixpacks passing the Dockerfile path as content. - Agent artifact endpoint: GET /api/agents/artifacts/:id streams build contexts and docker-save tarballs behind agent-token auth; hex-validated ids, per-deployment cleanup, boot-time purge. - Agent commands: build_image (docker build -, --build-arg values via process env, not argv) and load_image (docker load for containr-* rollback tags no registry serves). - Deployment flow: remote git builds package context → artifact → node docker-builds the deployment tag; remote rollbacks of local tags export via SaveImage → artifact → node docker-loads. Verified end-to-end: agent-pinned git service built on the node (build_image → create_container → deployed) and rollback shipped a docker-save tarball (load_image → deployed). Artifact auth returns 401/404 correctly; workspaces and blobs are removed after deploy. Authored By: TDvorak <[email protected]>
This commit is contained in:
1 parent
d57ac72aa5
commit
3cc84553f4
18 files changed
+1020
-51
No files matched your search
@@ -0,0 +1,152 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeCloneURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, want string
|
||||
err bool
|
||||
}{
|
||||
{"https://github.com/org/repo.git", "https://github.com/org/repo.git", false},
|
||||
{"[email protected]:org/repo.git", "https://github.com/org/repo.git", false},
|
||||
{"ssh://[email protected]/org/repo.git", "https://gitlab.example.com/org/repo.git", false},
|
||||
{"org/repo", "org/repo", false},
|
||||
{" https://gitlab.com/a/b.git ", "https://gitlab.com/a/b.git", false},
|
||||
{"", "", true},
|
||||
{"ftp://example.com/repo", "", true},
|
||||
{"weird string", "", true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, err := NormalizeCloneURL(c.in)
|
||||
if c.err {
|
||||
if err == nil {
|
||||
t.Errorf("%q: expected error, got %q", c.in, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("%q: unexpected error %v", c.in, err)
|
||||
continue
|
||||
}
|
||||
if got != c.want {
|
||||
t.Errorf("%q: got %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRemoteURL(t *testing.T) {
|
||||
if err := ValidateRemoteURL("https://github.com/o/r.git"); err != nil {
|
||||
t.Errorf("https should pass: %v", err)
|
||||
}
|
||||
if err := ValidateRemoteURL("file:///tmp/repo"); err == nil {
|
||||
t.Error("file:// should be rejected")
|
||||
}
|
||||
if err := ValidateRemoteURL("/tmp/repo"); err == nil {
|
||||
t.Error("local path should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// initRepo creates a small git repo with a branch and returns its path.
|
||||
func initRepo(t *testing.T) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
run := func(args ...string) {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@t",
|
||||
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@t")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("git %v: %s", args, out)
|
||||
}
|
||||
}
|
||||
run("init", "-q", "-b", "main")
|
||||
if err := os.WriteFile(filepath.Join(dir, "app.txt"), []byte("v1"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "sub", "inner.txt"), []byte("sub"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
run("add", ".")
|
||||
run("commit", "-q", "-m", "first")
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestCheckoutBranch(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not available")
|
||||
}
|
||||
repo := initRepo(t)
|
||||
dest := t.TempDir()
|
||||
dir, err := Checkout(context.Background(), dest, Spec{CloneURL: repo, Branch: "main"})
|
||||
if err != nil {
|
||||
t.Fatalf("checkout: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "app.txt")); err != nil {
|
||||
t.Errorf("expected app.txt in checkout: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutBuildPath(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not available")
|
||||
}
|
||||
repo := initRepo(t)
|
||||
dest := t.TempDir()
|
||||
dir, err := Checkout(context.Background(), dest, Spec{CloneURL: repo, Branch: "main", BuildPath: "sub"})
|
||||
if err != nil {
|
||||
t.Fatalf("checkout: %v", err)
|
||||
}
|
||||
if filepath.Base(dir) != "sub" {
|
||||
t.Errorf("expected sub build path, got %s", dir)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "inner.txt")); err != nil {
|
||||
t.Errorf("expected inner.txt under build path: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckoutCommit(t *testing.T) {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
t.Skip("git not available")
|
||||
}
|
||||
repo := initRepo(t)
|
||||
out, err := exec.Command("git", "-C", repo, "rev-parse", "HEAD").Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sha := string(out[:len(out)-1])
|
||||
dest := t.TempDir()
|
||||
dir, err := Checkout(context.Background(), dest, Spec{CloneURL: repo, Commit: sha, Branch: "main"})
|
||||
if err != nil {
|
||||
t.Fatalf("checkout commit: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "app.txt")); err != nil {
|
||||
t.Errorf("expected app.txt in pinned checkout: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveBuildPathEscapes(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
if _, err := ResolveBuildPath(root, "../../etc"); err == nil {
|
||||
t.Error("escape should be rejected")
|
||||
}
|
||||
if _, err := ResolveBuildPath(root, "/abs/path"); err == nil {
|
||||
t.Error("absolute path should be rejected")
|
||||
}
|
||||
if _, err := ResolveBuildPath(root, "missing"); err == nil {
|
||||
t.Error("missing dir should be rejected")
|
||||
}
|
||||
if got, err := ResolveBuildPath(root, "."); err != nil || got != root {
|
||||
t.Errorf("dot should resolve to root, got %q err=%v", got, err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user