1// Package registry implements the container registry use case.
2package registry
3
4import (
5 "context"
6 "crypto/sha256"
7 "fmt"
8 "io"
9 "strings"
10
11 "github.com/bnema/zerowrap"
12
13 "github.com/bnema/gordon/internal/boundaries/out"
14 "github.com/bnema/gordon/internal/domain"
15 "github.com/bnema/gordon/pkg/validation"
16)
17
18// Service implements the RegistryService interface.
19type Service struct {
20 blobStorage out.BlobStorage
21 manifestStorage out.ManifestStorage
22 eventBus out.EventPublisher
23}
24
25// NewService creates a new registry service.
26func NewService(
27 blobStorage out.BlobStorage,
28 manifestStorage out.ManifestStorage,
29 eventBus out.EventPublisher,
30) *Service {
31 return &Service{
32 blobStorage: blobStorage,
33 manifestStorage: manifestStorage,
34 eventBus: eventBus,
35 }
36}
37
38// GetManifest retrieves a manifest by name and reference.
39func (s *Service) GetManifest(ctx context.Context, name, reference string) (*domain.Manifest, error) {
40 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
41 zerowrap.FieldLayer: "usecase",
42 zerowrap.FieldUseCase: "GetManifest",
43 "name": name,
44 "reference": reference,
45 })
46 log := zerowrap.FromCtx(ctx)
47
48 data, contentType, err := s.manifestStorage.GetManifest(name, reference)
49 if err != nil {
50 return nil, log.WrapErr(err, "failed to get manifest")
51 }
52
53 return &domain.Manifest{
54 Name: name,
55 Reference: reference,
56 ContentType: contentType,
57 Data: data,
58 }, nil
59}
60
61// PutManifest stores a manifest and returns the calculated digest.
62func (s *Service) PutManifest(ctx context.Context, manifest *domain.Manifest) (string, error) {
63 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
64 zerowrap.FieldLayer: "usecase",
65 zerowrap.FieldUseCase: "PutManifest",
66 "name": manifest.Name,
67 "reference": manifest.Reference,
68 })
69 log := zerowrap.FromCtx(ctx)
70
71 // Calculate digest
72 digest := fmt.Sprintf("sha256:%x", sha256.Sum256(manifest.Data))
73
74 if err := s.manifestStorage.PutManifest(manifest.Name, manifest.Reference, manifest.ContentType, manifest.Data); err != nil {
75 return "", log.WrapErr(err, "failed to store manifest")
76 }
77
78 // Publish image pushed event only for tag references (not digests).
79 // A docker push sends manifests by both digest and tag; firing only on
80 // tag prevents duplicate deploy triggers for the same push.
81 if s.eventBus != nil && !strings.HasPrefix(manifest.Reference, "sha256:") {
82 if err := s.eventBus.Publish(domain.EventImagePushed, domain.ImagePushedPayload{
83 Name: manifest.Name,
84 Reference: manifest.Reference,
85 Manifest: manifest.Data,
86 Annotations: manifest.Annotations,
87 }); err != nil {
88 log.Warn().Err(err).Msg("failed to publish image pushed event")
89 }
90 }
91
92 log.Info().Str("digest", digest).Msg("manifest stored")
93 return digest, nil
94}
95
96// DeleteManifest removes a manifest.
97func (s *Service) DeleteManifest(ctx context.Context, name, reference string) error {
98 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
99 zerowrap.FieldLayer: "usecase",
100 zerowrap.FieldUseCase: "DeleteManifest",
101 "name": name,
102 "reference": reference,
103 })
104 log := zerowrap.FromCtx(ctx)
105
106 if err := s.manifestStorage.DeleteManifest(name, reference); err != nil {
107 return log.WrapErr(err, "failed to delete manifest")
108 }
109
110 log.Info().Msg("manifest deleted")
111 return nil
112}
113
114// GetBlob retrieves a blob by digest.
115func (s *Service) GetBlob(ctx context.Context, digest string) (io.ReadCloser, error) {
116 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
117 zerowrap.FieldLayer: "usecase",
118 zerowrap.FieldUseCase: "GetBlob",
119 "digest": digest,
120 })
121 log := zerowrap.FromCtx(ctx)
122
123 reader, err := s.blobStorage.GetBlob(digest)
124 if err != nil {
125 return nil, log.WrapErr(err, "failed to get blob")
126 }
127
128 return reader, nil
129}
130
131// GetBlobPath returns the filesystem path to a blob for direct serving.
132func (s *Service) GetBlobPath(ctx context.Context, digest string) (string, error) {
133 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
134 zerowrap.FieldLayer: "usecase",
135 zerowrap.FieldUseCase: "GetBlobPath",
136 "digest": digest,
137 })
138 log := zerowrap.FromCtx(ctx)
139
140 path, err := s.blobStorage.GetBlobPath(digest)
141 if err != nil {
142 return "", log.WrapErr(err, "failed to get blob path")
143 }
144
145 return path, nil
146}
147
148// PutBlob stores a blob with the given digest.
149func (s *Service) PutBlob(ctx context.Context, digest string, data io.Reader, size int64) error {
150 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
151 zerowrap.FieldLayer: "usecase",
152 zerowrap.FieldUseCase: "PutBlob",
153 "digest": digest,
154 zerowrap.FieldSize: size,
155 })
156 log := zerowrap.FromCtx(ctx)
157
158 if err := s.blobStorage.PutBlob(digest, data, size); err != nil {
159 return log.WrapErr(err, "failed to store blob")
160 }
161
162 log.Info().Msg("blob stored")
163 return nil
164}
165
166// BlobExists checks if a blob exists.
167func (s *Service) BlobExists(_ context.Context, digest string) bool {
168 return s.blobStorage.BlobExists(digest)
169}
170
171// StartUpload starts a new blob upload.
172func (s *Service) StartUpload(ctx context.Context, name string) (string, error) {
173 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
174 zerowrap.FieldLayer: "usecase",
175 zerowrap.FieldUseCase: "StartUpload",
176 "name": name,
177 })
178 log := zerowrap.FromCtx(ctx)
179
180 uuid, err := s.blobStorage.StartBlobUpload(name)
181 if err != nil {
182 return "", log.WrapErr(err, "failed to start blob upload")
183 }
184
185 log.Info().Str("uuid", uuid).Msg("blob upload started")
186 return uuid, nil
187}
188
189// AppendBlobChunk appends data to an in-progress blob upload.
190func (s *Service) AppendBlobChunk(ctx context.Context, name, uuid string, data io.Reader) (int64, error) {
191 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
192 zerowrap.FieldLayer: "usecase",
193 zerowrap.FieldUseCase: "AppendBlobChunk",
194 "name": name,
195 "uuid": uuid,
196 })
197 log := zerowrap.FromCtx(ctx)
198
199 length, err := s.blobStorage.AppendBlobChunk(name, uuid, data)
200 if err != nil {
201 return 0, log.WrapErr(err, "failed to append blob chunk")
202 }
203
204 return length, nil
205}
206
207// FinishUpload completes a blob upload.
208func (s *Service) FinishUpload(ctx context.Context, uuid, digest string) error {
209 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
210 zerowrap.FieldLayer: "usecase",
211 zerowrap.FieldUseCase: "FinishUpload",
212 "uuid": uuid,
213 "digest": digest,
214 })
215 log := zerowrap.FromCtx(ctx)
216
217 if err := s.blobStorage.FinishBlobUpload(uuid, digest); err != nil {
218 return log.WrapErr(err, "failed to finish blob upload")
219 }
220
221 log.Info().Msg("blob upload finished")
222 return nil
223}
224
225// CancelUpload cancels an in-progress upload.
226func (s *Service) CancelUpload(ctx context.Context, uuid string) error {
227 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
228 zerowrap.FieldLayer: "usecase",
229 zerowrap.FieldUseCase: "CancelUpload",
230 "uuid": uuid,
231 })
232 log := zerowrap.FromCtx(ctx)
233
234 if err := s.blobStorage.CancelBlobUpload(uuid); err != nil {
235 return log.WrapErr(err, "failed to cancel blob upload")
236 }
237
238 log.Info().Msg("blob upload cancelled")
239 return nil
240}
241
242// ListTags returns all tags for a repository.
243func (s *Service) ListTags(ctx context.Context, name string) ([]string, error) {
244 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
245 zerowrap.FieldLayer: "usecase",
246 zerowrap.FieldUseCase: "ListTags",
247 "name": name,
248 })
249 log := zerowrap.FromCtx(ctx)
250
251 tags, err := s.manifestStorage.ListTags(name)
252 if err != nil {
253 return nil, log.WrapErr(err, "failed to list tags")
254 }
255
256 filtered := make([]string, 0, len(tags))
257 for _, tag := range tags {
258 if validation.IsDigest(tag) {
259 continue
260 }
261 filtered = append(filtered, tag)
262 }
263
264 return filtered, nil
265}
266
267// ListRepositories returns all repository names.
268func (s *Service) ListRepositories(ctx context.Context) ([]string, error) {
269 ctx = zerowrap.CtxWithFields(ctx, map[string]any{
270 zerowrap.FieldLayer: "usecase",
271 zerowrap.FieldUseCase: "ListRepositories",
272 })
273 log := zerowrap.FromCtx(ctx)
274
275 repos, err := s.manifestStorage.ListRepositories()
276 if err != nil {
277 return nil, log.WrapErr(err, "failed to list repositories")
278 }
279
280 return repos, nil
281}