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