Skip to content

Commit b655d8e

Browse files
committed
feat(events): add actor and request metadata to webhook events
Signed-off-by: cainydev <wajo432@gmail.com>
1 parent 3bc5f97 commit b655d8e

28 files changed

Lines changed: 868 additions & 409 deletions

pkg/api/routes.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"zotregistry.dev/zot/v2/pkg/debug/pprof"
4343
debug "zotregistry.dev/zot/v2/pkg/debug/swagger"
4444
ext "zotregistry.dev/zot/v2/pkg/extensions"
45+
"zotregistry.dev/zot/v2/pkg/extensions/events"
4546
"zotregistry.dev/zot/v2/pkg/log"
4647
"zotregistry.dev/zot/v2/pkg/meta"
4748
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
@@ -756,6 +757,8 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht
756757
return
757758
}
758759

760+
ctx := events.WithEventContext(request.Context(), eventContextFromRequest(request))
761+
759762
if len(digestQueryTags) > 0 && !zcommon.IsDigest(reference) {
760763
err := apiErr.NewError(apiErr.MANIFEST_INVALID).AddDetail(map[string]string{
761764
"reason": "tag query parameters are only valid when pushing a manifest by digest",
@@ -765,7 +768,7 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht
765768
return
766769
}
767770

768-
digest, subjectDigest, err := imgStore.PutImageManifest(name, reference, mediaType, body, digestQueryTags)
771+
digest, subjectDigest, err := imgStore.PutImageManifest(ctx, name, reference, mediaType, body, digestQueryTags)
769772
if err != nil {
770773
details := zerr.GetDetails(err)
771774
if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
@@ -792,7 +795,7 @@ func (rh *RouteHandler) UpdateManifest(response http.ResponseWriter, request *ht
792795
// could be syscall.EMFILE (Err:0x18 too many opened files), etc
793796
rh.c.Log.Error().Err(err).Msg("unexpected error, performing cleanup")
794797

795-
if err = imgStore.DeleteImageManifest(name, reference, false); err != nil {
798+
if err = imgStore.DeleteImageManifest(ctx, name, reference, false); err != nil {
796799
// deletion of image manifest is important, but not critical for image repo consistency
797800
// in the worst scenario a partial manifest file written to disk will not affect the repo because
798801
// the new manifest was not added to "index.json" file (it is possible that GC will take care of it)
@@ -937,7 +940,9 @@ func (rh *RouteHandler) DeleteManifest(response http.ResponseWriter, request *ht
937940
return
938941
}
939942

940-
err = imgStore.DeleteImageManifest(name, reference, detectCollision)
943+
ctx := events.WithEventContext(request.Context(), eventContextFromRequest(request))
944+
945+
err = imgStore.DeleteImageManifest(ctx, name, reference, detectCollision)
941946
if err != nil { //nolint: dupl
942947
details := zerr.GetDetails(err)
943948
if errors.Is(err, zerr.ErrRepoNotFound) { //nolint:gocritic // errorslint conflicts with gocritic:IfElseChain
@@ -2500,3 +2505,23 @@ func isSyncOnDemandEnabled(ctlr *Controller) bool {
25002505

25012506
return false
25022507
}
2508+
2509+
func eventContextFromRequest(r *http.Request) *events.EventContext {
2510+
ectx := &events.EventContext{
2511+
Request: &events.RequestInfo{
2512+
Addr: r.RemoteAddr,
2513+
Method: r.Method,
2514+
UserAgent: r.UserAgent(),
2515+
},
2516+
}
2517+
2518+
userAc, err := reqCtx.UserAcFromContext(r.Context())
2519+
if err == nil && userAc != nil {
2520+
username := userAc.GetUsername()
2521+
if username != "" {
2522+
ectx.Actor = &events.ActorInfo{Name: username}
2523+
}
2524+
}
2525+
2526+
return ectx
2527+
}

pkg/api/routes_test.go

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ func TestRoutes(t *testing.T) {
266266
"reference": "reference",
267267
},
268268
&mocks.MockedImageStore{
269-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
270-
godigest.Digest, error,
271-
) {
269+
PutImageManifestFn: func(ctx context.Context, repo, reference, mediaType string,
270+
body []byte, extraTags []string,
271+
) (godigest.Digest, godigest.Digest, error) {
272272
return "", "", zerr.ErrRepoNotFound
273273
},
274274
})
@@ -281,9 +281,9 @@ func TestRoutes(t *testing.T) {
281281
},
282282

283283
&mocks.MockedImageStore{
284-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
285-
godigest.Digest, error,
286-
) {
284+
PutImageManifestFn: func(ctx context.Context, repo, reference, mediaType string,
285+
body []byte, extraTags []string,
286+
) (godigest.Digest, godigest.Digest, error) {
287287
return "", "", zerr.ErrManifestNotFound
288288
},
289289
})
@@ -295,9 +295,9 @@ func TestRoutes(t *testing.T) {
295295
"reference": "reference",
296296
},
297297
&mocks.MockedImageStore{
298-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
299-
godigest.Digest, error,
300-
) {
298+
PutImageManifestFn: func(ctx context.Context, repo, reference, mediaType string,
299+
body []byte, extraTags []string,
300+
) (godigest.Digest, godigest.Digest, error) {
301301
return "", "", zerr.ErrBadManifest
302302
},
303303
})
@@ -309,9 +309,9 @@ func TestRoutes(t *testing.T) {
309309
"reference": "reference",
310310
},
311311
&mocks.MockedImageStore{
312-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
313-
godigest.Digest, error,
314-
) {
312+
PutImageManifestFn: func(ctx context.Context, repo, reference, mediaType string,
313+
body []byte, extraTags []string,
314+
) (godigest.Digest, godigest.Digest, error) {
315315
return "", "", zerr.ErrBlobNotFound
316316
},
317317
})
@@ -324,9 +324,9 @@ func TestRoutes(t *testing.T) {
324324
"reference": "reference",
325325
},
326326
&mocks.MockedImageStore{
327-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, _ []string) (godigest.Digest,
328-
godigest.Digest, error,
329-
) {
327+
PutImageManifestFn: func(ctx context.Context, repo, reference, mediaType string,
328+
body []byte, extraTags []string,
329+
) (godigest.Digest, godigest.Digest, error) {
330330
return "", "", zerr.ErrRepoBadVersion
331331
},
332332
})
@@ -358,9 +358,9 @@ func TestRoutes(t *testing.T) {
358358
digestRef := manifestDigest.String()
359359

360360
ism := &mocks.MockedImageStore{
361-
PutImageManifestFn: func(repo, reference, mediaType string, body []byte, extraTags []string) (
362-
godigest.Digest, godigest.Digest, error,
363-
) {
361+
PutImageManifestFn: func(ctx context.Context, repo, reference, mediaType string,
362+
body []byte, extraTags []string,
363+
) (godigest.Digest, godigest.Digest, error) {
364364
So(extraTags, ShouldResemble, []string{"meta-a", "meta-b"})
365365
So(string(body), ShouldEqual, string(mcontent))
366366

@@ -455,7 +455,7 @@ func TestRoutes(t *testing.T) {
455455
"reference": "reference",
456456
},
457457
&mocks.MockedImageStore{
458-
DeleteImageManifestFn: func(repo, reference string, detectCollision bool) error {
458+
DeleteImageManifestFn: func(ctx context.Context, repo, reference string, detectCollision bool) error {
459459
return zerr.ErrRepoNotFound
460460
},
461461
},
@@ -470,7 +470,7 @@ func TestRoutes(t *testing.T) {
470470
"reference": "reference",
471471
},
472472
&mocks.MockedImageStore{
473-
DeleteImageManifestFn: func(repo, reference string, detectCollision bool) error {
473+
DeleteImageManifestFn: func(ctx context.Context, repo, reference string, detectCollision bool) error {
474474
return zerr.ErrManifestNotFound
475475
},
476476
},
@@ -485,7 +485,7 @@ func TestRoutes(t *testing.T) {
485485
"reference": "reference",
486486
},
487487
&mocks.MockedImageStore{
488-
DeleteImageManifestFn: func(repo, reference string, detectCollision bool) error {
488+
DeleteImageManifestFn: func(ctx context.Context, repo, reference string, detectCollision bool) error {
489489
return ErrUnexpectedError
490490
},
491491
},
@@ -500,7 +500,7 @@ func TestRoutes(t *testing.T) {
500500
"reference": "reference",
501501
},
502502
&mocks.MockedImageStore{
503-
DeleteImageManifestFn: func(repo, reference string, detectCollision bool) error {
503+
DeleteImageManifestFn: func(ctx context.Context, repo, reference string, detectCollision bool) error {
504504
return zerr.ErrBadManifest
505505
},
506506
},

pkg/extensions/events/builder.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ func (b *eventBuilder) WithEventType(eventType EventType) *eventBuilder {
3434
return b
3535
}
3636

37+
func (b *eventBuilder) WithEventContext(ectx *EventContext) *eventBuilder {
38+
if ectx == nil {
39+
return b
40+
}
41+
42+
if ectx.Actor != nil {
43+
b.data["actor"] = ectx.Actor
44+
}
45+
46+
if ectx.Request != nil {
47+
b.data["request"] = ectx.Request
48+
}
49+
50+
return b
51+
}
52+
3753
func (b *eventBuilder) Build() (*cloudevents.Event, error) {
3854
if b.eventType == "" {
3955
return nil, zerr.ErrEventTypeEmpty

pkg/extensions/events/common.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package events
22

33
import (
4+
"context"
45
"time"
56
)
67

@@ -22,11 +23,43 @@ func (e EventType) String() string {
2223
return string(e)
2324
}
2425

26+
// ActorInfo describes who triggered an event.
27+
type ActorInfo struct {
28+
Name string `json:"name"`
29+
}
30+
31+
// RequestInfo describes the HTTP request that triggered an event.
32+
type RequestInfo struct {
33+
Addr string `json:"addr"`
34+
Method string `json:"method"`
35+
UserAgent string `json:"useragent"`
36+
}
37+
38+
// EventContext carries actor and request metadata for events.
39+
type EventContext struct {
40+
Actor *ActorInfo `json:"actor,omitempty"`
41+
Request *RequestInfo `json:"request,omitempty"`
42+
}
43+
44+
type eventContextKey struct{}
45+
46+
// WithEventContext attaches an EventContext to a context.Context.
47+
func WithEventContext(ctx context.Context, ec *EventContext) context.Context {
48+
return context.WithValue(ctx, eventContextKey{}, ec)
49+
}
50+
51+
// EventContextFromContext retrieves the EventContext from a context.Context.
52+
func EventContextFromContext(ctx context.Context) *EventContext {
53+
ec, _ := ctx.Value(eventContextKey{}).(*EventContext)
54+
55+
return ec
56+
}
57+
2558
type Recorder interface {
2659
Close()
2760

28-
RepositoryCreated(name string)
29-
ImageUpdated(name, reference, digest, mediaType, manifest string)
30-
ImageDeleted(name, reference, digest, mediaType string)
31-
ImageLintFailed(name, reference, digest, mediaType, manifest string)
61+
RepositoryCreated(name string, ectx *EventContext)
62+
ImageUpdated(name, reference, digest, mediaType, manifest string, ectx *EventContext)
63+
ImageDeleted(name, reference, digest, mediaType string, ectx *EventContext)
64+
ImageLintFailed(name, reference, digest, mediaType, manifest string, ectx *EventContext)
3265
}

pkg/extensions/events/events.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ func (r eventRecorder) publish(event *cloudevents.Event) {
5252
}()
5353
}
5454

55-
func (r eventRecorder) RepositoryCreated(name string) {
55+
func (r eventRecorder) RepositoryCreated(name string, ectx *EventContext) {
5656
event, err := newEventBuilder().
5757
WithEventType(RepositoryCreatedEventType).
5858
WithDataField("name", name).
59+
WithEventContext(ectx).
5960
Build()
6061
if err != nil {
6162
r.log.Warn().Err(err).Msg("failed to create event")
@@ -66,14 +67,15 @@ func (r eventRecorder) RepositoryCreated(name string) {
6667
r.publish(event)
6768
}
6869

69-
func (r eventRecorder) ImageUpdated(name, reference, digest, mediaType, manifest string) {
70+
func (r eventRecorder) ImageUpdated(name, reference, digest, mediaType, manifest string, ectx *EventContext) {
7071
event, err := newEventBuilder().
7172
WithEventType(ImageUpdatedEventType).
7273
WithDataField("name", name).
7374
WithDataField("reference", reference).
7475
WithDataField("digest", digest).
7576
WithDataField("mediaType", mediaType).
7677
WithDataField("manifest", manifest).
78+
WithEventContext(ectx).
7779
Build()
7880
if err != nil {
7981
r.log.Warn().Err(err).Msg("failed to create event")
@@ -84,13 +86,14 @@ func (r eventRecorder) ImageUpdated(name, reference, digest, mediaType, manifest
8486
r.publish(event)
8587
}
8688

87-
func (r eventRecorder) ImageDeleted(name, reference, digest, mediaType string) {
89+
func (r eventRecorder) ImageDeleted(name, reference, digest, mediaType string, ectx *EventContext) {
8890
event, err := newEventBuilder().
8991
WithEventType(ImageDeletedEventType).
9092
WithDataField("name", name).
9193
WithDataField("reference", reference).
9294
WithDataField("digest", digest).
9395
WithDataField("mediaType", mediaType).
96+
WithEventContext(ectx).
9497
Build()
9598
if err != nil {
9699
r.log.Warn().Err(err).Msg("failed to create event")
@@ -101,14 +104,15 @@ func (r eventRecorder) ImageDeleted(name, reference, digest, mediaType string) {
101104
r.publish(event)
102105
}
103106

104-
func (r eventRecorder) ImageLintFailed(name, reference, digest, mediaType, manifest string) {
107+
func (r eventRecorder) ImageLintFailed(name, reference, digest, mediaType, manifest string, ectx *EventContext) {
105108
event, err := newEventBuilder().
106109
WithEventType(ImageLintFailedEventType).
107110
WithDataField("name", name).
108111
WithDataField("reference", reference).
109112
WithDataField("digest", digest).
110113
WithDataField("mediaType", mediaType).
111114
WithDataField("manifest", manifest).
115+
WithEventContext(ectx).
112116
Build()
113117
if err != nil {
114118
r.log.Warn().Err(err).Msg("failed to create event")

0 commit comments

Comments
 (0)