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

236 lines
8.7 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) {
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], "Config": map[string]any{"Labels": labels}})
case "/v1.47/containers/create":
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":
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)
}
npmBuildClient = &http.Client{Transport: imageRoundTrip(func(r *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(`{"version":"0.2.0-rc.3"}`)), Header: make(http.Header)}, nil
})}
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)
}
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)
}
}