Luigit
repositories / bugabinga.net

bugabinga.net

personal infrastructure for bugabinga!

owned by admin

services/luci/internal/buildctx/buildctx_test.go

Raw
package buildctx

import (
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"testing"
)

func TestStageCopiesOnlySafeUnignoredContext(t *testing.T) {
	checkout := t.TempDir()
	write(t, filepath.Join(checkout, ".ci", "Containerfile"), "FROM alpine\nCOPY . /src\n")
	write(t, filepath.Join(checkout, ".ci", "image", ".containerignore"), "ignored\n*.secret\n")
	write(t, filepath.Join(checkout, ".ci", "image", "keep"), "ok")
	write(t, filepath.Join(checkout, ".ci", "image", "ignored", "file"), "no")
	write(t, filepath.Join(checkout, ".ci", "image", "token.secret"), "no")
	containerfile, context, err := Stage(checkout, ".ci/Containerfile", ".ci/image", filepath.Join(t.TempDir(), "stage"), 100, 100)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(containerfile); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(context, "keep")); err != nil {
		t.Fatal(err)
	}
	for _, name := range []string{"ignored", "token.secret"} {
		if _, err := os.Stat(filepath.Join(context, name)); !os.IsNotExist(err) {
			t.Fatalf("ignored %s exists: %v", name, err)
		}
	}
}

func TestStageRejectsEscapesMetadataSpecialFilesAndExternalLinks(t *testing.T) {
	for name, setup := range map[string]func(t *testing.T, root string){
		"traversal": func(t *testing.T, root string) { write(t, filepath.Join(root, ".ci", "Containerfile"), "FROM scratch") },
		"containerfile-git": func(t *testing.T, root string) {
			write(t, filepath.Join(root, ".ci", "image", ".git", "Containerfile"), "FROM scratch")
		},
		"external-link": func(t *testing.T, root string) {
			if err := os.Symlink("/etc/passwd", filepath.Join(root, ".ci", "image", "link")); err != nil {
				t.Fatal(err)
			}
		},
		"special": func(t *testing.T, root string) {
			if err := syscall.Mkfifo(filepath.Join(root, ".ci", "image", "pipe"), 0o600); err != nil {
				t.Fatal(err)
			}
		},
	} {
		t.Run(name, func(t *testing.T) {
			root := t.TempDir()
			write(t, filepath.Join(root, ".ci", "Containerfile"), "FROM scratch")
			if err := os.MkdirAll(filepath.Join(root, ".ci", "image"), 0o755); err != nil {
				t.Fatal(err)
			}
			setup(t, root)
			containerfile := ".ci/Containerfile"
			if name == "traversal" {
				containerfile = "../Containerfile"
			}
			if name == "containerfile-git" {
				containerfile = ".ci/image/.git/Containerfile"
			}
			_, _, err := Stage(root, containerfile, ".ci/image", filepath.Join(t.TempDir(), "stage"), 100, 100)
			if err == nil {
				t.Fatal("unsafe input accepted")
			}
		})
	}
}

func TestStageBoundsBytesAndAllowsInternalLink(t *testing.T) {
	root := t.TempDir()
	write(t, filepath.Join(root, ".ci", "Containerfile"), "FROM scratch")
	write(t, filepath.Join(root, ".ci", "image", "file"), strings.Repeat("x", 40))
	if err := os.Symlink("file", filepath.Join(root, ".ci", "image", "link")); err != nil {
		t.Fatal(err)
	}
	_, context, err := Stage(root, ".ci/Containerfile", ".ci/image", filepath.Join(t.TempDir(), "stage"), 20, 100)
	if err == nil || !strings.Contains(err.Error(), "byte limit") || context != "" {
		t.Fatalf("err=%v context=%q", err, context)
	}
	_, context, err = Stage(root, ".ci/Containerfile", ".ci/image", filepath.Join(t.TempDir(), "stage-ok"), 100, 100)
	if err != nil {
		t.Fatal(err)
	}
	if target, err := os.Readlink(filepath.Join(context, "link")); err != nil || target != "file" {
		t.Fatalf("link=%q err=%v", target, err)
	}
}

func TestStageAllowsRootContextExcludesGitAndHonorsNegation(t *testing.T) {
	root := t.TempDir()
	write(t, filepath.Join(root, "Containerfile"), "FROM scratch")
	write(t, filepath.Join(root, ".git", "config"), "private")
	write(t, filepath.Join(root, ".containerignore"), "ignored/\n!ignored/keep\n")
	write(t, filepath.Join(root, "ignored", "drop"), "no")
	write(t, filepath.Join(root, "ignored", "keep"), "yes")
	_, context, err := Stage(root, "Containerfile", ".", filepath.Join(t.TempDir(), "stage"), 1000, 100)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(context, ".git")); !os.IsNotExist(err) {
		t.Fatalf("Git metadata staged: %v", err)
	}
	if _, err := os.Stat(filepath.Join(context, "ignored", "keep")); err != nil {
		t.Fatalf("negated path missing: %v", err)
	}
	if _, err := os.Stat(filepath.Join(context, "ignored", "drop")); !os.IsNotExist(err) {
		t.Fatalf("ignored path staged: %v", err)
	}
}

func TestStageBoundsFilesAndRejectsIgnoreSymlink(t *testing.T) {
	root := t.TempDir()
	write(t, filepath.Join(root, "Containerfile"), "FROM scratch")
	write(t, filepath.Join(root, "one"), "1")
	write(t, filepath.Join(root, "two"), "2")
	if _, _, err := Stage(root, "Containerfile", ".", filepath.Join(t.TempDir(), "limited"), 100, 2); err == nil || !strings.Contains(err.Error(), "file limit") {
		t.Fatalf("file limit err=%v", err)
	}
	ignoreRoot := t.TempDir()
	write(t, filepath.Join(ignoreRoot, "Containerfile"), "FROM scratch")
	write(t, filepath.Join(ignoreRoot, "outside"), "x")
	if err := os.Symlink("outside", filepath.Join(ignoreRoot, ".containerignore")); err != nil {
		t.Fatal(err)
	}
	if _, _, err := Stage(ignoreRoot, "Containerfile", ".", filepath.Join(t.TempDir(), "ignore-link"), 100, 100); err == nil || !strings.Contains(err.Error(), "not a regular") {
		t.Fatalf("ignore symlink err=%v", err)
	}
}

func TestStageCountsContainerfileInByteLimit(t *testing.T) {
	root := t.TempDir()
	write(t, filepath.Join(root, ".ci", "Containerfile"), strings.Repeat("x", 60))
	write(t, filepath.Join(root, ".ci", "image", "file"), strings.Repeat("x", 50))
	_, _, err := Stage(root, ".ci/Containerfile", ".ci/image", filepath.Join(t.TempDir(), "stage"), 100, 100)
	if err == nil || !strings.Contains(err.Error(), "byte limit") {
		t.Fatalf("err=%v", err)
	}
}

func write(t *testing.T, path, value string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(path, []byte(value), 0o644); err != nil {
		t.Fatal(err)
	}
}