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