Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a3b7fe7b7 | |||
| 0ec28660ba |
@@ -185,13 +185,35 @@ func (h *LocalHandler) remove(w http.ResponseWriter, r *http.Request) {
|
|||||||
repoName := chi.URLParam(r, "name")
|
repoName := chi.URLParam(r, "name")
|
||||||
filePath := chi.URLParam(r, "*")
|
filePath := chi.URLParam(r, "*")
|
||||||
|
|
||||||
if err := h.db.DeleteLocalFile(r.Context(), repoName, filePath); err != nil {
|
if err := deleteLocalFile(r.Context(), h.db, repoName, filePath); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deleteLocalFile removes a local file and runs the provider's post-delete hook,
|
||||||
|
// so provider-derived state (e.g. RPM metadata that feeds generated repodata)
|
||||||
|
// stops referencing a package that no longer exists.
|
||||||
|
func deleteLocalFile(ctx context.Context, db *database.DB, repoName, filePath string) error {
|
||||||
|
if err := db.DeleteLocalFile(ctx, repoName, filePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
remote, err := db.GetRemote(ctx, repoName)
|
||||||
|
if err != nil {
|
||||||
|
return nil // file is gone; no repo left to resolve a cleanup hook from
|
||||||
|
}
|
||||||
|
prov, err := provider.Get(remote.PackageType)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if hook, ok := prov.(provider.PostDeleteHook); ok {
|
||||||
|
return hook.AfterDelete(ctx, repoName, filePath, db)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *LocalHandler) DB() *database.DB {
|
func (h *LocalHandler) DB() *database.DB {
|
||||||
return h.db
|
return h.db
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
|
"git.unkin.net/unkin/artifactapi/internal/database"
|
||||||
|
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||||
|
_ "git.unkin.net/unkin/artifactapi/internal/provider/rpm" // register the rpm provider so its PostDeleteHook runs
|
||||||
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLocalEvictCleansRPMMetadata verifies that evicting an RPM from a local
|
||||||
|
// repo also removes the derived rpm_metadata row, so generated repodata stops
|
||||||
|
// listing the deleted package.
|
||||||
|
func TestLocalEvictCleansRPMMetadata(t *testing.T) {
|
||||||
|
if testDSN == "" {
|
||||||
|
t.Skip("Docker unavailable")
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
db, err := database.New(testDSN)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
const repo = "rpm-evict-cleanup"
|
||||||
|
if err := db.CreateRemote(ctx, &models.Remote{Name: repo, PackageType: models.PackageRPM, RepoType: models.RepoTypeLocal}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = "sha256:bb22"
|
||||||
|
const path = "Packages/example-0.1.0-1.x86_64.rpm"
|
||||||
|
if err := db.UpsertBlob(ctx, hash, "blobs/bb/22", 2048, "application/x-rpm"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.CreateLocalFile(ctx, repo, path, hash); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.InsertRPMMetadata(ctx, &provider.RPMMetadata{
|
||||||
|
RepoName: repo, FilePath: path, ContentHash: hash,
|
||||||
|
Name: "example", Version: "0.1.0", Release: "1", Arch: "x86_64",
|
||||||
|
Requires: []provider.RPMDep{}, Provides: []provider.RPMDep{},
|
||||||
|
Files: []provider.RPMFile{}, Changelogs: []provider.RPMChangelog{},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewObjectsHandler(db)
|
||||||
|
router := chi.NewRouter()
|
||||||
|
router.Route("/locals/{name}/objects", func(r chi.Router) {
|
||||||
|
r.Delete("/*", h.LocalRoutes().ServeHTTP)
|
||||||
|
})
|
||||||
|
|
||||||
|
del := httptest.NewRequest("DELETE", "/locals/"+repo+"/objects/"+path, nil)
|
||||||
|
dw := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(dw, del)
|
||||||
|
if dw.Code != 204 {
|
||||||
|
t.Fatalf("evict = %d, want 204", dw.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, _ := db.GetLocalFile(ctx, repo, path); f != nil {
|
||||||
|
t.Fatalf("local file still present after evict: %+v", f)
|
||||||
|
}
|
||||||
|
entries, err := db.ListRPMMetadataEntries(ctx, repo)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(entries) != 0 {
|
||||||
|
t.Fatalf("rpm_metadata still present after evict: %+v", entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,7 +75,7 @@ func (h *ObjectsHandler) evictLocal(w http.ResponseWriter, r *http.Request) {
|
|||||||
repoName := chi.URLParam(r, "name")
|
repoName := chi.URLParam(r, "name")
|
||||||
path := chi.URLParam(r, "*")
|
path := chi.URLParam(r, "*")
|
||||||
|
|
||||||
if err := h.db.DeleteLocalFile(r.Context(), repoName, path); err != nil {
|
if err := deleteLocalFile(r.Context(), h.db, repoName, path); err != nil {
|
||||||
http.Error(w, fmt.Sprintf("evict failed: %v", err), http.StatusInternalServerError)
|
http.Error(w, fmt.Sprintf("evict failed: %v", err), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ func (db *DB) InsertRPMMetadata(ctx context.Context, meta *provider.RPMMetadata)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *DB) DeleteRPMMetadata(ctx context.Context, repoName, filePath string) error {
|
||||||
|
_, err := db.Pool.Exec(ctx, `DELETE FROM rpm_metadata WHERE repo_name = $1 AND file_path = $2`, repoName, filePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
type RPMMetadataRow struct {
|
type RPMMetadataRow struct {
|
||||||
RepoName string
|
RepoName string
|
||||||
FilePath string
|
FilePath string
|
||||||
|
|||||||
@@ -53,10 +53,20 @@ type PostUploadHook interface {
|
|||||||
AfterUpload(ctx context.Context, repoName, storagePath, contentHash string, blobs BlobReader, db MetadataStore)
|
AfterUpload(ctx context.Context, repoName, storagePath, contentHash string, blobs BlobReader, db MetadataStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PostDeleteHook lets a provider clean up derived state (e.g. RPM metadata that
|
||||||
|
// feeds generated repodata) after a local file is removed.
|
||||||
|
type PostDeleteHook interface {
|
||||||
|
AfterDelete(ctx context.Context, repoName, storagePath string, db MetadataDeleter) error
|
||||||
|
}
|
||||||
|
|
||||||
type MetadataStore interface {
|
type MetadataStore interface {
|
||||||
InsertRPMMetadata(ctx context.Context, meta *RPMMetadata) error
|
InsertRPMMetadata(ctx context.Context, meta *RPMMetadata) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MetadataDeleter interface {
|
||||||
|
DeleteRPMMetadata(ctx context.Context, repoName, filePath string) error
|
||||||
|
}
|
||||||
|
|
||||||
type RPMMetadataReader interface {
|
type RPMMetadataReader interface {
|
||||||
ListRPMMetadataEntries(ctx context.Context, repoName string) ([]RPMMetadata, error)
|
ListRPMMetadataEntries(ctx context.Context, repoName string) ([]RPMMetadata, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,6 +151,15 @@ func (p *Provider) AfterUpload(ctx context.Context, repoName, storagePath, conte
|
|||||||
slog.Info("rpm metadata: parsed", "repo", repoName, "name", meta.Name, "version", meta.Version, "arch", meta.Arch)
|
slog.Info("rpm metadata: parsed", "repo", repoName, "name", meta.Name, "version", meta.Version, "arch", meta.Arch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) AfterDelete(ctx context.Context, repoName, storagePath string, db provider.MetadataDeleter) error {
|
||||||
|
if err := db.DeleteRPMMetadata(ctx, repoName, storagePath); err != nil {
|
||||||
|
slog.Error("rpm metadata: delete failed", "repo", repoName, "path", storagePath, "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Info("rpm metadata: deleted", "repo", repoName, "path", storagePath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func rpmDepFromEntry(e rpmlib.Dependency) provider.RPMDep {
|
func rpmDepFromEntry(e rpmlib.Dependency) provider.RPMDep {
|
||||||
dep := provider.RPMDep{Name: e.Name()}
|
dep := provider.RPMDep{Name: e.Name()}
|
||||||
if e.Flags() != 0 {
|
if e.Flags() != 0 {
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ func (s *Server) routes() chi.Router {
|
|||||||
|
|
||||||
r.Get("/health", s.handleHealth)
|
r.Get("/health", s.handleHealth)
|
||||||
r.Get("/", s.handleRoot)
|
r.Get("/", s.handleRoot)
|
||||||
|
r.Get("/version", s.handleVersion)
|
||||||
|
|
||||||
proxyHandler := v1.NewProxyHandler(s.engine, s.virtEngine, s.db, s.store, s.localHandler)
|
proxyHandler := v1.NewProxyHandler(s.engine, s.virtEngine, s.db, s.store, s.localHandler)
|
||||||
r.Mount("/api/v1", proxyHandler.Routes())
|
r.Mount("/api/v1", proxyHandler.Routes())
|
||||||
@@ -143,7 +144,13 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
fmt.Fprint(w, `{"status":"ok"}`)
|
fmt.Fprint(w, `{"status":"ok"}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleRoot sends browsers landing on the bare domain to the web UI, which is
|
||||||
|
// served under /ui. The service identity that used to live here is at /version.
|
||||||
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/ui/", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
fmt.Fprintf(w, `{"name":"artifactapi","version":"%s"}`, s.version)
|
fmt.Fprintf(w, `{"name":"artifactapi","version":"%s"}`, s.version)
|
||||||
|
|||||||
@@ -129,13 +129,32 @@ func req(t *testing.T, method, path string, body string) (*http.Response, []byte
|
|||||||
return resp, b
|
return resp, b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reqNoRedirect issues a request without following redirects so the response's
|
||||||
|
// status and Location header can be asserted directly.
|
||||||
|
func reqNoRedirect(t *testing.T, method, path string) *http.Response {
|
||||||
|
t.Helper()
|
||||||
|
rq, _ := http.NewRequest(method, testTS.URL+path, nil)
|
||||||
|
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
|
||||||
|
return http.ErrUseLastResponse
|
||||||
|
}}
|
||||||
|
resp, err := client.Do(rq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s %s: %v", method, path, err)
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerHealthAndRoot(t *testing.T) {
|
func TestServerHealthAndRoot(t *testing.T) {
|
||||||
requireStack(t)
|
requireStack(t)
|
||||||
if resp, _ := req(t, "GET", "/health", ""); resp.StatusCode != 200 {
|
if resp, _ := req(t, "GET", "/health", ""); resp.StatusCode != 200 {
|
||||||
t.Errorf("health: %d", resp.StatusCode)
|
t.Errorf("health: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
if resp, b := req(t, "GET", "/", ""); resp.StatusCode != 200 || !strings.Contains(string(b), "test-version") {
|
if resp := reqNoRedirect(t, "GET", "/"); resp.StatusCode != http.StatusFound || resp.Header.Get("Location") != "/ui/" {
|
||||||
t.Errorf("root: %d %s", resp.StatusCode, b)
|
t.Errorf("root redirect: %d %q", resp.StatusCode, resp.Header.Get("Location"))
|
||||||
|
}
|
||||||
|
if resp, b := req(t, "GET", "/version", ""); resp.StatusCode != 200 || !strings.Contains(string(b), "test-version") {
|
||||||
|
t.Errorf("version: %d %s", resp.StatusCode, b)
|
||||||
}
|
}
|
||||||
if resp, _ := req(t, "GET", "/api/v2/health", ""); resp.StatusCode != 200 {
|
if resp, _ := req(t, "GET", "/api/v2/health", ""); resp.StatusCode != 200 {
|
||||||
t.Errorf("health v2: %d", resp.StatusCode)
|
t.Errorf("health v2: %d", resp.StatusCode)
|
||||||
|
|||||||
Reference in New Issue
Block a user