detached c642d32

refactor(http): follow clean architecture in handlers

- Registry handler: remove blobStorage dependency, use RegistryService
- Add GetBlobPath, AppendBlobChunk to RegistryService interface
- PutManifest now returns digest (moved calculation to use case)
- Config handler: add GetRoute for single route lookup
- Add route validation (ErrRouteDomainEmpty, ErrRouteImageEmpty)
- Update tests and regenerate mocks
commit
c642d3261b59281e740b534e142309e25ee53af4
author
bnema · · 2026-01-16 23:15 UTC
parent
22b6b85
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
14 files · +554 -192
M internal/adapters/in/http/admin/handler.go +21 -15
@@ -3,6 +3,7 @@ package admin
33
44 import (
55 "encoding/json"
6+ "errors"
67 "net/http"
78 "strings"
89
@@ -134,20 +135,18 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD
134135 return
135136 }
136137
137- routes := h.configSvc.GetRoutes(ctx)
138-
139138 if routeDomain == "" {
139+ routes := h.configSvc.GetRoutes(ctx)
140140 h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes})
141141 return
142142 }
143143
144- for _, route := range routes {
145- if route.Domain == routeDomain {
146- h.sendJSON(w, http.StatusOK, route)
147- return
148- }
144+ route, err := h.configSvc.GetRoute(ctx, routeDomain)
145+ if err != nil {
146+ h.sendError(w, http.StatusNotFound, "route not found")
147+ return
149148 }
150- h.sendError(w, http.StatusNotFound, "route not found")
149+ h.sendJSON(w, http.StatusOK, route)
151150 }
152151
153152 func (h *Handler) handleRoutesPost(w http.ResponseWriter, r *http.Request) {
@@ -170,14 +169,14 @@ func (h *Handler) handleRoutesPost(w http.ResponseWriter, r *http.Request) {
170169 return
171170 }
172171
173- if route.Domain == "" || route.Image == "" {
174- h.sendError(w, http.StatusBadRequest, "domain and image are required")
175- return
176- }
177-
178172 if err := h.configSvc.AddRoute(ctx, route); err != nil {
179173 log.Error().Err(err).Str("domain", route.Domain).Msg("failed to add route")
180- h.sendError(w, http.StatusInternalServerError, "failed to add route")
174+ switch {
175+ case errors.Is(err, domain.ErrRouteDomainEmpty), errors.Is(err, domain.ErrRouteImageEmpty):
176+ h.sendError(w, http.StatusBadRequest, err.Error())
177+ default:
178+ h.sendError(w, http.StatusInternalServerError, "failed to add route")
179+ }
181180 return
182181 }
183182
@@ -214,7 +213,14 @@ func (h *Handler) handleRoutesPut(w http.ResponseWriter, r *http.Request, routeD
214213
215214 if err := h.configSvc.UpdateRoute(ctx, route); err != nil {
216215 log.Error().Err(err).Str("domain", routeDomain).Msg("failed to update route")
217- h.sendError(w, http.StatusInternalServerError, "failed to update route")
216+ switch {
217+ case errors.Is(err, domain.ErrRouteNotFound):
218+ h.sendError(w, http.StatusNotFound, "route not found")
219+ case errors.Is(err, domain.ErrRouteDomainEmpty), errors.Is(err, domain.ErrRouteImageEmpty):
220+ h.sendError(w, http.StatusBadRequest, err.Error())
221+ default:
222+ h.sendError(w, http.StatusInternalServerError, "failed to update route")
223+ }
218224 return
219225 }
220226
@@ -3,6 +3,7 @@ package admin
3 3
4 import (4 import (
5 "encoding/json"5 "encoding/json"
6+ "errors"
6 "net/http"7 "net/http"
7 "strings"8 "strings"
8 9
@@ -134,20 +135,18 @@ func (h *Handler) handleRoutesGet(w http.ResponseWriter, r *http.Request, routeD
134 return135 return
135 }136 }
136 137
137- routes := h.configSvc.GetRoutes(ctx)
138-
139 if routeDomain == "" {138 if routeDomain == "" {
139+ routes := h.configSvc.GetRoutes(ctx)
140 h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes})140 h.sendJSON(w, http.StatusOK, map[string]any{"routes": routes})
141 return141 return
142 }142 }
143 143
144- for _, route := range routes {144+ route, err := h.configSvc.GetRoute(ctx, routeDomain)
145- if route.Domain == routeDomain {145+ if err != nil {
146- h.sendJSON(w, http.StatusOK, route)146+ h.sendError(w, http.StatusNotFound, "route not found")
147- return147+ return
148- }
149 }148 }
150- h.sendError(w, http.StatusNotFound, "route not found")149+ h.sendJSON(w, http.StatusOK, route)
151 }150 }
152 151
153 func (h *Handler) handleRoutesPost(w http.ResponseWriter, r *http.Request) {152 func (h *Handler) handleRoutesPost(w http.ResponseWriter, r *http.Request) {
@@ -170,14 +169,14 @@ func (h *Handler) handleRoutesPost(w http.ResponseWriter, r *http.Request) {
170 return169 return
171 }170 }
172 171
173- if route.Domain == "" || route.Image == "" {
174- h.sendError(w, http.StatusBadRequest, "domain and image are required")
175- return
176- }
177-
178 if err := h.configSvc.AddRoute(ctx, route); err != nil {172 if err := h.configSvc.AddRoute(ctx, route); err != nil {
179 log.Error().Err(err).Str("domain", route.Domain).Msg("failed to add route")173 log.Error().Err(err).Str("domain", route.Domain).Msg("failed to add route")
180- h.sendError(w, http.StatusInternalServerError, "failed to add route")174+ switch {
175+ case errors.Is(err, domain.ErrRouteDomainEmpty), errors.Is(err, domain.ErrRouteImageEmpty):
176+ h.sendError(w, http.StatusBadRequest, err.Error())
177+ default:
178+ h.sendError(w, http.StatusInternalServerError, "failed to add route")
179+ }
181 return180 return
182 }181 }
183 182
@@ -214,7 +213,14 @@ func (h *Handler) handleRoutesPut(w http.ResponseWriter, r *http.Request, routeD
214 213
215 if err := h.configSvc.UpdateRoute(ctx, route); err != nil {214 if err := h.configSvc.UpdateRoute(ctx, route); err != nil {
216 log.Error().Err(err).Str("domain", routeDomain).Msg("failed to update route")215 log.Error().Err(err).Str("domain", routeDomain).Msg("failed to update route")
217- h.sendError(w, http.StatusInternalServerError, "failed to update route")216+ switch {
217+ case errors.Is(err, domain.ErrRouteNotFound):
218+ h.sendError(w, http.StatusNotFound, "route not found")
219+ case errors.Is(err, domain.ErrRouteDomainEmpty), errors.Is(err, domain.ErrRouteImageEmpty):
220+ h.sendError(w, http.StatusBadRequest, err.Error())
221+ default:
222+ h.sendError(w, http.StatusInternalServerError, "failed to update route")
223+ }
218 return224 return
219 }225 }
220 226
M internal/adapters/in/http/admin/handler_test.go +25 -19
@@ -590,9 +590,9 @@ func TestHandler_RoutesGet_SingleRoute(t *testing.T) {
590590
591591 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())
592592
593- configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{
594- {Domain: "app.example.com", Image: "app:latest"},
595- })
593+ configSvc.EXPECT().GetRoute(mock.Anything, "app.example.com").Return(&domain.Route{
594+ Domain: "app.example.com", Image: "app:latest",
595+ }, nil)
596596
597597 req := httptest.NewRequest("GET", "/admin/routes/app.example.com", nil)
598598 req = req.WithContext(ctxWithScopes("admin:routes:read"))
@@ -617,7 +617,7 @@ func TestHandler_RoutesGet_NotFound(t *testing.T) {
617617
618618 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())
619619
620- configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{})
620+ configSvc.EXPECT().GetRoute(mock.Anything, "nonexistent.example.com").Return(nil, domain.ErrRouteNotFound)
621621
622622 req := httptest.NewRequest("GET", "/admin/routes/nonexistent.example.com", nil)
623623 req = req.WithContext(ctxWithScopes("admin:routes:read"))
@@ -629,33 +629,39 @@ func TestHandler_RoutesGet_NotFound(t *testing.T) {
629629 }
630630
631631 func TestHandler_RoutesPost_MissingFields(t *testing.T) {
632- configSvc := inmocks.NewMockConfigService(t)
633- authSvc := inmocks.NewMockAuthService(t)
634- containerSvc := inmocks.NewMockContainerService(t)
635- secretSvc := inmocks.NewMockSecretService(t)
636-
637- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())
638-
639632 tests := []struct {
640- name string
641- body string
633+ name string
634+ body string
635+ mockErr error
642636 }{
643637 {
644- name: "missing domain",
645- body: `{"image": "app:latest"}`,
638+ name: "missing domain",
639+ body: `{"image": "app:latest"}`,
640+ mockErr: domain.ErrRouteDomainEmpty,
646641 },
647642 {
648- name: "missing image",
649- body: `{"domain": "app.example.com"}`,
643+ name: "missing image",
644+ body: `{"domain": "app.example.com"}`,
645+ mockErr: domain.ErrRouteImageEmpty,
650646 },
651647 {
652- name: "empty object",
653- body: `{}`,
648+ name: "empty object",
649+ body: `{}`,
650+ mockErr: domain.ErrRouteDomainEmpty,
654651 },
655652 }
656653
657654 for _, tt := range tests {
658655 t.Run(tt.name, func(t *testing.T) {
656+ configSvc := inmocks.NewMockConfigService(t)
657+ authSvc := inmocks.NewMockAuthService(t)
658+ containerSvc := inmocks.NewMockContainerService(t)
659+ secretSvc := inmocks.NewMockSecretService(t)
660+
661+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())
662+
663+ configSvc.EXPECT().AddRoute(mock.Anything, mock.Anything).Return(tt.mockErr)
664+
659665 req := httptest.NewRequest("POST", "/admin/routes", bytes.NewBufferString(tt.body))
660666 req = req.WithContext(ctxWithScopes("admin:routes:write"))
661667 rec := httptest.NewRecorder()
@@ -590,9 +590,9 @@ func TestHandler_RoutesGet_SingleRoute(t *testing.T) {
590 590
591 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())591 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())
592 592
593- configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{593+ configSvc.EXPECT().GetRoute(mock.Anything, "app.example.com").Return(&domain.Route{
594- {Domain: "app.example.com", Image: "app:latest"},594+ Domain: "app.example.com", Image: "app:latest",
595- })595+ }, nil)
596 596
597 req := httptest.NewRequest("GET", "/admin/routes/app.example.com", nil)597 req := httptest.NewRequest("GET", "/admin/routes/app.example.com", nil)
598 req = req.WithContext(ctxWithScopes("admin:routes:read"))598 req = req.WithContext(ctxWithScopes("admin:routes:read"))
@@ -617,7 +617,7 @@ func TestHandler_RoutesGet_NotFound(t *testing.T) {
617 617
618 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())618 handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())
619 619
620- configSvc.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{})620+ configSvc.EXPECT().GetRoute(mock.Anything, "nonexistent.example.com").Return(nil, domain.ErrRouteNotFound)
621 621
622 req := httptest.NewRequest("GET", "/admin/routes/nonexistent.example.com", nil)622 req := httptest.NewRequest("GET", "/admin/routes/nonexistent.example.com", nil)
623 req = req.WithContext(ctxWithScopes("admin:routes:read"))623 req = req.WithContext(ctxWithScopes("admin:routes:read"))
@@ -629,33 +629,39 @@ func TestHandler_RoutesGet_NotFound(t *testing.T) {
629 }629 }
630 630
631 func TestHandler_RoutesPost_MissingFields(t *testing.T) {631 func TestHandler_RoutesPost_MissingFields(t *testing.T) {
632- configSvc := inmocks.NewMockConfigService(t)
633- authSvc := inmocks.NewMockAuthService(t)
634- containerSvc := inmocks.NewMockContainerService(t)
635- secretSvc := inmocks.NewMockSecretService(t)
636-
637- handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())
638-
639 tests := []struct {632 tests := []struct {
640- name string633+ name string
641- body string634+ body string
635+ mockErr error
642 }{636 }{
643 {637 {
644- name: "missing domain",638+ name: "missing domain",
645- body: `{"image": "app:latest"}`,639+ body: `{"image": "app:latest"}`,
640+ mockErr: domain.ErrRouteDomainEmpty,
646 },641 },
647 {642 {
648- name: "missing image",643+ name: "missing image",
649- body: `{"domain": "app.example.com"}`,644+ body: `{"domain": "app.example.com"}`,
645+ mockErr: domain.ErrRouteImageEmpty,
650 },646 },
651 {647 {
652- name: "empty object",648+ name: "empty object",
653- body: `{}`,649+ body: `{}`,
650+ mockErr: domain.ErrRouteDomainEmpty,
654 },651 },
655 }652 }
656 653
657 for _, tt := range tests {654 for _, tt := range tests {
658 t.Run(tt.name, func(t *testing.T) {655 t.Run(tt.name, func(t *testing.T) {
656+ configSvc := inmocks.NewMockConfigService(t)
657+ authSvc := inmocks.NewMockAuthService(t)
658+ containerSvc := inmocks.NewMockContainerService(t)
659+ secretSvc := inmocks.NewMockSecretService(t)
660+
661+ handler := NewHandler(configSvc, authSvc, containerSvc, inmocks.NewMockHealthService(t), secretSvc, nil, testLogger())
662+
663+ configSvc.EXPECT().AddRoute(mock.Anything, mock.Anything).Return(tt.mockErr)
664+
659 req := httptest.NewRequest("POST", "/admin/routes", bytes.NewBufferString(tt.body))665 req := httptest.NewRequest("POST", "/admin/routes", bytes.NewBufferString(tt.body))
660 req = req.WithContext(ctxWithScopes("admin:routes:write"))666 req = req.WithContext(ctxWithScopes("admin:routes:write"))
661 rec := httptest.NewRecorder()667 rec := httptest.NewRecorder()
M internal/adapters/in/http/registry/handler.go +4 -13
@@ -2,7 +2,6 @@
22 package registry
33
44 import (
5- "crypto/sha256"
65 "encoding/json"
76 "errors"
87 "fmt"
@@ -14,7 +13,6 @@ import (
1413 "github.com/bnema/zerowrap"
1514
1615 "gordon/internal/boundaries/in"
17- "gordon/internal/boundaries/out"
1816 "gordon/internal/domain"
1917 "gordon/pkg/manifest"
2018 "gordon/pkg/validation"
@@ -30,22 +28,16 @@ const (
3028 // Handler implements the HTTP handler for Docker Registry API v2.
3129 type Handler struct {
3230 registrySvc in.RegistryService
33- blobStorage out.BlobStorage
34- eventBus out.EventPublisher
3531 log zerowrap.Logger
3632 }
3733
3834 // NewHandler creates a new registry HTTP handler.
3935 func NewHandler(
4036 registrySvc in.RegistryService,
41- blobStorage out.BlobStorage,
42- eventBus out.EventPublisher,
4337 log zerowrap.Logger,
4438 ) *Handler {
4539 return &Handler{
4640 registrySvc: registrySvc,
47- blobStorage: blobStorage,
48- eventBus: eventBus,
4941 log: log,
5042 }
5143 }
@@ -331,14 +323,13 @@ func (h *Handler) handlePutManifest(w http.ResponseWriter, r *http.Request) {
331323 Annotations: annotations,
332324 }
333325
334- if err := h.registrySvc.PutManifest(ctx, manifestObj); err != nil {
326+ digest, err := h.registrySvc.PutManifest(ctx, manifestObj)
327+ if err != nil {
335328 log.Error().Err(err).Str("name", name).Str("reference", reference).Msg("failed to store manifest")
336329 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
337330 return
338331 }
339332
340- // Calculate digest for Docker-Content-Digest header
341- digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data))
342333 w.Header().Set("Docker-Content-Digest", digest)
343334 w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference))
344335 w.WriteHeader(http.StatusCreated)
@@ -352,7 +343,7 @@ func (h *Handler) handleGetBlob(w http.ResponseWriter, r *http.Request) {
352343
353344 log.Debug().Str("name", name).Str("digest", digest).Msg("GET blob")
354345
355- path, err := h.blobStorage.GetBlobPath(digest)
346+ path, err := h.registrySvc.GetBlobPath(ctx, digest)
356347 if err != nil {
357348 log.Warn().Err(err).Str("name", name).Str("digest", digest).Msg("blob not found")
358349 http.NotFound(w, r)
@@ -422,7 +413,7 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) {
422413 }
423414
424415 // Append the chunk to the upload
425- length, err := h.blobStorage.AppendBlobChunk(name, uuid, chunk)
416+ length, err := h.registrySvc.AppendBlobChunk(ctx, name, uuid, chunk)
426417 if err != nil {
427418 log.Error().Err(err).Msg("failed to append blob chunk")
428419 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
@@ -2,7 +2,6 @@
2 package registry2 package registry
3 3
4 import (4 import (
5- "crypto/sha256"
6 "encoding/json"5 "encoding/json"
7 "errors"6 "errors"
8 "fmt"7 "fmt"
@@ -14,7 +13,6 @@ import (
14 "github.com/bnema/zerowrap"13 "github.com/bnema/zerowrap"
15 14
16 "gordon/internal/boundaries/in"15 "gordon/internal/boundaries/in"
17- "gordon/internal/boundaries/out"
18 "gordon/internal/domain"16 "gordon/internal/domain"
19 "gordon/pkg/manifest"17 "gordon/pkg/manifest"
20 "gordon/pkg/validation"18 "gordon/pkg/validation"
@@ -30,22 +28,16 @@ const (
30 // Handler implements the HTTP handler for Docker Registry API v2.28 // Handler implements the HTTP handler for Docker Registry API v2.
31 type Handler struct {29 type Handler struct {
32 registrySvc in.RegistryService30 registrySvc in.RegistryService
33- blobStorage out.BlobStorage
34- eventBus out.EventPublisher
35 log zerowrap.Logger31 log zerowrap.Logger
36 }32 }
37 33
38 // NewHandler creates a new registry HTTP handler.34 // NewHandler creates a new registry HTTP handler.
39 func NewHandler(35 func NewHandler(
40 registrySvc in.RegistryService,36 registrySvc in.RegistryService,
41- blobStorage out.BlobStorage,
42- eventBus out.EventPublisher,
43 log zerowrap.Logger,37 log zerowrap.Logger,
44 ) *Handler {38 ) *Handler {
45 return &Handler{39 return &Handler{
46 registrySvc: registrySvc,40 registrySvc: registrySvc,
47- blobStorage: blobStorage,
48- eventBus: eventBus,
49 log: log,41 log: log,
50 }42 }
51 }43 }
@@ -331,14 +323,13 @@ func (h *Handler) handlePutManifest(w http.ResponseWriter, r *http.Request) {
331 Annotations: annotations,323 Annotations: annotations,
332 }324 }
333 325
334- if err := h.registrySvc.PutManifest(ctx, manifestObj); err != nil {326+ digest, err := h.registrySvc.PutManifest(ctx, manifestObj)
327+ if err != nil {
335 log.Error().Err(err).Str("name", name).Str("reference", reference).Msg("failed to store manifest")328 log.Error().Err(err).Str("name", name).Str("reference", reference).Msg("failed to store manifest")
336 http.Error(w, "Internal Server Error", http.StatusInternalServerError)329 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
337 return330 return
338 }331 }
339 332
340- // Calculate digest for Docker-Content-Digest header
341- digest := fmt.Sprintf("sha256:%x", sha256.Sum256(data))
342 w.Header().Set("Docker-Content-Digest", digest)333 w.Header().Set("Docker-Content-Digest", digest)
343 w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference))334 w.Header().Set("Location", fmt.Sprintf("/v2/%s/manifests/%s", name, reference))
344 w.WriteHeader(http.StatusCreated)335 w.WriteHeader(http.StatusCreated)
@@ -352,7 +343,7 @@ func (h *Handler) handleGetBlob(w http.ResponseWriter, r *http.Request) {
352 343
353 log.Debug().Str("name", name).Str("digest", digest).Msg("GET blob")344 log.Debug().Str("name", name).Str("digest", digest).Msg("GET blob")
354 345
355- path, err := h.blobStorage.GetBlobPath(digest)346+ path, err := h.registrySvc.GetBlobPath(ctx, digest)
356 if err != nil {347 if err != nil {
357 log.Warn().Err(err).Str("name", name).Str("digest", digest).Msg("blob not found")348 log.Warn().Err(err).Str("name", name).Str("digest", digest).Msg("blob not found")
358 http.NotFound(w, r)349 http.NotFound(w, r)
@@ -422,7 +413,7 @@ func (h *Handler) handleBlobUpload(w http.ResponseWriter, r *http.Request) {
422 }413 }
423 414
424 // Append the chunk to the upload415 // Append the chunk to the upload
425- length, err := h.blobStorage.AppendBlobChunk(name, uuid, chunk)416+ length, err := h.registrySvc.AppendBlobChunk(ctx, name, uuid, chunk)
426 if err != nil {417 if err != nil {
427 log.Error().Err(err).Msg("failed to append blob chunk")418 log.Error().Err(err).Msg("failed to append blob chunk")
428 http.Error(w, "Internal Server Error", http.StatusInternalServerError)419 http.Error(w, "Internal Server Error", http.StatusInternalServerError)
M internal/adapters/in/http/registry/handler_test.go +29 -74
@@ -12,7 +12,6 @@ import (
1212 "github.com/stretchr/testify/mock"
1313
1414 inmocks "gordon/internal/boundaries/in/mocks"
15- "gordon/internal/boundaries/out/mocks"
1615 "gordon/internal/domain"
1716 )
1817
@@ -22,10 +21,8 @@ func testLogger() zerowrap.Logger {
2221
2322 func TestHandler_Base(t *testing.T) {
2423 registrySvc := inmocks.NewMockRegistryService(t)
25- blobStorage := mocks.NewMockBlobStorage(t)
26- eventBus := mocks.NewMockEventPublisher(t)
2724
28- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
25+ handler := NewHandler(registrySvc, testLogger())
2926
3027 req := httptest.NewRequest("GET", "/v2/", nil)
3128 rec := httptest.NewRecorder()
@@ -38,10 +35,8 @@ func TestHandler_Base(t *testing.T) {
3835
3936 func TestHandler_NotFound(t *testing.T) {
4037 registrySvc := inmocks.NewMockRegistryService(t)
41- blobStorage := mocks.NewMockBlobStorage(t)
42- eventBus := mocks.NewMockEventPublisher(t)
4338
44- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
39+ handler := NewHandler(registrySvc, testLogger())
4540
4641 req := httptest.NewRequest("GET", "/v2/unknown/path", nil)
4742 rec := httptest.NewRecorder()
@@ -53,10 +48,8 @@ func TestHandler_NotFound(t *testing.T) {
5348
5449 func TestHandler_GetManifest_Success(t *testing.T) {
5550 registrySvc := inmocks.NewMockRegistryService(t)
56- blobStorage := mocks.NewMockBlobStorage(t)
57- eventBus := mocks.NewMockEventPublisher(t)
5851
59- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
52+ handler := NewHandler(registrySvc, testLogger())
6053
6154 manifestData := []byte(`{"schemaVersion": 2}`)
6255 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{
@@ -78,10 +71,8 @@ func TestHandler_GetManifest_Success(t *testing.T) {
7871
7972 func TestHandler_GetManifest_HEAD(t *testing.T) {
8073 registrySvc := inmocks.NewMockRegistryService(t)
81- blobStorage := mocks.NewMockBlobStorage(t)
82- eventBus := mocks.NewMockEventPublisher(t)
8374
84- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
75+ handler := NewHandler(registrySvc, testLogger())
8576
8677 manifestData := []byte(`{"schemaVersion": 2}`)
8778 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{
@@ -104,10 +95,8 @@ func TestHandler_GetManifest_HEAD(t *testing.T) {
10495
10596 func TestHandler_GetManifest_NotFound(t *testing.T) {
10697 registrySvc := inmocks.NewMockRegistryService(t)
107- blobStorage := mocks.NewMockBlobStorage(t)
108- eventBus := mocks.NewMockEventPublisher(t)
10998
110- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
99+ handler := NewHandler(registrySvc, testLogger())
111100
112101 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError)
113102
@@ -121,10 +110,8 @@ func TestHandler_GetManifest_NotFound(t *testing.T) {
121110
122111 func TestHandler_GetManifest_NestedName(t *testing.T) {
123112 registrySvc := inmocks.NewMockRegistryService(t)
124- blobStorage := mocks.NewMockBlobStorage(t)
125- eventBus := mocks.NewMockEventPublisher(t)
126113
127- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
114+ handler := NewHandler(registrySvc, testLogger())
128115
129116 manifestData := []byte(`{"schemaVersion": 2}`)
130117 registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{
@@ -144,15 +131,13 @@ func TestHandler_GetManifest_NestedName(t *testing.T) {
144131
145132 func TestHandler_PutManifest_Success(t *testing.T) {
146133 registrySvc := inmocks.NewMockRegistryService(t)
147- blobStorage := mocks.NewMockBlobStorage(t)
148- eventBus := mocks.NewMockEventPublisher(t)
149134
150- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
135+ handler := NewHandler(registrySvc, testLogger())
151136
152137 manifestData := []byte(`{"schemaVersion": 2}`)
153138 registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool {
154139 return m.Name == "myapp" && m.Reference == "latest" && m.ContentType == "application/vnd.docker.distribution.manifest.v2+json"
155- })).Return(nil)
140+ })).Return("sha256:abc123", nil)
156141
157142 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
158143 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
@@ -161,16 +146,14 @@ func TestHandler_PutManifest_Success(t *testing.T) {
161146 handler.ServeHTTP(rec, req)
162147
163148 assert.Equal(t, http.StatusCreated, rec.Code)
164- assert.NotEmpty(t, rec.Header().Get("Docker-Content-Digest"))
149+ assert.Equal(t, "sha256:abc123", rec.Header().Get("Docker-Content-Digest"))
165150 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/manifests/latest")
166151 }
167152
168153 func TestHandler_PutManifest_MissingContentType(t *testing.T) {
169154 registrySvc := inmocks.NewMockRegistryService(t)
170- blobStorage := mocks.NewMockBlobStorage(t)
171- eventBus := mocks.NewMockEventPublisher(t)
172155
173- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
156+ handler := NewHandler(registrySvc, testLogger())
174157
175158 manifestData := []byte(`{"schemaVersion": 2}`)
176159
@@ -185,13 +168,11 @@ func TestHandler_PutManifest_MissingContentType(t *testing.T) {
185168
186169 func TestHandler_PutManifest_StorageError(t *testing.T) {
187170 registrySvc := inmocks.NewMockRegistryService(t)
188- blobStorage := mocks.NewMockBlobStorage(t)
189- eventBus := mocks.NewMockEventPublisher(t)
190171
191- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
172+ handler := NewHandler(registrySvc, testLogger())
192173
193174 manifestData := []byte(`{"schemaVersion": 2}`)
194- registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return(assert.AnError)
175+ registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return("", assert.AnError)
195176
196177 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
197178 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
@@ -204,10 +185,8 @@ func TestHandler_PutManifest_StorageError(t *testing.T) {
204185
205186 func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) {
206187 registrySvc := inmocks.NewMockRegistryService(t)
207- blobStorage := mocks.NewMockBlobStorage(t)
208- eventBus := mocks.NewMockEventPublisher(t)
209188
210- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
189+ handler := NewHandler(registrySvc, testLogger())
211190
212191 req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil)
213192 rec := httptest.NewRecorder()
@@ -219,10 +198,8 @@ func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) {
219198
220199 func TestHandler_GetBlob_Success(t *testing.T) {
221200 registrySvc := inmocks.NewMockRegistryService(t)
222- blobStorage := mocks.NewMockBlobStorage(t)
223- eventBus := mocks.NewMockEventPublisher(t)
224201
225- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
202+ handler := NewHandler(registrySvc, testLogger())
226203
227204 // Create a temp file for http.ServeFile
228205 tmpFile, err := os.CreateTemp("", "blob-*")
@@ -233,7 +210,7 @@ func TestHandler_GetBlob_Success(t *testing.T) {
233210 _, err = tmpFile.Write(blobContent)
234211 assert.NoError(t, err)
235212
236- blobStorage.EXPECT().GetBlobPath("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(tmpFile.Name(), nil)
213+ registrySvc.EXPECT().GetBlobPath(mock.Anything, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(tmpFile.Name(), nil)
237214
238215 req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)
239216 rec := httptest.NewRecorder()
@@ -246,12 +223,10 @@ func TestHandler_GetBlob_Success(t *testing.T) {
246223
247224 func TestHandler_GetBlob_NotFound(t *testing.T) {
248225 registrySvc := inmocks.NewMockRegistryService(t)
249- blobStorage := mocks.NewMockBlobStorage(t)
250- eventBus := mocks.NewMockEventPublisher(t)
251226
252- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
227+ handler := NewHandler(registrySvc, testLogger())
253228
254- blobStorage.EXPECT().GetBlobPath("sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError)
229+ registrySvc.EXPECT().GetBlobPath(mock.Anything, "sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError)
255230
256231 req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:0000000000000000000000000000000000000000000000000000000000000000", nil)
257232 rec := httptest.NewRecorder()
@@ -263,10 +238,8 @@ func TestHandler_GetBlob_NotFound(t *testing.T) {
263238
264239 func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) {
265240 registrySvc := inmocks.NewMockRegistryService(t)
266- blobStorage := mocks.NewMockBlobStorage(t)
267- eventBus := mocks.NewMockEventPublisher(t)
268241
269- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
242+ handler := NewHandler(registrySvc, testLogger())
270243
271244 req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)
272245 rec := httptest.NewRecorder()
@@ -278,10 +251,8 @@ func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) {
278251
279252 func TestHandler_StartBlobUpload_Success(t *testing.T) {
280253 registrySvc := inmocks.NewMockRegistryService(t)
281- blobStorage := mocks.NewMockBlobStorage(t)
282- eventBus := mocks.NewMockEventPublisher(t)
283254
284- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
255+ handler := NewHandler(registrySvc, testLogger())
285256
286257 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("1234567890-myapp", nil)
287258
@@ -297,10 +268,8 @@ func TestHandler_StartBlobUpload_Success(t *testing.T) {
297268
298269 func TestHandler_StartBlobUpload_Error(t *testing.T) {
299270 registrySvc := inmocks.NewMockRegistryService(t)
300- blobStorage := mocks.NewMockBlobStorage(t)
301- eventBus := mocks.NewMockEventPublisher(t)
302271
303- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
272+ handler := NewHandler(registrySvc, testLogger())
304273
305274 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError)
306275
@@ -314,13 +283,11 @@ func TestHandler_StartBlobUpload_Error(t *testing.T) {
314283
315284 func TestHandler_BlobUpload_PATCH(t *testing.T) {
316285 registrySvc := inmocks.NewMockRegistryService(t)
317- blobStorage := mocks.NewMockBlobStorage(t)
318- eventBus := mocks.NewMockEventPublisher(t)
319286
320- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
287+ handler := NewHandler(registrySvc, testLogger())
321288
322289 chunkData := []byte("chunk content")
323- blobStorage.EXPECT().AppendBlobChunk("myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)
290+ registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)
324291
325292 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/1234567890-myapp", bytes.NewReader(chunkData))
326293 rec := httptest.NewRecorder()
@@ -333,13 +300,11 @@ func TestHandler_BlobUpload_PATCH(t *testing.T) {
333300
334301 func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) {
335302 registrySvc := inmocks.NewMockRegistryService(t)
336- blobStorage := mocks.NewMockBlobStorage(t)
337- eventBus := mocks.NewMockEventPublisher(t)
338303
339- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
304+ handler := NewHandler(registrySvc, testLogger())
340305
341306 chunkData := []byte("final chunk")
342- blobStorage.EXPECT().AppendBlobChunk("myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)
307+ registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)
343308 registrySvc.EXPECT().FinishUpload(mock.Anything, "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
344309
345310 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData))
@@ -354,10 +319,8 @@ func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) {
354319
355320 func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) {
356321 registrySvc := inmocks.NewMockRegistryService(t)
357- blobStorage := mocks.NewMockBlobStorage(t)
358- eventBus := mocks.NewMockEventPublisher(t)
359322
360- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
323+ handler := NewHandler(registrySvc, testLogger())
361324
362325 chunkData := []byte("final chunk")
363326 // Note: Invalid digest format is rejected by validation before reaching storage layer
@@ -373,10 +336,8 @@ func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) {
373336
374337 func TestHandler_ListTags_Success(t *testing.T) {
375338 registrySvc := inmocks.NewMockRegistryService(t)
376- blobStorage := mocks.NewMockBlobStorage(t)
377- eventBus := mocks.NewMockEventPublisher(t)
378339
379- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
340+ handler := NewHandler(registrySvc, testLogger())
380341
381342 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil)
382343
@@ -393,10 +354,8 @@ func TestHandler_ListTags_Success(t *testing.T) {
393354
394355 func TestHandler_ListTags_NotFound(t *testing.T) {
395356 registrySvc := inmocks.NewMockRegistryService(t)
396- blobStorage := mocks.NewMockBlobStorage(t)
397- eventBus := mocks.NewMockEventPublisher(t)
398357
399- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
358+ handler := NewHandler(registrySvc, testLogger())
400359
401360 registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError)
402361
@@ -410,10 +369,8 @@ func TestHandler_ListTags_NotFound(t *testing.T) {
410369
411370 func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {
412371 registrySvc := inmocks.NewMockRegistryService(t)
413- blobStorage := mocks.NewMockBlobStorage(t)
414- eventBus := mocks.NewMockEventPublisher(t)
415372
416- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
373+ handler := NewHandler(registrySvc, testLogger())
417374
418375 req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil)
419376 rec := httptest.NewRecorder()
@@ -425,10 +382,8 @@ func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {
425382
426383 func TestHandler_RegisterRoutes(t *testing.T) {
427384 registrySvc := inmocks.NewMockRegistryService(t)
428- blobStorage := mocks.NewMockBlobStorage(t)
429- eventBus := mocks.NewMockEventPublisher(t)
430385
431- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())
386+ handler := NewHandler(registrySvc, testLogger())
432387
433388 mux := http.NewServeMux()
434389 handler.RegisterRoutes(mux)
@@ -12,7 +12,6 @@ import (
12 "github.com/stretchr/testify/mock"12 "github.com/stretchr/testify/mock"
13 13
14 inmocks "gordon/internal/boundaries/in/mocks"14 inmocks "gordon/internal/boundaries/in/mocks"
15- "gordon/internal/boundaries/out/mocks"
16 "gordon/internal/domain"15 "gordon/internal/domain"
17 )16 )
18 17
@@ -22,10 +21,8 @@ func testLogger() zerowrap.Logger {
22 21
23 func TestHandler_Base(t *testing.T) {22 func TestHandler_Base(t *testing.T) {
24 registrySvc := inmocks.NewMockRegistryService(t)23 registrySvc := inmocks.NewMockRegistryService(t)
25- blobStorage := mocks.NewMockBlobStorage(t)
26- eventBus := mocks.NewMockEventPublisher(t)
27 24
28- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())25+ handler := NewHandler(registrySvc, testLogger())
29 26
30 req := httptest.NewRequest("GET", "/v2/", nil)27 req := httptest.NewRequest("GET", "/v2/", nil)
31 rec := httptest.NewRecorder()28 rec := httptest.NewRecorder()
@@ -38,10 +35,8 @@ func TestHandler_Base(t *testing.T) {
38 35
39 func TestHandler_NotFound(t *testing.T) {36 func TestHandler_NotFound(t *testing.T) {
40 registrySvc := inmocks.NewMockRegistryService(t)37 registrySvc := inmocks.NewMockRegistryService(t)
41- blobStorage := mocks.NewMockBlobStorage(t)
42- eventBus := mocks.NewMockEventPublisher(t)
43 38
44- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())39+ handler := NewHandler(registrySvc, testLogger())
45 40
46 req := httptest.NewRequest("GET", "/v2/unknown/path", nil)41 req := httptest.NewRequest("GET", "/v2/unknown/path", nil)
47 rec := httptest.NewRecorder()42 rec := httptest.NewRecorder()
@@ -53,10 +48,8 @@ func TestHandler_NotFound(t *testing.T) {
53 48
54 func TestHandler_GetManifest_Success(t *testing.T) {49 func TestHandler_GetManifest_Success(t *testing.T) {
55 registrySvc := inmocks.NewMockRegistryService(t)50 registrySvc := inmocks.NewMockRegistryService(t)
56- blobStorage := mocks.NewMockBlobStorage(t)
57- eventBus := mocks.NewMockEventPublisher(t)
58 51
59- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())52+ handler := NewHandler(registrySvc, testLogger())
60 53
61 manifestData := []byte(`{"schemaVersion": 2}`)54 manifestData := []byte(`{"schemaVersion": 2}`)
62 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{55 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{
@@ -78,10 +71,8 @@ func TestHandler_GetManifest_Success(t *testing.T) {
78 71
79 func TestHandler_GetManifest_HEAD(t *testing.T) {72 func TestHandler_GetManifest_HEAD(t *testing.T) {
80 registrySvc := inmocks.NewMockRegistryService(t)73 registrySvc := inmocks.NewMockRegistryService(t)
81- blobStorage := mocks.NewMockBlobStorage(t)
82- eventBus := mocks.NewMockEventPublisher(t)
83 74
84- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())75+ handler := NewHandler(registrySvc, testLogger())
85 76
86 manifestData := []byte(`{"schemaVersion": 2}`)77 manifestData := []byte(`{"schemaVersion": 2}`)
87 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{78 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "latest").Return(&domain.Manifest{
@@ -104,10 +95,8 @@ func TestHandler_GetManifest_HEAD(t *testing.T) {
104 95
105 func TestHandler_GetManifest_NotFound(t *testing.T) {96 func TestHandler_GetManifest_NotFound(t *testing.T) {
106 registrySvc := inmocks.NewMockRegistryService(t)97 registrySvc := inmocks.NewMockRegistryService(t)
107- blobStorage := mocks.NewMockBlobStorage(t)
108- eventBus := mocks.NewMockEventPublisher(t)
109 98
110- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())99+ handler := NewHandler(registrySvc, testLogger())
111 100
112 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError)101 registrySvc.EXPECT().GetManifest(mock.Anything, "myapp", "notexists").Return(nil, assert.AnError)
113 102
@@ -121,10 +110,8 @@ func TestHandler_GetManifest_NotFound(t *testing.T) {
121 110
122 func TestHandler_GetManifest_NestedName(t *testing.T) {111 func TestHandler_GetManifest_NestedName(t *testing.T) {
123 registrySvc := inmocks.NewMockRegistryService(t)112 registrySvc := inmocks.NewMockRegistryService(t)
124- blobStorage := mocks.NewMockBlobStorage(t)
125- eventBus := mocks.NewMockEventPublisher(t)
126 113
127- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())114+ handler := NewHandler(registrySvc, testLogger())
128 115
129 manifestData := []byte(`{"schemaVersion": 2}`)116 manifestData := []byte(`{"schemaVersion": 2}`)
130 registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{117 registrySvc.EXPECT().GetManifest(mock.Anything, "org/project/app", "v1.0").Return(&domain.Manifest{
@@ -144,15 +131,13 @@ func TestHandler_GetManifest_NestedName(t *testing.T) {
144 131
145 func TestHandler_PutManifest_Success(t *testing.T) {132 func TestHandler_PutManifest_Success(t *testing.T) {
146 registrySvc := inmocks.NewMockRegistryService(t)133 registrySvc := inmocks.NewMockRegistryService(t)
147- blobStorage := mocks.NewMockBlobStorage(t)
148- eventBus := mocks.NewMockEventPublisher(t)
149 134
150- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())135+ handler := NewHandler(registrySvc, testLogger())
151 136
152 manifestData := []byte(`{"schemaVersion": 2}`)137 manifestData := []byte(`{"schemaVersion": 2}`)
153 registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool {138 registrySvc.EXPECT().PutManifest(mock.Anything, mock.MatchedBy(func(m *domain.Manifest) bool {
154 return m.Name == "myapp" && m.Reference == "latest" && m.ContentType == "application/vnd.docker.distribution.manifest.v2+json"139 return m.Name == "myapp" && m.Reference == "latest" && m.ContentType == "application/vnd.docker.distribution.manifest.v2+json"
155- })).Return(nil)140+ })).Return("sha256:abc123", nil)
156 141
157 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))142 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
158 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")143 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
@@ -161,16 +146,14 @@ func TestHandler_PutManifest_Success(t *testing.T) {
161 handler.ServeHTTP(rec, req)146 handler.ServeHTTP(rec, req)
162 147
163 assert.Equal(t, http.StatusCreated, rec.Code)148 assert.Equal(t, http.StatusCreated, rec.Code)
164- assert.NotEmpty(t, rec.Header().Get("Docker-Content-Digest"))149+ assert.Equal(t, "sha256:abc123", rec.Header().Get("Docker-Content-Digest"))
165 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/manifests/latest")150 assert.Contains(t, rec.Header().Get("Location"), "/v2/myapp/manifests/latest")
166 }151 }
167 152
168 func TestHandler_PutManifest_MissingContentType(t *testing.T) {153 func TestHandler_PutManifest_MissingContentType(t *testing.T) {
169 registrySvc := inmocks.NewMockRegistryService(t)154 registrySvc := inmocks.NewMockRegistryService(t)
170- blobStorage := mocks.NewMockBlobStorage(t)
171- eventBus := mocks.NewMockEventPublisher(t)
172 155
173- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())156+ handler := NewHandler(registrySvc, testLogger())
174 157
175 manifestData := []byte(`{"schemaVersion": 2}`)158 manifestData := []byte(`{"schemaVersion": 2}`)
176 159
@@ -185,13 +168,11 @@ func TestHandler_PutManifest_MissingContentType(t *testing.T) {
185 168
186 func TestHandler_PutManifest_StorageError(t *testing.T) {169 func TestHandler_PutManifest_StorageError(t *testing.T) {
187 registrySvc := inmocks.NewMockRegistryService(t)170 registrySvc := inmocks.NewMockRegistryService(t)
188- blobStorage := mocks.NewMockBlobStorage(t)
189- eventBus := mocks.NewMockEventPublisher(t)
190 171
191- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())172+ handler := NewHandler(registrySvc, testLogger())
192 173
193 manifestData := []byte(`{"schemaVersion": 2}`)174 manifestData := []byte(`{"schemaVersion": 2}`)
194- registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return(assert.AnError)175+ registrySvc.EXPECT().PutManifest(mock.Anything, mock.Anything).Return("", assert.AnError)
195 176
196 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))177 req := httptest.NewRequest("PUT", "/v2/myapp/manifests/latest", bytes.NewReader(manifestData))
197 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")178 req.Header.Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
@@ -204,10 +185,8 @@ func TestHandler_PutManifest_StorageError(t *testing.T) {
204 185
205 func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) {186 func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) {
206 registrySvc := inmocks.NewMockRegistryService(t)187 registrySvc := inmocks.NewMockRegistryService(t)
207- blobStorage := mocks.NewMockBlobStorage(t)
208- eventBus := mocks.NewMockEventPublisher(t)
209 188
210- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())189+ handler := NewHandler(registrySvc, testLogger())
211 190
212 req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil)191 req := httptest.NewRequest("DELETE", "/v2/myapp/manifests/latest", nil)
213 rec := httptest.NewRecorder()192 rec := httptest.NewRecorder()
@@ -219,10 +198,8 @@ func TestHandler_ManifestRoutes_MethodNotAllowed(t *testing.T) {
219 198
220 func TestHandler_GetBlob_Success(t *testing.T) {199 func TestHandler_GetBlob_Success(t *testing.T) {
221 registrySvc := inmocks.NewMockRegistryService(t)200 registrySvc := inmocks.NewMockRegistryService(t)
222- blobStorage := mocks.NewMockBlobStorage(t)
223- eventBus := mocks.NewMockEventPublisher(t)
224 201
225- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())202+ handler := NewHandler(registrySvc, testLogger())
226 203
227 // Create a temp file for http.ServeFile204 // Create a temp file for http.ServeFile
228 tmpFile, err := os.CreateTemp("", "blob-*")205 tmpFile, err := os.CreateTemp("", "blob-*")
@@ -233,7 +210,7 @@ func TestHandler_GetBlob_Success(t *testing.T) {
233 _, err = tmpFile.Write(blobContent)210 _, err = tmpFile.Write(blobContent)
234 assert.NoError(t, err)211 assert.NoError(t, err)
235 212
236- blobStorage.EXPECT().GetBlobPath("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(tmpFile.Name(), nil)213+ registrySvc.EXPECT().GetBlobPath(mock.Anything, "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(tmpFile.Name(), nil)
237 214
238 req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)215 req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)
239 rec := httptest.NewRecorder()216 rec := httptest.NewRecorder()
@@ -246,12 +223,10 @@ func TestHandler_GetBlob_Success(t *testing.T) {
246 223
247 func TestHandler_GetBlob_NotFound(t *testing.T) {224 func TestHandler_GetBlob_NotFound(t *testing.T) {
248 registrySvc := inmocks.NewMockRegistryService(t)225 registrySvc := inmocks.NewMockRegistryService(t)
249- blobStorage := mocks.NewMockBlobStorage(t)
250- eventBus := mocks.NewMockEventPublisher(t)
251 226
252- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())227+ handler := NewHandler(registrySvc, testLogger())
253 228
254- blobStorage.EXPECT().GetBlobPath("sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError)229+ registrySvc.EXPECT().GetBlobPath(mock.Anything, "sha256:0000000000000000000000000000000000000000000000000000000000000000").Return("", assert.AnError)
255 230
256 req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:0000000000000000000000000000000000000000000000000000000000000000", nil)231 req := httptest.NewRequest("GET", "/v2/myapp/blobs/sha256:0000000000000000000000000000000000000000000000000000000000000000", nil)
257 rec := httptest.NewRecorder()232 rec := httptest.NewRecorder()
@@ -263,10 +238,8 @@ func TestHandler_GetBlob_NotFound(t *testing.T) {
263 238
264 func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) {239 func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) {
265 registrySvc := inmocks.NewMockRegistryService(t)240 registrySvc := inmocks.NewMockRegistryService(t)
266- blobStorage := mocks.NewMockBlobStorage(t)
267- eventBus := mocks.NewMockEventPublisher(t)
268 241
269- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())242+ handler := NewHandler(registrySvc, testLogger())
270 243
271 req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)244 req := httptest.NewRequest("POST", "/v2/myapp/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", nil)
272 rec := httptest.NewRecorder()245 rec := httptest.NewRecorder()
@@ -278,10 +251,8 @@ func TestHandler_BlobRoutes_MethodNotAllowed(t *testing.T) {
278 251
279 func TestHandler_StartBlobUpload_Success(t *testing.T) {252 func TestHandler_StartBlobUpload_Success(t *testing.T) {
280 registrySvc := inmocks.NewMockRegistryService(t)253 registrySvc := inmocks.NewMockRegistryService(t)
281- blobStorage := mocks.NewMockBlobStorage(t)
282- eventBus := mocks.NewMockEventPublisher(t)
283 254
284- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())255+ handler := NewHandler(registrySvc, testLogger())
285 256
286 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("1234567890-myapp", nil)257 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("1234567890-myapp", nil)
287 258
@@ -297,10 +268,8 @@ func TestHandler_StartBlobUpload_Success(t *testing.T) {
297 268
298 func TestHandler_StartBlobUpload_Error(t *testing.T) {269 func TestHandler_StartBlobUpload_Error(t *testing.T) {
299 registrySvc := inmocks.NewMockRegistryService(t)270 registrySvc := inmocks.NewMockRegistryService(t)
300- blobStorage := mocks.NewMockBlobStorage(t)
301- eventBus := mocks.NewMockEventPublisher(t)
302 271
303- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())272+ handler := NewHandler(registrySvc, testLogger())
304 273
305 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError)274 registrySvc.EXPECT().StartUpload(mock.Anything, "myapp").Return("", assert.AnError)
306 275
@@ -314,13 +283,11 @@ func TestHandler_StartBlobUpload_Error(t *testing.T) {
314 283
315 func TestHandler_BlobUpload_PATCH(t *testing.T) {284 func TestHandler_BlobUpload_PATCH(t *testing.T) {
316 registrySvc := inmocks.NewMockRegistryService(t)285 registrySvc := inmocks.NewMockRegistryService(t)
317- blobStorage := mocks.NewMockBlobStorage(t)
318- eventBus := mocks.NewMockEventPublisher(t)
319 286
320- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())287+ handler := NewHandler(registrySvc, testLogger())
321 288
322 chunkData := []byte("chunk content")289 chunkData := []byte("chunk content")
323- blobStorage.EXPECT().AppendBlobChunk("myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)290+ registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)
324 291
325 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/1234567890-myapp", bytes.NewReader(chunkData))292 req := httptest.NewRequest("PATCH", "/v2/myapp/blobs/uploads/1234567890-myapp", bytes.NewReader(chunkData))
326 rec := httptest.NewRecorder()293 rec := httptest.NewRecorder()
@@ -333,13 +300,11 @@ func TestHandler_BlobUpload_PATCH(t *testing.T) {
333 300
334 func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) {301 func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) {
335 registrySvc := inmocks.NewMockRegistryService(t)302 registrySvc := inmocks.NewMockRegistryService(t)
336- blobStorage := mocks.NewMockBlobStorage(t)
337- eventBus := mocks.NewMockEventPublisher(t)
338 303
339- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())304+ handler := NewHandler(registrySvc, testLogger())
340 305
341 chunkData := []byte("final chunk")306 chunkData := []byte("final chunk")
342- blobStorage.EXPECT().AppendBlobChunk("myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)307+ registrySvc.EXPECT().AppendBlobChunk(mock.Anything, "myapp", "1234567890-myapp", chunkData).Return(int64(len(chunkData)), nil)
343 registrySvc.EXPECT().FinishUpload(mock.Anything, "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)308 registrySvc.EXPECT().FinishUpload(mock.Anything, "1234567890-myapp", "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4").Return(nil)
344 309
345 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData))310 req := httptest.NewRequest("PUT", "/v2/myapp/blobs/uploads/1234567890-myapp?digest=sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", bytes.NewReader(chunkData))
@@ -354,10 +319,8 @@ func TestHandler_BlobUpload_PUT_Finalize(t *testing.T) {
354 319
355 func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) {320 func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) {
356 registrySvc := inmocks.NewMockRegistryService(t)321 registrySvc := inmocks.NewMockRegistryService(t)
357- blobStorage := mocks.NewMockBlobStorage(t)
358- eventBus := mocks.NewMockEventPublisher(t)
359 322
360- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())323+ handler := NewHandler(registrySvc, testLogger())
361 324
362 chunkData := []byte("final chunk")325 chunkData := []byte("final chunk")
363 // Note: Invalid digest format is rejected by validation before reaching storage layer326 // Note: Invalid digest format is rejected by validation before reaching storage layer
@@ -373,10 +336,8 @@ func TestHandler_BlobUpload_PUT_DigestMismatch(t *testing.T) {
373 336
374 func TestHandler_ListTags_Success(t *testing.T) {337 func TestHandler_ListTags_Success(t *testing.T) {
375 registrySvc := inmocks.NewMockRegistryService(t)338 registrySvc := inmocks.NewMockRegistryService(t)
376- blobStorage := mocks.NewMockBlobStorage(t)
377- eventBus := mocks.NewMockEventPublisher(t)
378 339
379- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())340+ handler := NewHandler(registrySvc, testLogger())
380 341
381 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil)342 registrySvc.EXPECT().ListTags(mock.Anything, "myapp").Return([]string{"latest", "v1.0", "v2.0"}, nil)
382 343
@@ -393,10 +354,8 @@ func TestHandler_ListTags_Success(t *testing.T) {
393 354
394 func TestHandler_ListTags_NotFound(t *testing.T) {355 func TestHandler_ListTags_NotFound(t *testing.T) {
395 registrySvc := inmocks.NewMockRegistryService(t)356 registrySvc := inmocks.NewMockRegistryService(t)
396- blobStorage := mocks.NewMockBlobStorage(t)
397- eventBus := mocks.NewMockEventPublisher(t)
398 357
399- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())358+ handler := NewHandler(registrySvc, testLogger())
400 359
401 registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError)360 registrySvc.EXPECT().ListTags(mock.Anything, "notexists").Return(nil, assert.AnError)
402 361
@@ -410,10 +369,8 @@ func TestHandler_ListTags_NotFound(t *testing.T) {
410 369
411 func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {370 func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {
412 registrySvc := inmocks.NewMockRegistryService(t)371 registrySvc := inmocks.NewMockRegistryService(t)
413- blobStorage := mocks.NewMockBlobStorage(t)
414- eventBus := mocks.NewMockEventPublisher(t)
415 372
416- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())373+ handler := NewHandler(registrySvc, testLogger())
417 374
418 req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil)375 req := httptest.NewRequest("POST", "/v2/myapp/tags/list", nil)
419 rec := httptest.NewRecorder()376 rec := httptest.NewRecorder()
@@ -425,10 +382,8 @@ func TestHandler_ListTags_MethodNotAllowed(t *testing.T) {
425 382
426 func TestHandler_RegisterRoutes(t *testing.T) {383 func TestHandler_RegisterRoutes(t *testing.T) {
427 registrySvc := inmocks.NewMockRegistryService(t)384 registrySvc := inmocks.NewMockRegistryService(t)
428- blobStorage := mocks.NewMockBlobStorage(t)
429- eventBus := mocks.NewMockEventPublisher(t)
430 385
431- handler := NewHandler(registrySvc, blobStorage, eventBus, testLogger())386+ handler := NewHandler(registrySvc, testLogger())
432 387
433 mux := http.NewServeMux()388 mux := http.NewServeMux()
434 handler.RegisterRoutes(mux)389 handler.RegisterRoutes(mux)
M internal/app/run.go +1 -1
@@ -772,7 +772,7 @@ func syncAndAutoStart(ctx context.Context, svc *services, log zerowrap.Logger) {
772772
773773 // createHTTPHandlers creates HTTP handlers with middleware.
774774 func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler) {
775- registryHandler := registry.NewHandler(svc.registrySvc, svc.blobStorage, svc.eventBus, log)
775+ registryHandler := registry.NewHandler(svc.registrySvc, log)
776776
777777 registryMiddlewares := []func(http.Handler) http.Handler{
778778 middleware.PanicRecovery(log),
@@ -772,7 +772,7 @@ func syncAndAutoStart(ctx context.Context, svc *services, log zerowrap.Logger) {
772 772
773 // createHTTPHandlers creates HTTP handlers with middleware.773 // createHTTPHandlers creates HTTP handlers with middleware.
774 func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler) {774 func createHTTPHandlers(svc *services, cfg Config, log zerowrap.Logger) (http.Handler, http.Handler) {
775- registryHandler := registry.NewHandler(svc.registrySvc, svc.blobStorage, svc.eventBus, log)775+ registryHandler := registry.NewHandler(svc.registrySvc, log)
776 776
777 registryMiddlewares := []func(http.Handler) http.Handler{777 registryMiddlewares := []func(http.Handler) http.Handler{
778 middleware.PanicRecovery(log),778 middleware.PanicRecovery(log),
M internal/boundaries/in/config.go +3 -0
@@ -14,6 +14,9 @@ type ConfigService interface {
1414 // GetRoutes returns all configured routes.
1515 GetRoutes(ctx context.Context) []domain.Route
1616
17+ // GetRoute returns a single route by domain.
18+ GetRoute(ctx context.Context, domain string) (*domain.Route, error)
19+
1720 // AddRoute adds a new route to the configuration.
1821 AddRoute(ctx context.Context, route domain.Route) error
1922
@@ -14,6 +14,9 @@ type ConfigService interface {
14 // GetRoutes returns all configured routes.14 // GetRoutes returns all configured routes.
15 GetRoutes(ctx context.Context) []domain.Route15 GetRoutes(ctx context.Context) []domain.Route
16 16
17+ // GetRoute returns a single route by domain.
18+ GetRoute(ctx context.Context, domain string) (*domain.Route, error)
19+
17 // AddRoute adds a new route to the configuration.20 // AddRoute adds a new route to the configuration.
18 AddRoute(ctx context.Context, route domain.Route) error21 AddRoute(ctx context.Context, route domain.Route) error
19 22
M internal/boundaries/in/mocks/mock_config_service.go +68 -0
@@ -317,6 +317,74 @@ func (_c *MockConfigService_GetRegistryPort_Call) RunAndReturn(run func() int) *
317317 return _c
318318 }
319319
320+// GetRoute provides a mock function for the type MockConfigService
321+func (_mock *MockConfigService) GetRoute(ctx context.Context, domain1 string) (*domain.Route, error) {
322+ ret := _mock.Called(ctx, domain1)
323+
324+ if len(ret) == 0 {
325+ panic("no return value specified for GetRoute")
326+ }
327+
328+ var r0 *domain.Route
329+ var r1 error
330+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*domain.Route, error)); ok {
331+ return returnFunc(ctx, domain1)
332+ }
333+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) *domain.Route); ok {
334+ r0 = returnFunc(ctx, domain1)
335+ } else {
336+ if ret.Get(0) != nil {
337+ r0 = ret.Get(0).(*domain.Route)
338+ }
339+ }
340+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
341+ r1 = returnFunc(ctx, domain1)
342+ } else {
343+ r1 = ret.Error(1)
344+ }
345+ return r0, r1
346+}
347+
348+// MockConfigService_GetRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRoute'
349+type MockConfigService_GetRoute_Call struct {
350+ *mock.Call
351+}
352+
353+// GetRoute is a helper method to define mock.On call
354+// - ctx context.Context
355+// - domain1 string
356+func (_e *MockConfigService_Expecter) GetRoute(ctx interface{}, domain1 interface{}) *MockConfigService_GetRoute_Call {
357+ return &MockConfigService_GetRoute_Call{Call: _e.mock.On("GetRoute", ctx, domain1)}
358+}
359+
360+func (_c *MockConfigService_GetRoute_Call) Run(run func(ctx context.Context, domain1 string)) *MockConfigService_GetRoute_Call {
361+ _c.Call.Run(func(args mock.Arguments) {
362+ var arg0 context.Context
363+ if args[0] != nil {
364+ arg0 = args[0].(context.Context)
365+ }
366+ var arg1 string
367+ if args[1] != nil {
368+ arg1 = args[1].(string)
369+ }
370+ run(
371+ arg0,
372+ arg1,
373+ )
374+ })
375+ return _c
376+}
377+
378+func (_c *MockConfigService_GetRoute_Call) Return(route *domain.Route, err error) *MockConfigService_GetRoute_Call {
379+ _c.Call.Return(route, err)
380+ return _c
381+}
382+
383+func (_c *MockConfigService_GetRoute_Call) RunAndReturn(run func(ctx context.Context, domain1 string) (*domain.Route, error)) *MockConfigService_GetRoute_Call {
384+ _c.Call.Return(run)
385+ return _c
386+}
387+
320388 // GetRoutes provides a mock function for the type MockConfigService
321389 func (_mock *MockConfigService) GetRoutes(ctx context.Context) []domain.Route {
322390 ret := _mock.Called(ctx)
@@ -317,6 +317,74 @@ func (_c *MockConfigService_GetRegistryPort_Call) RunAndReturn(run func() int) *
317 return _c317 return _c
318 }318 }
319 319
320+// GetRoute provides a mock function for the type MockConfigService
321+func (_mock *MockConfigService) GetRoute(ctx context.Context, domain1 string) (*domain.Route, error) {
322+ ret := _mock.Called(ctx, domain1)
323+
324+ if len(ret) == 0 {
325+ panic("no return value specified for GetRoute")
326+ }
327+
328+ var r0 *domain.Route
329+ var r1 error
330+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*domain.Route, error)); ok {
331+ return returnFunc(ctx, domain1)
332+ }
333+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) *domain.Route); ok {
334+ r0 = returnFunc(ctx, domain1)
335+ } else {
336+ if ret.Get(0) != nil {
337+ r0 = ret.Get(0).(*domain.Route)
338+ }
339+ }
340+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
341+ r1 = returnFunc(ctx, domain1)
342+ } else {
343+ r1 = ret.Error(1)
344+ }
345+ return r0, r1
346+}
347+
348+// MockConfigService_GetRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRoute'
349+type MockConfigService_GetRoute_Call struct {
350+ *mock.Call
351+}
352+
353+// GetRoute is a helper method to define mock.On call
354+// - ctx context.Context
355+// - domain1 string
356+func (_e *MockConfigService_Expecter) GetRoute(ctx interface{}, domain1 interface{}) *MockConfigService_GetRoute_Call {
357+ return &MockConfigService_GetRoute_Call{Call: _e.mock.On("GetRoute", ctx, domain1)}
358+}
359+
360+func (_c *MockConfigService_GetRoute_Call) Run(run func(ctx context.Context, domain1 string)) *MockConfigService_GetRoute_Call {
361+ _c.Call.Run(func(args mock.Arguments) {
362+ var arg0 context.Context
363+ if args[0] != nil {
364+ arg0 = args[0].(context.Context)
365+ }
366+ var arg1 string
367+ if args[1] != nil {
368+ arg1 = args[1].(string)
369+ }
370+ run(
371+ arg0,
372+ arg1,
373+ )
374+ })
375+ return _c
376+}
377+
378+func (_c *MockConfigService_GetRoute_Call) Return(route *domain.Route, err error) *MockConfigService_GetRoute_Call {
379+ _c.Call.Return(route, err)
380+ return _c
381+}
382+
383+func (_c *MockConfigService_GetRoute_Call) RunAndReturn(run func(ctx context.Context, domain1 string) (*domain.Route, error)) *MockConfigService_GetRoute_Call {
384+ _c.Call.Return(run)
385+ return _c
386+}
387+
320 // GetRoutes provides a mock function for the type MockConfigService388 // GetRoutes provides a mock function for the type MockConfigService
321 func (_mock *MockConfigService) GetRoutes(ctx context.Context) []domain.Route {389 func (_mock *MockConfigService) GetRoutes(ctx context.Context) []domain.Route {
322 ret := _mock.Called(ctx)390 ret := _mock.Called(ctx)
M internal/boundaries/in/mocks/mock_registry_service.go +161 -8
@@ -39,6 +39,84 @@ func (_m *MockRegistryService) EXPECT() *MockRegistryService_Expecter {
3939 return &MockRegistryService_Expecter{mock: &_m.Mock}
4040 }
4141
42+// AppendBlobChunk provides a mock function for the type MockRegistryService
43+func (_mock *MockRegistryService) AppendBlobChunk(ctx context.Context, name string, uuid string, chunk []byte) (int64, error) {
44+ ret := _mock.Called(ctx, name, uuid, chunk)
45+
46+ if len(ret) == 0 {
47+ panic("no return value specified for AppendBlobChunk")
48+ }
49+
50+ var r0 int64
51+ var r1 error
52+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []byte) (int64, error)); ok {
53+ return returnFunc(ctx, name, uuid, chunk)
54+ }
55+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []byte) int64); ok {
56+ r0 = returnFunc(ctx, name, uuid, chunk)
57+ } else {
58+ r0 = ret.Get(0).(int64)
59+ }
60+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, []byte) error); ok {
61+ r1 = returnFunc(ctx, name, uuid, chunk)
62+ } else {
63+ r1 = ret.Error(1)
64+ }
65+ return r0, r1
66+}
67+
68+// MockRegistryService_AppendBlobChunk_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppendBlobChunk'
69+type MockRegistryService_AppendBlobChunk_Call struct {
70+ *mock.Call
71+}
72+
73+// AppendBlobChunk is a helper method to define mock.On call
74+// - ctx context.Context
75+// - name string
76+// - uuid string
77+// - chunk []byte
78+func (_e *MockRegistryService_Expecter) AppendBlobChunk(ctx interface{}, name interface{}, uuid interface{}, chunk interface{}) *MockRegistryService_AppendBlobChunk_Call {
79+ return &MockRegistryService_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", ctx, name, uuid, chunk)}
80+}
81+
82+func (_c *MockRegistryService_AppendBlobChunk_Call) Run(run func(ctx context.Context, name string, uuid string, chunk []byte)) *MockRegistryService_AppendBlobChunk_Call {
83+ _c.Call.Run(func(args mock.Arguments) {
84+ var arg0 context.Context
85+ if args[0] != nil {
86+ arg0 = args[0].(context.Context)
87+ }
88+ var arg1 string
89+ if args[1] != nil {
90+ arg1 = args[1].(string)
91+ }
92+ var arg2 string
93+ if args[2] != nil {
94+ arg2 = args[2].(string)
95+ }
96+ var arg3 []byte
97+ if args[3] != nil {
98+ arg3 = args[3].([]byte)
99+ }
100+ run(
101+ arg0,
102+ arg1,
103+ arg2,
104+ arg3,
105+ )
106+ })
107+ return _c
108+}
109+
110+func (_c *MockRegistryService_AppendBlobChunk_Call) Return(n int64, err error) *MockRegistryService_AppendBlobChunk_Call {
111+ _c.Call.Return(n, err)
112+ return _c
113+}
114+
115+func (_c *MockRegistryService_AppendBlobChunk_Call) RunAndReturn(run func(ctx context.Context, name string, uuid string, chunk []byte) (int64, error)) *MockRegistryService_AppendBlobChunk_Call {
116+ _c.Call.Return(run)
117+ return _c
118+}
119+
42120 // BlobExists provides a mock function for the type MockRegistryService
43121 func (_mock *MockRegistryService) BlobExists(ctx context.Context, digest string) bool {
44122 ret := _mock.Called(ctx, digest)
@@ -347,6 +425,72 @@ func (_c *MockRegistryService_GetBlob_Call) RunAndReturn(run func(ctx context.Co
347425 return _c
348426 }
349427
428+// GetBlobPath provides a mock function for the type MockRegistryService
429+func (_mock *MockRegistryService) GetBlobPath(ctx context.Context, digest string) (string, error) {
430+ ret := _mock.Called(ctx, digest)
431+
432+ if len(ret) == 0 {
433+ panic("no return value specified for GetBlobPath")
434+ }
435+
436+ var r0 string
437+ var r1 error
438+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok {
439+ return returnFunc(ctx, digest)
440+ }
441+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok {
442+ r0 = returnFunc(ctx, digest)
443+ } else {
444+ r0 = ret.Get(0).(string)
445+ }
446+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
447+ r1 = returnFunc(ctx, digest)
448+ } else {
449+ r1 = ret.Error(1)
450+ }
451+ return r0, r1
452+}
453+
454+// MockRegistryService_GetBlobPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlobPath'
455+type MockRegistryService_GetBlobPath_Call struct {
456+ *mock.Call
457+}
458+
459+// GetBlobPath is a helper method to define mock.On call
460+// - ctx context.Context
461+// - digest string
462+func (_e *MockRegistryService_Expecter) GetBlobPath(ctx interface{}, digest interface{}) *MockRegistryService_GetBlobPath_Call {
463+ return &MockRegistryService_GetBlobPath_Call{Call: _e.mock.On("GetBlobPath", ctx, digest)}
464+}
465+
466+func (_c *MockRegistryService_GetBlobPath_Call) Run(run func(ctx context.Context, digest string)) *MockRegistryService_GetBlobPath_Call {
467+ _c.Call.Run(func(args mock.Arguments) {
468+ var arg0 context.Context
469+ if args[0] != nil {
470+ arg0 = args[0].(context.Context)
471+ }
472+ var arg1 string
473+ if args[1] != nil {
474+ arg1 = args[1].(string)
475+ }
476+ run(
477+ arg0,
478+ arg1,
479+ )
480+ })
481+ return _c
482+}
483+
484+func (_c *MockRegistryService_GetBlobPath_Call) Return(s string, err error) *MockRegistryService_GetBlobPath_Call {
485+ _c.Call.Return(s, err)
486+ return _c
487+}
488+
489+func (_c *MockRegistryService_GetBlobPath_Call) RunAndReturn(run func(ctx context.Context, digest string) (string, error)) *MockRegistryService_GetBlobPath_Call {
490+ _c.Call.Return(run)
491+ return _c
492+}
493+
350494 // GetManifest provides a mock function for the type MockRegistryService
351495 func (_mock *MockRegistryService) GetManifest(ctx context.Context, name string, reference string) (*domain.Manifest, error) {
352496 ret := _mock.Called(ctx, name, reference)
@@ -621,20 +765,29 @@ func (_c *MockRegistryService_PutBlob_Call) RunAndReturn(run func(ctx context.Co
621765 }
622766
623767 // PutManifest provides a mock function for the type MockRegistryService
624-func (_mock *MockRegistryService) PutManifest(ctx context.Context, manifest *domain.Manifest) error {
768+func (_mock *MockRegistryService) PutManifest(ctx context.Context, manifest *domain.Manifest) (string, error) {
625769 ret := _mock.Called(ctx, manifest)
626770
627771 if len(ret) == 0 {
628772 panic("no return value specified for PutManifest")
629773 }
630774
631- var r0 error
632- if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.Manifest) error); ok {
775+ var r0 string
776+ var r1 error
777+ if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.Manifest) (string, error)); ok {
778+ return returnFunc(ctx, manifest)
779+ }
780+ if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.Manifest) string); ok {
633781 r0 = returnFunc(ctx, manifest)
634782 } else {
635- r0 = ret.Error(0)
783+ r0 = ret.Get(0).(string)
636784 }
637- return r0
785+ if returnFunc, ok := ret.Get(1).(func(context.Context, *domain.Manifest) error); ok {
786+ r1 = returnFunc(ctx, manifest)
787+ } else {
788+ r1 = ret.Error(1)
789+ }
790+ return r0, r1
638791 }
639792
640793 // MockRegistryService_PutManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutManifest'
@@ -667,12 +820,12 @@ func (_c *MockRegistryService_PutManifest_Call) Run(run func(ctx context.Context
667820 return _c
668821 }
669822
670-func (_c *MockRegistryService_PutManifest_Call) Return(err error) *MockRegistryService_PutManifest_Call {
671- _c.Call.Return(err)
823+func (_c *MockRegistryService_PutManifest_Call) Return(digest string, err error) *MockRegistryService_PutManifest_Call {
824+ _c.Call.Return(digest, err)
672825 return _c
673826 }
674827
675-func (_c *MockRegistryService_PutManifest_Call) RunAndReturn(run func(ctx context.Context, manifest *domain.Manifest) error) *MockRegistryService_PutManifest_Call {
828+func (_c *MockRegistryService_PutManifest_Call) RunAndReturn(run func(ctx context.Context, manifest *domain.Manifest) (string, error)) *MockRegistryService_PutManifest_Call {
676829 _c.Call.Return(run)
677830 return _c
678831 }
@@ -39,6 +39,84 @@ func (_m *MockRegistryService) EXPECT() *MockRegistryService_Expecter {
39 return &MockRegistryService_Expecter{mock: &_m.Mock}39 return &MockRegistryService_Expecter{mock: &_m.Mock}
40 }40 }
41 41
42+// AppendBlobChunk provides a mock function for the type MockRegistryService
43+func (_mock *MockRegistryService) AppendBlobChunk(ctx context.Context, name string, uuid string, chunk []byte) (int64, error) {
44+ ret := _mock.Called(ctx, name, uuid, chunk)
45+
46+ if len(ret) == 0 {
47+ panic("no return value specified for AppendBlobChunk")
48+ }
49+
50+ var r0 int64
51+ var r1 error
52+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []byte) (int64, error)); ok {
53+ return returnFunc(ctx, name, uuid, chunk)
54+ }
55+ if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []byte) int64); ok {
56+ r0 = returnFunc(ctx, name, uuid, chunk)
57+ } else {
58+ r0 = ret.Get(0).(int64)
59+ }
60+ if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, []byte) error); ok {
61+ r1 = returnFunc(ctx, name, uuid, chunk)
62+ } else {
63+ r1 = ret.Error(1)
64+ }
65+ return r0, r1
66+}
67+
68+// MockRegistryService_AppendBlobChunk_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AppendBlobChunk'
69+type MockRegistryService_AppendBlobChunk_Call struct {
70+ *mock.Call
71+}
72+
73+// AppendBlobChunk is a helper method to define mock.On call
74+// - ctx context.Context
75+// - name string
76+// - uuid string
77+// - chunk []byte
78+func (_e *MockRegistryService_Expecter) AppendBlobChunk(ctx interface{}, name interface{}, uuid interface{}, chunk interface{}) *MockRegistryService_AppendBlobChunk_Call {
79+ return &MockRegistryService_AppendBlobChunk_Call{Call: _e.mock.On("AppendBlobChunk", ctx, name, uuid, chunk)}
80+}
81+
82+func (_c *MockRegistryService_AppendBlobChunk_Call) Run(run func(ctx context.Context, name string, uuid string, chunk []byte)) *MockRegistryService_AppendBlobChunk_Call {
83+ _c.Call.Run(func(args mock.Arguments) {
84+ var arg0 context.Context
85+ if args[0] != nil {
86+ arg0 = args[0].(context.Context)
87+ }
88+ var arg1 string
89+ if args[1] != nil {
90+ arg1 = args[1].(string)
91+ }
92+ var arg2 string
93+ if args[2] != nil {
94+ arg2 = args[2].(string)
95+ }
96+ var arg3 []byte
97+ if args[3] != nil {
98+ arg3 = args[3].([]byte)
99+ }
100+ run(
101+ arg0,
102+ arg1,
103+ arg2,
104+ arg3,
105+ )
106+ })
107+ return _c
108+}
109+
110+func (_c *MockRegistryService_AppendBlobChunk_Call) Return(n int64, err error) *MockRegistryService_AppendBlobChunk_Call {
111+ _c.Call.Return(n, err)
112+ return _c
113+}
114+
115+func (_c *MockRegistryService_AppendBlobChunk_Call) RunAndReturn(run func(ctx context.Context, name string, uuid string, chunk []byte) (int64, error)) *MockRegistryService_AppendBlobChunk_Call {
116+ _c.Call.Return(run)
117+ return _c
118+}
119+
42 // BlobExists provides a mock function for the type MockRegistryService120 // BlobExists provides a mock function for the type MockRegistryService
43 func (_mock *MockRegistryService) BlobExists(ctx context.Context, digest string) bool {121 func (_mock *MockRegistryService) BlobExists(ctx context.Context, digest string) bool {
44 ret := _mock.Called(ctx, digest)122 ret := _mock.Called(ctx, digest)
@@ -347,6 +425,72 @@ func (_c *MockRegistryService_GetBlob_Call) RunAndReturn(run func(ctx context.Co
347 return _c425 return _c
348 }426 }
349 427
428+// GetBlobPath provides a mock function for the type MockRegistryService
429+func (_mock *MockRegistryService) GetBlobPath(ctx context.Context, digest string) (string, error) {
430+ ret := _mock.Called(ctx, digest)
431+
432+ if len(ret) == 0 {
433+ panic("no return value specified for GetBlobPath")
434+ }
435+
436+ var r0 string
437+ var r1 error
438+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok {
439+ return returnFunc(ctx, digest)
440+ }
441+ if returnFunc, ok := ret.Get(0).(func(context.Context, string) string); ok {
442+ r0 = returnFunc(ctx, digest)
443+ } else {
444+ r0 = ret.Get(0).(string)
445+ }
446+ if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
447+ r1 = returnFunc(ctx, digest)
448+ } else {
449+ r1 = ret.Error(1)
450+ }
451+ return r0, r1
452+}
453+
454+// MockRegistryService_GetBlobPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlobPath'
455+type MockRegistryService_GetBlobPath_Call struct {
456+ *mock.Call
457+}
458+
459+// GetBlobPath is a helper method to define mock.On call
460+// - ctx context.Context
461+// - digest string
462+func (_e *MockRegistryService_Expecter) GetBlobPath(ctx interface{}, digest interface{}) *MockRegistryService_GetBlobPath_Call {
463+ return &MockRegistryService_GetBlobPath_Call{Call: _e.mock.On("GetBlobPath", ctx, digest)}
464+}
465+
466+func (_c *MockRegistryService_GetBlobPath_Call) Run(run func(ctx context.Context, digest string)) *MockRegistryService_GetBlobPath_Call {
467+ _c.Call.Run(func(args mock.Arguments) {
468+ var arg0 context.Context
469+ if args[0] != nil {
470+ arg0 = args[0].(context.Context)
471+ }
472+ var arg1 string
473+ if args[1] != nil {
474+ arg1 = args[1].(string)
475+ }
476+ run(
477+ arg0,
478+ arg1,
479+ )
480+ })
481+ return _c
482+}
483+
484+func (_c *MockRegistryService_GetBlobPath_Call) Return(s string, err error) *MockRegistryService_GetBlobPath_Call {
485+ _c.Call.Return(s, err)
486+ return _c
487+}
488+
489+func (_c *MockRegistryService_GetBlobPath_Call) RunAndReturn(run func(ctx context.Context, digest string) (string, error)) *MockRegistryService_GetBlobPath_Call {
490+ _c.Call.Return(run)
491+ return _c
492+}
493+
350 // GetManifest provides a mock function for the type MockRegistryService494 // GetManifest provides a mock function for the type MockRegistryService
351 func (_mock *MockRegistryService) GetManifest(ctx context.Context, name string, reference string) (*domain.Manifest, error) {495 func (_mock *MockRegistryService) GetManifest(ctx context.Context, name string, reference string) (*domain.Manifest, error) {
352 ret := _mock.Called(ctx, name, reference)496 ret := _mock.Called(ctx, name, reference)
@@ -621,20 +765,29 @@ func (_c *MockRegistryService_PutBlob_Call) RunAndReturn(run func(ctx context.Co
621 }765 }
622 766
623 // PutManifest provides a mock function for the type MockRegistryService767 // PutManifest provides a mock function for the type MockRegistryService
624-func (_mock *MockRegistryService) PutManifest(ctx context.Context, manifest *domain.Manifest) error {768+func (_mock *MockRegistryService) PutManifest(ctx context.Context, manifest *domain.Manifest) (string, error) {
625 ret := _mock.Called(ctx, manifest)769 ret := _mock.Called(ctx, manifest)
626 770
627 if len(ret) == 0 {771 if len(ret) == 0 {
628 panic("no return value specified for PutManifest")772 panic("no return value specified for PutManifest")
629 }773 }
630 774
631- var r0 error775+ var r0 string
632- if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.Manifest) error); ok {776+ var r1 error
777+ if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.Manifest) (string, error)); ok {
778+ return returnFunc(ctx, manifest)
779+ }
780+ if returnFunc, ok := ret.Get(0).(func(context.Context, *domain.Manifest) string); ok {
633 r0 = returnFunc(ctx, manifest)781 r0 = returnFunc(ctx, manifest)
634 } else {782 } else {
635- r0 = ret.Error(0)783+ r0 = ret.Get(0).(string)
636 }784 }
637- return r0785+ if returnFunc, ok := ret.Get(1).(func(context.Context, *domain.Manifest) error); ok {
786+ r1 = returnFunc(ctx, manifest)
787+ } else {
788+ r1 = ret.Error(1)
789+ }
790+ return r0, r1
638 }791 }
639 792
640 // MockRegistryService_PutManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutManifest'793 // MockRegistryService_PutManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutManifest'
@@ -667,12 +820,12 @@ func (_c *MockRegistryService_PutManifest_Call) Run(run func(ctx context.Context
667 return _c820 return _c
668 }821 }
669 822
670-func (_c *MockRegistryService_PutManifest_Call) Return(err error) *MockRegistryService_PutManifest_Call {823+func (_c *MockRegistryService_PutManifest_Call) Return(digest string, err error) *MockRegistryService_PutManifest_Call {
671- _c.Call.Return(err)824+ _c.Call.Return(digest, err)
672 return _c825 return _c
673 }826 }
674 827
675-func (_c *MockRegistryService_PutManifest_Call) RunAndReturn(run func(ctx context.Context, manifest *domain.Manifest) error) *MockRegistryService_PutManifest_Call {828+func (_c *MockRegistryService_PutManifest_Call) RunAndReturn(run func(ctx context.Context, manifest *domain.Manifest) (string, error)) *MockRegistryService_PutManifest_Call {
676 _c.Call.Return(run)829 _c.Call.Return(run)
677 return _c830 return _c
678 }831 }
M internal/boundaries/in/registry.go +3 -1
@@ -11,16 +11,18 @@ import (
1111 type RegistryService interface {
1212 // Manifest operations
1313 GetManifest(ctx context.Context, name, reference string) (*domain.Manifest, error)
14- PutManifest(ctx context.Context, manifest *domain.Manifest) error
14+ PutManifest(ctx context.Context, manifest *domain.Manifest) (digest string, err error)
1515 DeleteManifest(ctx context.Context, name, reference string) error
1616
1717 // Blob operations
1818 GetBlob(ctx context.Context, digest string) (io.ReadCloser, error)
19+ GetBlobPath(ctx context.Context, digest string) (string, error)
1920 PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error
2021 BlobExists(ctx context.Context, digest string) bool
2122
2223 // Upload operations
2324 StartUpload(ctx context.Context, name string) (string, error)
25+ AppendBlobChunk(ctx context.Context, name, uuid string, chunk []byte) (int64, error)
2426 FinishUpload(ctx context.Context, uuid, digest string) error
2527 CancelUpload(ctx context.Context, uuid string) error
2628
@@ -11,16 +11,18 @@ import (
11 type RegistryService interface {11 type RegistryService interface {
12 // Manifest operations12 // Manifest operations
13 GetManifest(ctx context.Context, name, reference string) (*domain.Manifest, error)13 GetManifest(ctx context.Context, name, reference string) (*domain.Manifest, error)
14- PutManifest(ctx context.Context, manifest *domain.Manifest) error14+ PutManifest(ctx context.Context, manifest *domain.Manifest) (digest string, err error)
15 DeleteManifest(ctx context.Context, name, reference string) error15 DeleteManifest(ctx context.Context, name, reference string) error
16 16
17 // Blob operations17 // Blob operations
18 GetBlob(ctx context.Context, digest string) (io.ReadCloser, error)18 GetBlob(ctx context.Context, digest string) (io.ReadCloser, error)
19+ GetBlobPath(ctx context.Context, digest string) (string, error)
19 PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error20 PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error
20 BlobExists(ctx context.Context, digest string) bool21 BlobExists(ctx context.Context, digest string) bool
21 22
22 // Upload operations23 // Upload operations
23 StartUpload(ctx context.Context, name string) (string, error)24 StartUpload(ctx context.Context, name string) (string, error)
25+ AppendBlobChunk(ctx context.Context, name, uuid string, chunk []byte) (int64, error)
24 FinishUpload(ctx context.Context, uuid, digest string) error26 FinishUpload(ctx context.Context, uuid, digest string) error
25 CancelUpload(ctx context.Context, uuid string) error27 CancelUpload(ctx context.Context, uuid string) error
26 28
M internal/domain/errors.go +2 -0
@@ -20,6 +20,8 @@ var (
2020 ErrRouteNotFound = errors.New("route not found")
2121 ErrRouteExists = errors.New("route already exists")
2222 ErrInvalidRoute = errors.New("invalid route configuration")
23+ ErrRouteDomainEmpty = errors.New("route domain cannot be empty")
24+ ErrRouteImageEmpty = errors.New("route image cannot be empty")
2325 ErrNoTargetAvailable = errors.New("no target available for route")
2426
2527 // Registry errors
@@ -20,6 +20,8 @@ var (
20 ErrRouteNotFound = errors.New("route not found")20 ErrRouteNotFound = errors.New("route not found")
21 ErrRouteExists = errors.New("route already exists")21 ErrRouteExists = errors.New("route already exists")
22 ErrInvalidRoute = errors.New("invalid route configuration")22 ErrInvalidRoute = errors.New("invalid route configuration")
23+ ErrRouteDomainEmpty = errors.New("route domain cannot be empty")
24+ ErrRouteImageEmpty = errors.New("route image cannot be empty")
23 ErrNoTargetAvailable = errors.New("no target available for route")25 ErrNoTargetAvailable = errors.New("no target available for route")
24 26
25 // Registry errors27 // Registry errors
M internal/usecase/config/service.go +41 -0
@@ -182,6 +182,31 @@ func (s *Service) GetRoutes(_ context.Context) []domain.Route {
182182 return routes
183183 }
184184
185+// GetRoute returns a single route by domain.
186+func (s *Service) GetRoute(_ context.Context, domainName string) (*domain.Route, error) {
187+ s.mu.RLock()
188+ defer s.mu.RUnlock()
189+
190+ image, exists := s.config.Routes[domainName]
191+ if !exists {
192+ return nil, domain.ErrRouteNotFound
193+ }
194+
195+ route := &domain.Route{
196+ Domain: domainName,
197+ Image: image,
198+ HTTPS: true,
199+ }
200+
201+ // Check if domain has http:// prefix
202+ if strings.HasPrefix(domainName, "http://") {
203+ route.Domain = strings.TrimPrefix(domainName, "http://")
204+ route.HTTPS = false
205+ }
206+
207+ return route, nil
208+}
209+
185210 // AddRoute adds a new route to the configuration and persists it.
186211 func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {
187212 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -191,6 +216,14 @@ func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {
191216 })
192217 log := zerowrap.FromCtx(ctx)
193218
219+ // Validate route
220+ if route.Domain == "" {
221+ return domain.ErrRouteDomainEmpty
222+ }
223+ if route.Image == "" {
224+ return domain.ErrRouteImageEmpty
225+ }
226+
194227 // Store previous value for rollback
195228 s.mu.Lock()
196229 previousImage, existed := s.config.Routes[route.Domain]
@@ -226,6 +259,14 @@ func (s *Service) UpdateRoute(ctx context.Context, route domain.Route) error {
226259 })
227260 log := zerowrap.FromCtx(ctx)
228261
262+ // Validate route
263+ if route.Domain == "" {
264+ return domain.ErrRouteDomainEmpty
265+ }
266+ if route.Image == "" {
267+ return domain.ErrRouteImageEmpty
268+ }
269+
229270 // Store previous value for rollback
230271 s.mu.Lock()
231272 previousImage, exists := s.config.Routes[route.Domain]
@@ -182,6 +182,31 @@ func (s *Service) GetRoutes(_ context.Context) []domain.Route {
182 return routes182 return routes
183 }183 }
184 184
185+// GetRoute returns a single route by domain.
186+func (s *Service) GetRoute(_ context.Context, domainName string) (*domain.Route, error) {
187+ s.mu.RLock()
188+ defer s.mu.RUnlock()
189+
190+ image, exists := s.config.Routes[domainName]
191+ if !exists {
192+ return nil, domain.ErrRouteNotFound
193+ }
194+
195+ route := &domain.Route{
196+ Domain: domainName,
197+ Image: image,
198+ HTTPS: true,
199+ }
200+
201+ // Check if domain has http:// prefix
202+ if strings.HasPrefix(domainName, "http://") {
203+ route.Domain = strings.TrimPrefix(domainName, "http://")
204+ route.HTTPS = false
205+ }
206+
207+ return route, nil
208+}
209+
185 // AddRoute adds a new route to the configuration and persists it.210 // AddRoute adds a new route to the configuration and persists it.
186 func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {211 func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {
187 ctx = zerowrap.CtxWithFields(ctx, map[string]any{212 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -191,6 +216,14 @@ func (s *Service) AddRoute(ctx context.Context, route domain.Route) error {
191 })216 })
192 log := zerowrap.FromCtx(ctx)217 log := zerowrap.FromCtx(ctx)
193 218
219+ // Validate route
220+ if route.Domain == "" {
221+ return domain.ErrRouteDomainEmpty
222+ }
223+ if route.Image == "" {
224+ return domain.ErrRouteImageEmpty
225+ }
226+
194 // Store previous value for rollback227 // Store previous value for rollback
195 s.mu.Lock()228 s.mu.Lock()
196 previousImage, existed := s.config.Routes[route.Domain]229 previousImage, existed := s.config.Routes[route.Domain]
@@ -226,6 +259,14 @@ func (s *Service) UpdateRoute(ctx context.Context, route domain.Route) error {
226 })259 })
227 log := zerowrap.FromCtx(ctx)260 log := zerowrap.FromCtx(ctx)
228 261
262+ // Validate route
263+ if route.Domain == "" {
264+ return domain.ErrRouteDomainEmpty
265+ }
266+ if route.Image == "" {
267+ return domain.ErrRouteImageEmpty
268+ }
269+
229 // Store previous value for rollback270 // Store previous value for rollback
230 s.mu.Lock()271 s.mu.Lock()
231 previousImage, exists := s.config.Routes[route.Domain]272 previousImage, exists := s.config.Routes[route.Domain]
M internal/usecase/config/service_test.go +143 -52
@@ -98,83 +98,174 @@ func TestService_GetRoutes(t *testing.T) {
9898 assert.False(t, routeMap["insecure.example"].HTTPS)
9999 }
100100
101-func TestService_AddRoute(t *testing.T) {
102- // Create temp config file for Save() to work
103- tmpDir := t.TempDir()
104- configFile := filepath.Join(tmpDir, "gordon.toml")
105- err := os.WriteFile(configFile, []byte("[routes]\n"), 0600)
106- require.NoError(t, err)
107-
101+func TestService_GetRoute(t *testing.T) {
108102 v := viper.New()
109- v.SetConfigFile(configFile)
103+ v.Set("routes", map[string]interface{}{
104+ "app.example.com": "myapp:latest",
105+ "http://insecure.example": "insecureapp:latest",
106+ })
107+
110108 eventBus := mocks.NewMockEventPublisher(t)
111109 svc := NewService(v, eventBus)
112110 ctx := testContext()
113111
114112 _ = svc.Load(ctx)
115113
116- route := domain.Route{
117- Domain: "new.example.com",
118- Image: "newapp:latest",
119- }
114+ t.Run("existing route", func(t *testing.T) {
115+ route, err := svc.GetRoute(ctx, "app.example.com")
116+ require.NoError(t, err)
117+ assert.Equal(t, "app.example.com", route.Domain)
118+ assert.Equal(t, "myapp:latest", route.Image)
119+ assert.True(t, route.HTTPS)
120+ })
120121
121- err = svc.AddRoute(ctx, route)
122+ t.Run("non-existent route", func(t *testing.T) {
123+ route, err := svc.GetRoute(ctx, "notfound.example.com")
124+ assert.ErrorIs(t, err, domain.ErrRouteNotFound)
125+ assert.Nil(t, route)
126+ })
127+}
122128
123- assert.NoError(t, err)
129+func TestService_AddRoute(t *testing.T) {
130+ t.Run("success", func(t *testing.T) {
131+ // Create temp config file for Save() to work
132+ tmpDir := t.TempDir()
133+ configFile := filepath.Join(tmpDir, "gordon.toml")
134+ err := os.WriteFile(configFile, []byte("[routes]\n"), 0600)
135+ require.NoError(t, err)
124136
125- // Verify route was added
126- config := svc.GetConfig()
127- assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])
128-}
137+ v := viper.New()
138+ v.SetConfigFile(configFile)
139+ eventBus := mocks.NewMockEventPublisher(t)
140+ svc := NewService(v, eventBus)
141+ ctx := testContext()
129142
130-func TestService_UpdateRoute(t *testing.T) {
131- // Create temp config file for Save() to work
132- tmpDir := t.TempDir()
133- configFile := filepath.Join(tmpDir, "gordon.toml")
134- err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:v1\"\n"), 0600)
135- require.NoError(t, err)
143+ _ = svc.Load(ctx)
136144
137- v := viper.New()
138- v.SetConfigFile(configFile)
139- v.Set("routes", map[string]interface{}{
140- "app.example.com": "myapp:v1",
145+ route := domain.Route{
146+ Domain: "new.example.com",
147+ Image: "newapp:latest",
148+ }
149+
150+ err = svc.AddRoute(ctx, route)
151+
152+ assert.NoError(t, err)
153+
154+ // Verify route was added
155+ config := svc.GetConfig()
156+ assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])
141157 })
142158
143- eventBus := mocks.NewMockEventPublisher(t)
144- svc := NewService(v, eventBus)
145- ctx := testContext()
159+ t.Run("empty domain", func(t *testing.T) {
160+ v := viper.New()
161+ eventBus := mocks.NewMockEventPublisher(t)
162+ svc := NewService(v, eventBus)
163+ ctx := testContext()
146164
147- _ = svc.Load(ctx)
165+ route := domain.Route{
166+ Domain: "",
167+ Image: "myapp:latest",
168+ }
148169
149- route := domain.Route{
150- Domain: "app.example.com",
151- Image: "myapp:v2",
152- }
170+ err := svc.AddRoute(ctx, route)
171+ assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
172+ })
153173
154- err = svc.UpdateRoute(ctx, route)
174+ t.Run("empty image", func(t *testing.T) {
175+ v := viper.New()
176+ eventBus := mocks.NewMockEventPublisher(t)
177+ svc := NewService(v, eventBus)
178+ ctx := testContext()
155179
156- assert.NoError(t, err)
180+ route := domain.Route{
181+ Domain: "example.com",
182+ Image: "",
183+ }
157184
158- config := svc.GetConfig()
159- assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])
185+ err := svc.AddRoute(ctx, route)
186+ assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
187+ })
160188 }
161189
162-func TestService_UpdateRoute_NotFound(t *testing.T) {
163- v := viper.New()
164- eventBus := mocks.NewMockEventPublisher(t)
165- svc := NewService(v, eventBus)
166- ctx := testContext()
190+func TestService_UpdateRoute(t *testing.T) {
191+ t.Run("success", func(t *testing.T) {
192+ // Create temp config file for Save() to work
193+ tmpDir := t.TempDir()
194+ configFile := filepath.Join(tmpDir, "gordon.toml")
195+ err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:v1\"\n"), 0600)
196+ require.NoError(t, err)
197+
198+ v := viper.New()
199+ v.SetConfigFile(configFile)
200+ v.Set("routes", map[string]any{
201+ "app.example.com": "myapp:v1",
202+ })
167203
168- _ = svc.Load(ctx)
204+ eventBus := mocks.NewMockEventPublisher(t)
205+ svc := NewService(v, eventBus)
206+ ctx := testContext()
169207
170- route := domain.Route{
171- Domain: "nonexistent.example.com",
172- Image: "myapp:latest",
173- }
208+ _ = svc.Load(ctx)
174209
175- err := svc.UpdateRoute(ctx, route)
210+ route := domain.Route{
211+ Domain: "app.example.com",
212+ Image: "myapp:v2",
213+ }
176214
177- assert.ErrorIs(t, err, domain.ErrRouteNotFound)
215+ err = svc.UpdateRoute(ctx, route)
216+
217+ assert.NoError(t, err)
218+
219+ config := svc.GetConfig()
220+ assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])
221+ })
222+
223+ t.Run("not found", func(t *testing.T) {
224+ v := viper.New()
225+ eventBus := mocks.NewMockEventPublisher(t)
226+ svc := NewService(v, eventBus)
227+ ctx := testContext()
228+
229+ _ = svc.Load(ctx)
230+
231+ route := domain.Route{
232+ Domain: "nonexistent.example.com",
233+ Image: "myapp:latest",
234+ }
235+
236+ err := svc.UpdateRoute(ctx, route)
237+ assert.ErrorIs(t, err, domain.ErrRouteNotFound)
238+ })
239+
240+ t.Run("empty domain", func(t *testing.T) {
241+ v := viper.New()
242+ eventBus := mocks.NewMockEventPublisher(t)
243+ svc := NewService(v, eventBus)
244+ ctx := testContext()
245+
246+ route := domain.Route{
247+ Domain: "",
248+ Image: "myapp:latest",
249+ }
250+
251+ err := svc.UpdateRoute(ctx, route)
252+ assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
253+ })
254+
255+ t.Run("empty image", func(t *testing.T) {
256+ v := viper.New()
257+ eventBus := mocks.NewMockEventPublisher(t)
258+ svc := NewService(v, eventBus)
259+ ctx := testContext()
260+
261+ route := domain.Route{
262+ Domain: "example.com",
263+ Image: "",
264+ }
265+
266+ err := svc.UpdateRoute(ctx, route)
267+ assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
268+ })
178269 }
179270
180271 func TestService_RemoveRoute(t *testing.T) {
@@ -98,83 +98,174 @@ func TestService_GetRoutes(t *testing.T) {
98 assert.False(t, routeMap["insecure.example"].HTTPS)98 assert.False(t, routeMap["insecure.example"].HTTPS)
99 }99 }
100 100
101-func TestService_AddRoute(t *testing.T) {101+func TestService_GetRoute(t *testing.T) {
102- // Create temp config file for Save() to work
103- tmpDir := t.TempDir()
104- configFile := filepath.Join(tmpDir, "gordon.toml")
105- err := os.WriteFile(configFile, []byte("[routes]\n"), 0600)
106- require.NoError(t, err)
107-
108 v := viper.New()102 v := viper.New()
109- v.SetConfigFile(configFile)103+ v.Set("routes", map[string]interface{}{
104+ "app.example.com": "myapp:latest",
105+ "http://insecure.example": "insecureapp:latest",
106+ })
107+
110 eventBus := mocks.NewMockEventPublisher(t)108 eventBus := mocks.NewMockEventPublisher(t)
111 svc := NewService(v, eventBus)109 svc := NewService(v, eventBus)
112 ctx := testContext()110 ctx := testContext()
113 111
114 _ = svc.Load(ctx)112 _ = svc.Load(ctx)
115 113
116- route := domain.Route{114+ t.Run("existing route", func(t *testing.T) {
117- Domain: "new.example.com",115+ route, err := svc.GetRoute(ctx, "app.example.com")
118- Image: "newapp:latest",116+ require.NoError(t, err)
119- }117+ assert.Equal(t, "app.example.com", route.Domain)
118+ assert.Equal(t, "myapp:latest", route.Image)
119+ assert.True(t, route.HTTPS)
120+ })
120 121
121- err = svc.AddRoute(ctx, route)122+ t.Run("non-existent route", func(t *testing.T) {
123+ route, err := svc.GetRoute(ctx, "notfound.example.com")
124+ assert.ErrorIs(t, err, domain.ErrRouteNotFound)
125+ assert.Nil(t, route)
126+ })
127+}
122 128
123- assert.NoError(t, err)129+func TestService_AddRoute(t *testing.T) {
130+ t.Run("success", func(t *testing.T) {
131+ // Create temp config file for Save() to work
132+ tmpDir := t.TempDir()
133+ configFile := filepath.Join(tmpDir, "gordon.toml")
134+ err := os.WriteFile(configFile, []byte("[routes]\n"), 0600)
135+ require.NoError(t, err)
124 136
125- // Verify route was added137+ v := viper.New()
126- config := svc.GetConfig()138+ v.SetConfigFile(configFile)
127- assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])139+ eventBus := mocks.NewMockEventPublisher(t)
128-}140+ svc := NewService(v, eventBus)
141+ ctx := testContext()
129 142
130-func TestService_UpdateRoute(t *testing.T) {143+ _ = svc.Load(ctx)
131- // Create temp config file for Save() to work
132- tmpDir := t.TempDir()
133- configFile := filepath.Join(tmpDir, "gordon.toml")
134- err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:v1\"\n"), 0600)
135- require.NoError(t, err)
136 144
137- v := viper.New()145+ route := domain.Route{
138- v.SetConfigFile(configFile)146+ Domain: "new.example.com",
139- v.Set("routes", map[string]interface{}{147+ Image: "newapp:latest",
140- "app.example.com": "myapp:v1",148+ }
149+
150+ err = svc.AddRoute(ctx, route)
151+
152+ assert.NoError(t, err)
153+
154+ // Verify route was added
155+ config := svc.GetConfig()
156+ assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])
141 })157 })
142 158
143- eventBus := mocks.NewMockEventPublisher(t)159+ t.Run("empty domain", func(t *testing.T) {
144- svc := NewService(v, eventBus)160+ v := viper.New()
145- ctx := testContext()161+ eventBus := mocks.NewMockEventPublisher(t)
162+ svc := NewService(v, eventBus)
163+ ctx := testContext()
146 164
147- _ = svc.Load(ctx)165+ route := domain.Route{
166+ Domain: "",
167+ Image: "myapp:latest",
168+ }
148 169
149- route := domain.Route{170+ err := svc.AddRoute(ctx, route)
150- Domain: "app.example.com",171+ assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
151- Image: "myapp:v2",172+ })
152- }
153 173
154- err = svc.UpdateRoute(ctx, route)174+ t.Run("empty image", func(t *testing.T) {
175+ v := viper.New()
176+ eventBus := mocks.NewMockEventPublisher(t)
177+ svc := NewService(v, eventBus)
178+ ctx := testContext()
155 179
156- assert.NoError(t, err)180+ route := domain.Route{
181+ Domain: "example.com",
182+ Image: "",
183+ }
157 184
158- config := svc.GetConfig()185+ err := svc.AddRoute(ctx, route)
159- assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])186+ assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
187+ })
160 }188 }
161 189
162-func TestService_UpdateRoute_NotFound(t *testing.T) {190+func TestService_UpdateRoute(t *testing.T) {
163- v := viper.New()191+ t.Run("success", func(t *testing.T) {
164- eventBus := mocks.NewMockEventPublisher(t)192+ // Create temp config file for Save() to work
165- svc := NewService(v, eventBus)193+ tmpDir := t.TempDir()
166- ctx := testContext()194+ configFile := filepath.Join(tmpDir, "gordon.toml")
195+ err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:v1\"\n"), 0600)
196+ require.NoError(t, err)
197+
198+ v := viper.New()
199+ v.SetConfigFile(configFile)
200+ v.Set("routes", map[string]any{
201+ "app.example.com": "myapp:v1",
202+ })
167 203
168- _ = svc.Load(ctx)204+ eventBus := mocks.NewMockEventPublisher(t)
205+ svc := NewService(v, eventBus)
206+ ctx := testContext()
169 207
170- route := domain.Route{208+ _ = svc.Load(ctx)
171- Domain: "nonexistent.example.com",
172- Image: "myapp:latest",
173- }
174 209
175- err := svc.UpdateRoute(ctx, route)210+ route := domain.Route{
211+ Domain: "app.example.com",
212+ Image: "myapp:v2",
213+ }
176 214
177- assert.ErrorIs(t, err, domain.ErrRouteNotFound)215+ err = svc.UpdateRoute(ctx, route)
216+
217+ assert.NoError(t, err)
218+
219+ config := svc.GetConfig()
220+ assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])
221+ })
222+
223+ t.Run("not found", func(t *testing.T) {
224+ v := viper.New()
225+ eventBus := mocks.NewMockEventPublisher(t)
226+ svc := NewService(v, eventBus)
227+ ctx := testContext()
228+
229+ _ = svc.Load(ctx)
230+
231+ route := domain.Route{
232+ Domain: "nonexistent.example.com",
233+ Image: "myapp:latest",
234+ }
235+
236+ err := svc.UpdateRoute(ctx, route)
237+ assert.ErrorIs(t, err, domain.ErrRouteNotFound)
238+ })
239+
240+ t.Run("empty domain", func(t *testing.T) {
241+ v := viper.New()
242+ eventBus := mocks.NewMockEventPublisher(t)
243+ svc := NewService(v, eventBus)
244+ ctx := testContext()
245+
246+ route := domain.Route{
247+ Domain: "",
248+ Image: "myapp:latest",
249+ }
250+
251+ err := svc.UpdateRoute(ctx, route)
252+ assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
253+ })
254+
255+ t.Run("empty image", func(t *testing.T) {
256+ v := viper.New()
257+ eventBus := mocks.NewMockEventPublisher(t)
258+ svc := NewService(v, eventBus)
259+ ctx := testContext()
260+
261+ route := domain.Route{
262+ Domain: "example.com",
263+ Image: "",
264+ }
265+
266+ err := svc.UpdateRoute(ctx, route)
267+ assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
268+ })
178 }269 }
179 270
180 func TestService_RemoveRoute(t *testing.T) {271 func TestService_RemoveRoute(t *testing.T) {
M internal/usecase/registry/service.go +46 -5
@@ -3,6 +3,8 @@ package registry
33
44 import (
55 "context"
6+ "crypto/sha256"
7+ "fmt"
68 "io"
79
810 "github.com/bnema/zerowrap"
@@ -54,8 +56,8 @@ func (s *Service) GetManifest(ctx context.Context, name, reference string) (*dom
5456 }, nil
5557 }
5658
57-// PutManifest stores a manifest.
58-func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) error {
59+// PutManifest stores a manifest and returns the calculated digest.
60+func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) (string, error) {
5961 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
6062 zerowrap.FieldLayer: "usecase",
6163 zerowrap.FieldUseCase: "PutManifest",
@@ -64,8 +66,11 @@ func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) er
6466 })
6567 log := zerowrap.FromCtx(ctx)
6668
69+ // Calculate digest
70+ digest := fmt.Sprintf("sha256:%x", sha256.Sum256(manifest.Data))
71+
6772 if err := s.manifestStorage.PutManifest(manifest.Name, manifest.Reference, manifest.ContentType, manifest.Data); err != nil {
68- return log.WrapErr(err, "failed to store manifest")
73+ return "", log.WrapErr(err, "failed to store manifest")
6974 }
7075
7176 // Publish image pushed event
@@ -80,8 +85,8 @@ func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) er
8085 }
8186 }
8287
83- log.Info().Msg("manifest stored")
84- return nil
88+ log.Info().Str("digest", digest).Msg("manifest stored")
89+ return digest, nil
8590 }
8691
8792 // DeleteManifest removes a manifest.
@@ -119,6 +124,23 @@ func (s *Service) GetBlob(ctx context.Context, digest string) (io.ReadCloser, er
119124 return reader, nil
120125 }
121126
127+// GetBlobPath returns the filesystem path to a blob for direct serving.
128+func (s *Service) GetBlobPath(ctx context.Context, digest string) (string, error) {
129+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
130+ zerowrap.FieldLayer: "usecase",
131+ zerowrap.FieldUseCase: "GetBlobPath",
132+ "digest": digest,
133+ })
134+ log := zerowrap.FromCtx(ctx)
135+
136+ path, err := s.blobStorage.GetBlobPath(digest)
137+ if err != nil {
138+ return "", log.WrapErr(err, "failed to get blob path")
139+ }
140+
141+ return path, nil
142+}
143+
122144 // PutBlob stores a blob with the given digest.
123145 func (s *Service) PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error {
124146 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -160,6 +182,25 @@ func (s *Service) StartUpload(ctx context.Context, name string) (string, error)
160182 return uuid, nil
161183 }
162184
185+// AppendBlobChunk appends data to an in-progress blob upload.
186+func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, chunk []byte) (int64, error) {
187+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
188+ zerowrap.FieldLayer: "usecase",
189+ zerowrap.FieldUseCase: "AppendBlobChunk",
190+ "name": name,
191+ "uuid": uuid,
192+ zerowrap.FieldSize: len(chunk),
193+ })
194+ log := zerowrap.FromCtx(ctx)
195+
196+ length, err := s.blobStorage.AppendBlobChunk(name, uuid, chunk)
197+ if err != nil {
198+ return 0, log.WrapErr(err, "failed to append blob chunk")
199+ }
200+
201+ return length, nil
202+}
203+
163204 // FinishUpload completes a blob upload.
164205 func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error {
165206 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -3,6 +3,8 @@ package registry
3 3
4 import (4 import (
5 "context"5 "context"
6+ "crypto/sha256"
7+ "fmt"
6 "io"8 "io"
7 9
8 "github.com/bnema/zerowrap"10 "github.com/bnema/zerowrap"
@@ -54,8 +56,8 @@ func (s *Service) GetManifest(ctx context.Context, name, reference string) (*dom
54 }, nil56 }, nil
55 }57 }
56 58
57-// PutManifest stores a manifest.59+// PutManifest stores a manifest and returns the calculated digest.
58-func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) error {60+func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) (string, error) {
59 ctx = zerowrap.CtxWithFields(ctx, map[string]any{61 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
60 zerowrap.FieldLayer: "usecase",62 zerowrap.FieldLayer: "usecase",
61 zerowrap.FieldUseCase: "PutManifest",63 zerowrap.FieldUseCase: "PutManifest",
@@ -64,8 +66,11 @@ func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) er
64 })66 })
65 log := zerowrap.FromCtx(ctx)67 log := zerowrap.FromCtx(ctx)
66 68
69+ // Calculate digest
70+ digest := fmt.Sprintf("sha256:%x", sha256.Sum256(manifest.Data))
71+
67 if err := s.manifestStorage.PutManifest(manifest.Name, manifest.Reference, manifest.ContentType, manifest.Data); err != nil {72 if err := s.manifestStorage.PutManifest(manifest.Name, manifest.Reference, manifest.ContentType, manifest.Data); err != nil {
68- return log.WrapErr(err, "failed to store manifest")73+ return "", log.WrapErr(err, "failed to store manifest")
69 }74 }
70 75
71 // Publish image pushed event76 // Publish image pushed event
@@ -80,8 +85,8 @@ func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) er
80 }85 }
81 }86 }
82 87
83- log.Info().Msg("manifest stored")88+ log.Info().Str("digest", digest).Msg("manifest stored")
84- return nil89+ return digest, nil
85 }90 }
86 91
87 // DeleteManifest removes a manifest.92 // DeleteManifest removes a manifest.
@@ -119,6 +124,23 @@ func (s *Service) GetBlob(ctx context.Context, digest string) (io.ReadCloser, er
119 return reader, nil124 return reader, nil
120 }125 }
121 126
127+// GetBlobPath returns the filesystem path to a blob for direct serving.
128+func (s *Service) GetBlobPath(ctx context.Context, digest string) (string, error) {
129+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
130+ zerowrap.FieldLayer: "usecase",
131+ zerowrap.FieldUseCase: "GetBlobPath",
132+ "digest": digest,
133+ })
134+ log := zerowrap.FromCtx(ctx)
135+
136+ path, err := s.blobStorage.GetBlobPath(digest)
137+ if err != nil {
138+ return "", log.WrapErr(err, "failed to get blob path")
139+ }
140+
141+ return path, nil
142+}
143+
122 // PutBlob stores a blob with the given digest.144 // PutBlob stores a blob with the given digest.
123 func (s *Service) PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error {145 func (s *Service) PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error {
124 ctx = zerowrap.CtxWithFields(ctx, map[string]any{146 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
@@ -160,6 +182,25 @@ func (s *Service) StartUpload(ctx context.Context, name string) (string, error)
160 return uuid, nil182 return uuid, nil
161 }183 }
162 184
185+// AppendBlobChunk appends data to an in-progress blob upload.
186+func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, chunk []byte) (int64, error) {
187+ ctx = zerowrap.CtxWithFields(ctx, map[string]any{
188+ zerowrap.FieldLayer: "usecase",
189+ zerowrap.FieldUseCase: "AppendBlobChunk",
190+ "name": name,
191+ "uuid": uuid,
192+ zerowrap.FieldSize: len(chunk),
193+ })
194+ log := zerowrap.FromCtx(ctx)
195+
196+ length, err := s.blobStorage.AppendBlobChunk(name, uuid, chunk)
197+ if err != nil {
198+ return 0, log.WrapErr(err, "failed to append blob chunk")
199+ }
200+
201+ return length, nil
202+}
203+
163 // FinishUpload completes a blob upload.204 // FinishUpload completes a blob upload.
164 func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error {205 func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error {
165 ctx = zerowrap.CtxWithFields(ctx, map[string]any{206 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
M internal/usecase/registry/service_test.go +7 -4
@@ -5,6 +5,7 @@ import (
55 "context"
66 "errors"
77 "io"
8+ "strings"
89 "testing"
910
1011 "github.com/bnema/zerowrap"
@@ -75,9 +76,11 @@ func TestService_PutManifest_Success(t *testing.T) {
7576 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
7677 eventBus.EXPECT().Publish(domain.EventImagePushed, mock.AnythingOfType("domain.ImagePushedPayload")).Return(nil)
7778
78- err := svc.PutManifest(ctx, manifest)
79+ digest, err := svc.PutManifest(ctx, manifest)
7980
8081 assert.NoError(t, err)
82+ assert.NotEmpty(t, digest)
83+ assert.True(t, strings.HasPrefix(digest, "sha256:"))
8184 }
8285
8386 func TestService_PutManifest_StorageError(t *testing.T) {
@@ -97,7 +100,7 @@ func TestService_PutManifest_StorageError(t *testing.T) {
97100
98101 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(errors.New("storage full"))
99102
100- err := svc.PutManifest(ctx, manifest)
103+ _, err := svc.PutManifest(ctx, manifest)
101104
102105 assert.Error(t, err)
103106 assert.Contains(t, err.Error(), "failed to store manifest")
@@ -122,7 +125,7 @@ func TestService_PutManifest_EventPublishError(t *testing.T) {
122125 eventBus.EXPECT().Publish(domain.EventImagePushed, mock.Anything).Return(errors.New("event bus error"))
123126
124127 // Should still succeed - event publish error is logged but doesn't fail the operation
125- err := svc.PutManifest(ctx, manifest)
128+ _, err := svc.PutManifest(ctx, manifest)
126129
127130 assert.NoError(t, err)
128131 }
@@ -144,7 +147,7 @@ func TestService_PutManifest_NilEventBus(t *testing.T) {
144147
145148 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
146149
147- err := svc.PutManifest(ctx, manifest)
150+ _, err := svc.PutManifest(ctx, manifest)
148151
149152 assert.NoError(t, err)
150153 }
@@ -5,6 +5,7 @@ import (
5 "context"5 "context"
6 "errors"6 "errors"
7 "io"7 "io"
8+ "strings"
8 "testing"9 "testing"
9 10
10 "github.com/bnema/zerowrap"11 "github.com/bnema/zerowrap"
@@ -75,9 +76,11 @@ func TestService_PutManifest_Success(t *testing.T) {
75 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)76 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
76 eventBus.EXPECT().Publish(domain.EventImagePushed, mock.AnythingOfType("domain.ImagePushedPayload")).Return(nil)77 eventBus.EXPECT().Publish(domain.EventImagePushed, mock.AnythingOfType("domain.ImagePushedPayload")).Return(nil)
77 78
78- err := svc.PutManifest(ctx, manifest)79+ digest, err := svc.PutManifest(ctx, manifest)
79 80
80 assert.NoError(t, err)81 assert.NoError(t, err)
82+ assert.NotEmpty(t, digest)
83+ assert.True(t, strings.HasPrefix(digest, "sha256:"))
81 }84 }
82 85
83 func TestService_PutManifest_StorageError(t *testing.T) {86 func TestService_PutManifest_StorageError(t *testing.T) {
@@ -97,7 +100,7 @@ func TestService_PutManifest_StorageError(t *testing.T) {
97 100
98 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(errors.New("storage full"))101 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(errors.New("storage full"))
99 102
100- err := svc.PutManifest(ctx, manifest)103+ _, err := svc.PutManifest(ctx, manifest)
101 104
102 assert.Error(t, err)105 assert.Error(t, err)
103 assert.Contains(t, err.Error(), "failed to store manifest")106 assert.Contains(t, err.Error(), "failed to store manifest")
@@ -122,7 +125,7 @@ func TestService_PutManifest_EventPublishError(t *testing.T) {
122 eventBus.EXPECT().Publish(domain.EventImagePushed, mock.Anything).Return(errors.New("event bus error"))125 eventBus.EXPECT().Publish(domain.EventImagePushed, mock.Anything).Return(errors.New("event bus error"))
123 126
124 // Should still succeed - event publish error is logged but doesn't fail the operation127 // Should still succeed - event publish error is logged but doesn't fail the operation
125- err := svc.PutManifest(ctx, manifest)128+ _, err := svc.PutManifest(ctx, manifest)
126 129
127 assert.NoError(t, err)130 assert.NoError(t, err)
128 }131 }
@@ -144,7 +147,7 @@ func TestService_PutManifest_NilEventBus(t *testing.T) {
144 147
145 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)148 manifestStorage.EXPECT().PutManifest("myapp", "latest", "application/vnd.docker.distribution.manifest.v2+json", manifest.Data).Return(nil)
146 149
147- err := svc.PutManifest(ctx, manifest)150+ _, err := svc.PutManifest(ctx, manifest)
148 151
149 assert.NoError(t, err)152 assert.NoError(t, err)
150 }153 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close