diff --git a/notifications/bridge.go b/notifications/bridge.go index 8f6386d3..bc4a90aa 100644 --- a/notifications/bridge.go +++ b/notifications/bridge.go @@ -108,6 +108,14 @@ func (b *bridge) BlobDeleted(repo reference.Named, dgst digest.Digest) error { return b.createBlobDeleteEventAndWrite(EventActionDelete, repo, dgst) } +func (b *bridge) TagDeleted(repo reference.Named, tag string) error { + event := b.createEvent(EventActionDelete) + event.Target.Repository = repo.Name() + event.Target.Tag = tag + + return b.sink.Write(*event) +} + func (b *bridge) createManifestEventAndWrite(action string, repo reference.Named, sm distribution.Manifest) error { manifestEvent, err := b.createManifestEvent(action, repo, sm) if err != nil { diff --git a/notifications/bridge_test.go b/notifications/bridge_test.go index 86350993..0f4d7736 100644 --- a/notifications/bridge_test.go +++ b/notifications/bridge_test.go @@ -97,6 +97,9 @@ func TestEventBridgeManifestPulledWithTag(t *testing.T) { func TestEventBridgeManifestDeleted(t *testing.T) { l := createTestEnv(t, testSinkFn(func(events ...Event) error { checkDeleted(t, EventActionDelete, events...) + if events[0].Target.Digest != dgst { + t.Fatalf("unexpected digest on event target: %q != %q", events[0].Target.Digest, dgst) + } return nil })) @@ -106,6 +109,21 @@ func TestEventBridgeManifestDeleted(t *testing.T) { } } +func TestEventBridgeTagDeleted(t *testing.T) { + l := createTestEnv(t, testSinkFn(func(events ...Event) error { + checkDeleted(t, EventActionDelete, events...) + if events[0].Target.Tag != m.Tag { + t.Fatalf("unexpected tag on event target: %q != %q", events[0].Target.Tag, m.Tag) + } + return nil + })) + + repoRef, _ := reference.WithName(repo) + if err := l.TagDeleted(repoRef, m.Tag); err != nil { + t.Fatalf("unexpected error notifying tag deletion: %v", err) + } +} + func createTestEnv(t *testing.T, fn testSinkFn) Listener { pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { @@ -142,14 +160,9 @@ func checkDeleted(t *testing.T, action string, events ...Event) { t.Fatalf("request not equal: %#v != %#v", event.Actor, actor) } - if event.Target.Digest != dgst { - t.Fatalf("unexpected digest on event target: %q != %q", event.Target.Digest, dgst) - } - if event.Target.Repository != repo { t.Fatalf("unexpected repository: %q != %q", event.Target.Repository, repo) } - } func checkCommonManifest(t *testing.T, action string, events ...Event) { diff --git a/notifications/listener.go b/notifications/listener.go index 52ec0ee7..8cfdb67e 100644 --- a/notifications/listener.go +++ b/notifications/listener.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/docker/distribution" + dcontext "github.com/docker/distribution/context" "github.com/docker/distribution/reference" "github.com/opencontainers/go-digest" @@ -25,10 +26,16 @@ type BlobListener interface { BlobDeleted(repo reference.Named, desc digest.Digest) error } +// RepoListener describes a listener that can respond to repository related events. +type RepoListener interface { + TagDeleted(repo reference.Named, tag string) error +} + // Listener combines all repository events into a single interface. type Listener interface { ManifestListener BlobListener + RepoListener } type repositoryListener struct { @@ -214,3 +221,26 @@ func (bwl *blobWriterListener) Commit(ctx context.Context, desc distribution.Des return committed, err } + +type tagServiceListener struct { + distribution.TagService + parent *repositoryListener +} + +func (rl *repositoryListener) Tags(ctx context.Context) distribution.TagService { + return &tagServiceListener{ + TagService: rl.Repository.Tags(ctx), + parent: rl, + } +} + +func (tagSL *tagServiceListener) Untag(ctx context.Context, tag string) error { + if err := tagSL.TagService.Untag(ctx, tag); err != nil { + return err + } + if err := tagSL.parent.listener.TagDeleted(tagSL.parent.Repository.Named(), tag); err != nil { + dcontext.GetLogger(ctx).Errorf("error dispatching tag deleted to listener: %v", err) + return err + } + return nil +} diff --git a/notifications/listener_test.go b/notifications/listener_test.go index a5849807..32a7f6d9 100644 --- a/notifications/listener_test.go +++ b/notifications/listener_test.go @@ -50,12 +50,12 @@ func TestListener(t *testing.T) { "layer:push": 2, "layer:pull": 2, "layer:delete": 2, + "tag:delete": 1, } if !reflect.DeepEqual(tl.ops, expectedOps) { t.Fatalf("counts do not match:\n%v\n !=\n%v", tl.ops, expectedOps) } - } type testListener struct { @@ -64,7 +64,6 @@ type testListener struct { func (tl *testListener) ManifestPushed(repo reference.Named, m distribution.Manifest, options ...distribution.ManifestServiceOption) error { tl.ops["manifest:push"]++ - return nil } @@ -98,6 +97,11 @@ func (tl *testListener) BlobDeleted(repo reference.Named, d digest.Digest) error return nil } +func (tl *testListener) TagDeleted(repo reference.Named, tag string) error { + tl.ops["tag:delete"]++ + return nil +} + // checkExerciseRegistry takes the registry through all of its operations, // carrying out generic checks. func checkExerciseRepository(t *testing.T, repository distribution.Repository) { @@ -200,6 +204,10 @@ func checkExerciseRepository(t *testing.T, repository distribution.Repository) { if err != nil { t.Fatalf("unexpected error deleting blob: %v", err) } + } + err = repository.Tags(ctx).Untag(ctx, m.Tag) + if err != nil { + t.Fatalf("unexpected error deleting tag: %v", err) } }