Compare commits

..

2 Commits

Author SHA1 Message Date
unkin-agent 442c7870eb Wait for the deb Packages index to list the upload in e2e
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
2026-11-02 22:17:59 +11:00
unkin-agent f8d6db3bd2 Preserve repo_type on remote update (#179)
ci/woodpecker/tag/docker Pipeline failed
PUT /api/v2/remotes/{name} decodes into a fresh struct without seeding repo_type, so terraform updates wipe it to "" on every non-local remote. Virtual repos then redirect member downloads to `/api/v1//<remote>/...`, which 404s.

- keep the existing repo_type when the PUT omits it or sends "", default to `remote` only for a new row, reject invalid values
- require base_url on update when repo_type is `remote`, matching create
- add migration 0011 to repair empty repo_type rows as `local` (no base_url) or `remote`
- add TestRepoTypeKeptOnUpdate

Reviewed-on: #179
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
2026-11-01 21:18:37 +11:00
4 changed files with 67 additions and 6 deletions
+10 -6
View File
@@ -107,19 +107,23 @@ func TestLocalDebRepo(t *testing.T) {
deb := testsupport.MinimalDeb("e2e-testpkg", "1.0.0", "amd64")
uploadFile(t, "local-deb", "e2e-testpkg_1.0.0_amd64.deb", deb, "application/vnd.debian.binary-package")
// The index is generated asynchronously after upload; poll for it.
resp, body := getEventually(t, api("/api/v1/local/local-deb/Packages"), 15*time.Second)
if resp.StatusCode != http.StatusOK {
t.Fatalf("Packages: status %d: %s", resp.StatusCode, body)
// The index is served (empty) at once and lists the package only once the
// upload is parsed asynchronously; poll until it does.
var pkgs string
for deadline := time.Now().Add(15 * time.Second); !strings.Contains(pkgs, "Package: e2e-testpkg") && time.Now().Before(deadline); time.Sleep(250 * time.Millisecond) {
resp, body := doRequest(t, http.MethodGet, api("/api/v1/local/local-deb/Packages"), nil, "")
if resp.StatusCode != http.StatusOK {
t.Fatalf("Packages: status %d: %s", resp.StatusCode, body)
}
pkgs = string(body)
}
pkgs := string(body)
for _, want := range []string{"Package: e2e-testpkg", "Version: 1.0.0", "Architecture: amd64", "Filename: pool/e2e-testpkg_1.0.0_amd64.deb", "SHA256:"} {
if !strings.Contains(pkgs, want) {
t.Fatalf("Packages missing %q:\n%s", want, pkgs)
}
}
resp, body = doRequest(t, http.MethodGet, api("/api/v1/local/local-deb/Release"), nil, "")
resp, body := doRequest(t, http.MethodGet, api("/api/v1/local/local-deb/Release"), nil, "")
if resp.StatusCode != http.StatusOK {
t.Fatalf("Release: status %d: %s", resp.StatusCode, body)
}
+14
View File
@@ -141,6 +141,20 @@ func (h *RemotesHandler) update(w http.ResponseWriter, r *http.Request) {
return
}
remote.Name = name
if remote.RepoType == "" && oldKnown {
remote.RepoType = existing.RepoType
}
if remote.RepoType == "" {
remote.RepoType = models.RepoTypeRemote
}
if !remote.RepoType.Valid() {
http.Error(w, fmt.Sprintf("invalid repo type: %q", remote.RepoType), http.StatusBadRequest)
return
}
if remote.RepoType == models.RepoTypeRemote && remote.BaseURL == "" {
http.Error(w, "base_url is required for remote repositories", http.StatusBadRequest)
return
}
if err := remote.ValidateMirrorlist(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
+42
View File
@@ -172,3 +172,45 @@ func TestQuarantineDaysValidation(t *testing.T) {
}
}
}
func TestRepoTypeKeptOnUpdate(t *testing.T) {
if testDSN == "" {
t.Skip("Docker unavailable")
}
db, err := database.New(testDSN)
if err != nil {
t.Fatal(err)
}
defer db.Close()
h := NewRemotesHandler(db, nil, nil).Routes()
repoType := func(name string) string {
t.Helper()
r, err := db.GetRemote(context.Background(), name)
if err != nil {
t.Fatalf("get %s: %v", name, err)
}
return string(r.RepoType)
}
const body = `"package_type":"rpm","base_url":"https://example.com/repo"`
for i, s := range []struct {
method, path, json string
code int
name, want string
}{
{"POST", "/", `{"name":"rt-remote",` + body + `}`, 201, "rt-remote", "remote"},
{"PUT", "/rt-remote", `{` + body + `}`, 200, "rt-remote", "remote"},
{"POST", "/", `{"name":"rt-local","package_type":"rpm","repo_type":"local"}`, 201, "rt-local", "local"},
{"PUT", "/rt-local", `{"package_type":"rpm","description":"x"}`, 200, "rt-local", "local"},
{"PUT", "/rt-local", `{"package_type":"rpm","repo_type":""}`, 200, "rt-local", "local"},
{"PUT", "/rt-local", `{"package_type":"rpm","repo_type":"bogus"}`, 400, "rt-local", "local"},
{"PUT", "/rt-local", `{"package_type":"rpm","repo_type":"remote"}`, 400, "rt-local", "local"},
{"PUT", "/rt-remote", `{"package_type":"rpm","repo_type":"local"}`, 200, "rt-remote", "local"},
} {
if c := do(t, h, s.method, s.path, s.json); c != s.code {
t.Fatalf("step %d %s %s = %d, want %d", i, s.method, s.path, c, s.code)
}
if got := repoType(s.name); got != s.want {
t.Errorf("step %d: repo_type = %q, want %q", i, got, s.want)
}
}
}
@@ -0,0 +1 @@
UPDATE remotes SET repo_type = CASE WHEN COALESCE(base_url, '') = '' THEN 'local' ELSE 'remote' END WHERE repo_type = '' OR repo_type IS NULL;