Files
dsh-web/internal/api/releases_test.go
T

319 lines
12 KiB
Go

package api
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"testing"
)
func testReleaseArchive(t *testing.T, m string, files map[string]string) []byte {
t.Helper()
var b bytes.Buffer
tw := tar.NewWriter(&b)
files["release.json"] = m
for name, data := range files {
if err := tw.WriteHeader(&tar.Header{Name: name, Size: int64(len(data)), Mode: 0644}); err != nil {
t.Fatal(err)
}
if _, err := tw.Write([]byte(data)); err != nil {
t.Fatal(err)
}
}
if err := tw.Close(); err != nil {
t.Fatal(err)
}
return b.Bytes()
}
func releaseFixture() releaseInfo {
return releaseInfo{releaseManifest: releaseManifest{SchemaVersion: 1, ReleaseID: "runtime-7.1", Version: "0.2.0-rc.3", DeploymentSchema: 7, Platforms: []string{imagePlatform}}, Repository: "https://git.misaka.ren/m1saka/dsh-web.git", Commit: strings.Repeat("a", 40)}
}
func TestReleaseManifestAndContext(t *testing.T) {
info := releaseFixture()
data, _ := json.Marshal(info.releaseManifest)
for _, tc := range []struct {
name, manifest string
files map[string]string
wantErr bool
}{
{"valid", string(data), map[string]string{"image/Dockerfile": "FROM scratch", "image/nested/helper": "complete", "image/.git/config": "secret", "image/.env": "secret"}, false},
{"malformed", "{", map[string]string{"image/Dockerfile": "FROM scratch"}, true},
{"unknown", strings.Replace(string(data), "schema_version", "unknown", 1), map[string]string{"image/Dockerfile": "FROM scratch"}, true},
{"incompatible", strings.Replace(string(data), "deployment_schema\":7", "deployment_schema\":8", 1), map[string]string{"image/Dockerfile": "FROM scratch"}, true},
{"missing Dockerfile", string(data), map[string]string{"image/helper": "x"}, true},
{"unsafe path", string(data), map[string]string{"image/Dockerfile": "FROM scratch", "image/../escape": "x"}, true},
} {
t.Run(tc.name, func(t *testing.T) {
_, r, err := parseReleaseArchive(info, testReleaseArchive(t, tc.manifest, tc.files))
if (err != nil) != tc.wantErr {
t.Fatalf("err=%v", err)
}
if err != nil {
return
}
tr := tar.NewReader(r)
seen := map[string]bool{}
for {
h, e := tr.Next()
if e == io.EOF {
break
}
if e != nil {
t.Fatal(e)
}
seen[h.Name] = true
}
if !seen["nested/helper"] || seen[".env"] || seen[".git/config"] {
t.Fatal(seen)
}
})
}
}
func TestReleaseQueueIdentityRecoveryAndFailure(t *testing.T) {
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) })
info := releaseFixture()
b, err := a.enqueueRelease("u", info)
if err != nil {
t.Fatal(err)
}
again, err := a.enqueueRelease("u", info)
if err != nil || again.ID != b.ID {
t.Fatalf("repeat %v %v", again, err)
}
info.Commit = strings.Repeat("b", 40)
info.ReleaseID = "runtime-7.2"
next, err := a.enqueueRelease("u", info)
if err != nil || next.ID == b.ID || next.Version != b.Version {
t.Fatalf("next %v %v", next, err)
}
buildQueue.Lock()
delete(buildQueue.initialized, a.s.DB)
err = a.ensureBuilds()
buildQueue.Unlock()
if err != nil {
t.Fatal(err)
}
var recovered buildRecord
if err = scanBuildSummary(a.s.DB.QueryRow("SELECT "+buildSummaryColumns+" FROM image_builds WHERE id=?", next.ID), &recovered); err != nil {
t.Fatal(err)
}
if recovered.Status != "queued" || recovered.SourceCommit != info.Commit {
t.Fatal(recovered)
}
if _, err = a.s.DB.Exec("UPDATE image_builds SET status='failed' WHERE id=?", next.ID); err != nil {
t.Fatal(err)
}
retry, err := a.enqueueRelease("u", info)
if err != nil || retry.ID == next.ID {
t.Fatalf("retry %v %v", retry, err)
}
}
func TestReleaseEndpointsAuthorizationFailureAndMismatch(t *testing.T) {
a, sid := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) })
if w := imageRequest(a.latestRelease, sid, "GET", ""); w.Code != 403 {
t.Fatal(w.Code)
}
if _, err := a.s.DB.Exec("UPDATE users SET admin=1 WHERE id='u'"); err != nil {
t.Fatal(err)
}
old := fetchRelease
t.Cleanup(func() { fetchRelease = old })
fetchRelease = func(context.Context, string, string) (releaseInfo, io.Reader, error) {
return releaseInfo{}, nil, errors.New("network failure")
}
if w := imageRequest(a.latestRelease, sid, "GET", ""); w.Code != 502 {
t.Fatalf("%d %s", w.Code, w.Body.String())
}
fetchRelease = func(context.Context, string, string) (releaseInfo, io.Reader, error) {
return releaseFixture(), nil, nil
}
if w := imageRequest(a.releaseBuild, sid, "POST", `{"commit":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}`); w.Code != 409 {
t.Fatalf("%d %s", w.Code, w.Body.String())
}
fetchRelease = func(context.Context, string, string) (releaseInfo, io.Reader, error) {
return releaseFixture(), nil, errReleaseIncompatible
}
if w := imageRequest(a.latestRelease, sid, "GET", ""); w.Code != 200 || !strings.Contains(w.Body.String(), `"compatible":false`) {
t.Fatalf("%d %s", w.Code, w.Body.String())
}
}
func TestReleaseGitEnvironmentAndFailure(t *testing.T) {
out, err := gitReleaseCommand(context.Background(), t.TempDir(), "--version")
if err != nil || !strings.HasPrefix(string(out), "git version") {
t.Fatalf("%q %v", out, err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if _, err = gitReleaseCommand(ctx, t.TempDir(), "--version"); err == nil {
t.Fatal("cancel ignored")
}
for _, repository := range []string{"http://example.test/repo", "https://user:[email protected]/repo", "https://example.test/repo?token=secret"} {
t.Setenv("DSH_RELEASE_REPOSITORY", repository)
if _, _, err = releaseSource(); err == nil {
t.Fatal("accepted unsafe source")
}
}
}
func TestPinnedReleaseWorkerAdmissionAndBlock(t *testing.T) {
testPinnedReleaseWorkerAdmissionAndBlock(t, "")
}
func TestPinnedReleaseWorkerARM64V8AdmissionAndBlock(t *testing.T) {
previous := imagePlatform
imagePlatform = "linux/arm64"
t.Cleanup(func() { imagePlatform = previous })
testPinnedReleaseWorkerAdmissionAndBlock(t, "v8")
}
func testPinnedReleaseWorkerAdmissionAndBlock(t *testing.T, variant string) {
t.Helper()
info := releaseFixture()
labels := map[string]string{"dsh-mgr.role": "runtime", "dsh-mgr.version": info.Version, "dsh-mgr.recipe": "7", "dsh-mgr.release": info.ReleaseID, "dsh-mgr.source": info.Repository, "dsh-mgr.commit": info.Commit}
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1.47/build":
var got map[string]string
if err := json.Unmarshal([]byte(r.URL.Query().Get("labels")), &got); err != nil {
t.Error(err)
}
if got["dsh-mgr.commit"] != info.Commit || r.URL.Query().Get("t") != "dsh-web:release-"+info.Commit {
t.Error("missing provenance", r.URL)
}
data, _ := io.ReadAll(r.Body)
if !bytes.Contains(data, []byte("pinned context")) {
t.Error("wrong build context")
}
io.WriteString(w, `{"aux":{"ID":"sha256:release"}}`)
case "/v1.47/images/sha256:release/json":
parts := strings.Split(imagePlatform, "/")
writeJSON(w, map[string]any{"Id": "sha256:release", "Os": parts[0], "Architecture": parts[1], "Variant": variant, "Config": map[string]any{"Labels": labels}})
case "/v1.47/containers/create":
var cfg struct {
Image, User string
HostConfig struct {
NetworkMode string
ReadonlyRootfs bool
Tmpfs map[string]string
}
}
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil || cfg.Image != "sha256:release" || cfg.User != "1000:1000" || cfg.HostConfig.NetworkMode != "none" || !cfg.HostConfig.ReadonlyRootfs || !strings.Contains(cfg.HostConfig.Tmpfs["/tmp"], "exec") {
t.Errorf("unsafe verification config %+v err=%v", cfg, err)
}
io.WriteString(w, `{"Id":"check"}`)
case "/v1.47/containers/check/start", "/v1.47/exec/probe/start", "/v1.47/containers/check":
w.WriteHeader(204)
case "/v1.47/containers/check/exec":
var cfg struct{ Cmd []string }
expected, _ := json.Marshal(info.Version)
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil || len(cfg.Cmd) != 3 || !strings.Contains(cfg.Cmd[2], "execFileSync('dsh',['--version']") || !strings.Contains(cfg.Cmd[2], "actual!=="+string(expected)) {
t.Errorf("missing exact version check %+v err=%v", cfg, err)
}
io.WriteString(w, `{"Id":"probe"}`)
case "/v1.47/exec/probe/json":
io.WriteString(w, `{"Running":false,"ExitCode":0}`)
case "/v1.47/containers/check/json":
io.WriteString(w, `{"State":{"Running":true}}`)
default:
t.Errorf("unexpected %s", r.URL)
w.WriteHeader(500)
}
})
if _, err := a.s.DB.Exec("UPDATE users SET admin=1 WHERE id='u'"); err != nil {
t.Fatal(err)
}
old := fetchRelease
t.Cleanup(func() { fetchRelease = old })
fetchRelease = func(ctx context.Context, repository, ref string) (releaseInfo, io.Reader, error) {
if repository != info.Repository || ref != info.Commit {
t.Error("not pinned", repository, ref)
}
return info, strings.NewReader("pinned context"), nil
}
b, err := a.enqueueRelease("u", info)
if err != nil {
t.Fatal(err)
}
a.runImageBuild(b)
if err = scanBuildSummary(a.s.DB.QueryRow("SELECT "+buildSummaryColumns+" FROM image_builds WHERE id=?", b.ID), &b); err != nil {
t.Fatal(err)
}
if b.Status != "success" {
t.Fatalf("%+v", b)
}
records := []buildRecord{b}
if err = a.reconcileBuilds(context.Background(), records); err != nil || records[0].Status != "success" {
t.Fatalf("reconciliation: %+v %v", records, err)
}
im, err := a.evaluateImage(context.Background(), b.ImageID)
if err != nil || !im.Selectable {
t.Fatalf("%+v %v", im, err)
}
if _, err = a.s.DB.Exec("INSERT INTO image_admissions(image_id,version,recipe_rev,platform,state,evidence,admitted_by,admitted_at) VALUES(?,?,?,?,?,?,?,?)", b.ImageID, b.Version, "7", imagePlatform, "blocked", "explicit block", "u", "now"); err != nil {
t.Fatal(err)
}
im, err = a.evaluateImage(context.Background(), b.ImageID)
if err != nil || im.Selectable || im.Admission != "blocked" {
t.Fatalf("%+v %v", im, err)
}
}
func TestReleaseRateLimitDedup(t *testing.T) {
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Error("queued release contacted Docker") })
info := releaseFixture()
first, err := a.enqueueRelease("u", info)
if err != nil {
t.Fatal(err)
}
for _, commit := range []string{strings.Repeat("b", 40), strings.Repeat("c", 40)} {
next := info
next.Commit = commit
if _, err := a.enqueueRelease("u", next); err != nil {
t.Fatal(err)
}
}
again, err := a.enqueueRelease("u", info)
if err != nil || again.ID != first.ID {
t.Fatal(again, err)
}
info.Commit = strings.Repeat("d", 40)
if _, err := a.enqueueRelease("u", info); err != errBuildRateLimited {
t.Fatal(err)
}
var count int
if err := a.s.DB.QueryRow("SELECT count(*) FROM image_build_requests").Scan(&count); err != nil || count != 3 {
t.Fatal(count, err)
}
}
func TestReleaseUnknownDiskBlocksBuild(t *testing.T) {
a, _ := imageTestAPI(t, func(w http.ResponseWriter, r *http.Request) { t.Errorf("unexpected Docker request %s", r.URL) })
t.Setenv("DSH_DOCKER_ROOT_VISIBLE", "")
if _, err := a.s.DB.Exec("UPDATE users SET admin=1 WHERE id='u'"); err != nil {
t.Fatal(err)
}
info := releaseFixture()
old := fetchRelease
t.Cleanup(func() { fetchRelease = old })
fetchRelease = func(context.Context, string, string) (releaseInfo, io.Reader, error) {
return info, strings.NewReader("context"), nil
}
b, err := a.enqueueRelease("u", info)
if err != nil {
t.Fatal(err)
}
a.runImageBuild(b)
if err := scanBuildSummary(a.s.DB.QueryRow("SELECT "+buildSummaryColumns+" FROM image_builds WHERE id=?", b.ID), &b); err != nil {
t.Fatal(err)
}
if b.Status != "failed" || !strings.Contains(b.Error, "capacity unknown") {
t.Fatalf("%+v", b)
}
}