Skip to content

Commit 34bc600

Browse files
committed
distribution client: return structured response for untagged and deleted models (docker#107)
Signed-off-by: Dorin Geman <dorin.geman@docker.com>
1 parent a20c7c8 commit 34bc600

File tree

6 files changed

+76
-49
lines changed

6 files changed

+76
-49
lines changed

pkg/distribution/distribution/client.go

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"io"
77
"net/http"
88
"os"
9-
"strings"
109

1110
"github.com/sirupsen/logrus"
1211

@@ -243,51 +242,62 @@ func (c *Client) GetModel(reference string) (types.Model, error) {
243242
return model, nil
244243
}
245244

245+
type DeleteModelAction struct {
246+
Untagged *string `json:"Untagged,omitempty"`
247+
Deleted *string `json:"Deleted,omitempty"`
248+
}
249+
250+
type DeleteModelResponse []DeleteModelAction
251+
246252
// DeleteModel deletes a model
247-
func (c *Client) DeleteModel(reference string, force bool) (string, error) {
253+
func (c *Client) DeleteModel(reference string, force bool) (*DeleteModelResponse, error) {
248254
mdl, err := c.store.Read(reference)
249255
if err != nil {
250-
return "", err
256+
return &DeleteModelResponse{}, err
251257
}
252258
id, err := mdl.ID()
253259
if err != nil {
254-
return "", fmt.Errorf("getting model ID: %w", err)
260+
return &DeleteModelResponse{}, fmt.Errorf("getting model ID: %w", err)
255261
}
256262
isTag := id != reference
257263

264+
resp := DeleteModelResponse{}
265+
258266
if isTag {
259267
c.log.Infoln("Untagging model:", reference)
260-
if err := c.store.RemoveTags([]string{reference}); err != nil {
268+
tags, err := c.store.RemoveTags([]string{reference})
269+
if err != nil {
261270
c.log.Errorln("Failed to untag model:", err, "tag:", reference)
262-
return "", fmt.Errorf("untagging model: %w", err)
271+
return &DeleteModelResponse{}, fmt.Errorf("untagging model: %w", err)
263272
}
264-
}
265-
266-
if len(mdl.Tags()) > 1 {
267-
if isTag {
268-
// TODO: This should return the full matching tag.
269-
return fmt.Sprintf("Untagged: %s\n", reference), nil
270-
} else if !force {
271-
// if the reference is not a tag and there are multiple tags, return an error unless forced
272-
return "", fmt.Errorf(
273-
"unable to delete %q (must be forced) due to multiple tag references: %w",
274-
reference, ErrConflict,
275-
)
273+
for _, t := range tags {
274+
resp = append(resp, DeleteModelAction{Untagged: &t})
275+
}
276+
if len(mdl.Tags()) > 1 {
277+
return &resp, nil
276278
}
277279
}
278280

279-
var untaggedInfo strings.Builder
280-
for _, tag := range mdl.Tags() {
281-
untaggedInfo.WriteString(fmt.Sprintf("Untagged: %s\n", tag))
281+
if len(mdl.Tags()) > 1 && !force {
282+
// if the reference is not a tag and there are multiple tags, return an error unless forced
283+
return &DeleteModelResponse{}, fmt.Errorf(
284+
"unable to delete %q (must be forced) due to multiple tag references: %w",
285+
reference, ErrConflict,
286+
)
282287
}
283288

284289
c.log.Infoln("Deleting model:", id)
285-
if err := c.store.Delete(id); err != nil {
290+
deletedID, tags, err := c.store.Delete(id)
291+
if err != nil {
286292
c.log.Errorln("Failed to delete model:", err, "tag:", reference)
287-
return "", fmt.Errorf("deleting model: %w", err)
293+
return &DeleteModelResponse{}, fmt.Errorf("deleting model: %w", err)
288294
}
289295
c.log.Infoln("Successfully deleted model:", reference)
290-
return untaggedInfo.String(), nil
296+
for _, t := range tags {
297+
resp = append(resp, DeleteModelAction{Untagged: &t})
298+
}
299+
resp = append(resp, DeleteModelAction{Deleted: &deletedID})
300+
return &resp, nil
291301
}
292302

293303
// Tag adds a tag to a model

pkg/distribution/distribution/delete_test.go

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

33
import (
4+
"encoding/json"
45
"errors"
56
"os"
67
"slices"
@@ -123,26 +124,33 @@ func TestDeleteModel(t *testing.T) {
123124
}
124125

125126
// Attempt to delete the model and check for expected error
126-
out, err := client.DeleteModel(tc.ref, tc.force)
127+
resp, err := client.DeleteModel(tc.ref, tc.force)
127128
if !errors.Is(err, tc.expectedErr) {
128129
t.Fatalf("Expected error %v, got: %v", tc.expectedErr, err)
129130
}
130131
if tc.expectedErr != nil {
131132
return
132133
}
133134

134-
expectedOut := ""
135+
expectedOut := DeleteModelResponse{}
135136
if slices.Contains(tc.tags, tc.ref) {
136137
// tc.ref is a tag
137-
expectedOut = "Untagged: " + tc.ref + "\n"
138+
ref := "index.docker.io/library/" + tc.ref
139+
expectedOut = append(expectedOut, DeleteModelAction{Untagged: &ref})
140+
if !tc.untagOnly {
141+
expectedOut = append(expectedOut, DeleteModelAction{Deleted: &id})
142+
}
138143
} else {
139144
// tc.ref is an ID
140145
for _, tag := range tc.tags {
141-
expectedOut += "Untagged: " + tag + "\n"
146+
expectedOut = append(expectedOut, DeleteModelAction{Untagged: &tag})
142147
}
148+
expectedOut = append(expectedOut, DeleteModelAction{Deleted: &tc.ref})
143149
}
144-
if expectedOut != out {
145-
t.Fatalf("Expected output %q, got: %q", expectedOut, out)
150+
expectedOutJson, _ := json.Marshal(expectedOut)
151+
respJson, _ := json.Marshal(resp)
152+
if string(expectedOutJson) != string(respJson) {
153+
t.Fatalf("Expected output %s, got: %s", expectedOutJson, respJson)
146154
}
147155

148156
// Verify model ref unreachable by ref (untagged)

pkg/distribution/internal/store/index.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ func (i Index) Tag(reference string, tag string) (Index, error) {
3939
return result, nil
4040
}
4141

42-
func (i Index) UnTag(tag string) Index {
42+
func (i Index) UnTag(tag string) (name.Tag, Index) {
4343
tagRef, err := name.NewTag(tag)
4444
if err != nil {
45-
return Index{}
45+
return name.Tag{}, Index{}
4646
}
4747

4848
result := Index{
@@ -52,7 +52,7 @@ func (i Index) UnTag(tag string) Index {
5252
result.Models = append(result.Models, entry.UnTag(tagRef))
5353
}
5454

55-
return result
55+
return tagRef, result
5656
}
5757

5858
func (i Index) Find(reference string) (IndexEntry, int, bool) {

pkg/distribution/internal/store/index_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,12 +146,15 @@ func TestUntag(t *testing.T) {
146146
},
147147
},
148148
}
149-
idx = idx.UnTag("other-tag")
149+
tag, idx := idx.UnTag("other-tag")
150150
if len(idx.Models) != 2 {
151151
t.Fatalf("Expected 2 models, got %d", len(idx.Models))
152152
}
153153
if len(idx.Models[0].Tags) != 1 {
154154
t.Fatalf("Expected 1 tag, got %d", len(idx.Models[0].Tags))
155155
}
156+
if tag.String() != "other-tag" {
157+
t.Fatalf("Expected tag 'other-tag', got '%s'", tag)
158+
}
156159
})
157160
}

pkg/distribution/internal/store/store.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"path/filepath"
88

99
"github.com/docker/model-distribution/internal/progress"
10-
10+
"github.com/google/go-containerregistry/pkg/name"
1111
v1 "github.com/google/go-containerregistry/pkg/v1"
1212
)
1313

@@ -93,14 +93,14 @@ func (s *LocalStore) List() ([]IndexEntry, error) {
9393
}
9494

9595
// Delete deletes a model by reference
96-
func (s *LocalStore) Delete(ref string) error {
96+
func (s *LocalStore) Delete(ref string) (string, []string, error) {
9797
idx, err := s.readIndex()
9898
if err != nil {
99-
return fmt.Errorf("reading models file: %w", err)
99+
return "", nil, fmt.Errorf("reading models file: %w", err)
100100
}
101101
model, _, ok := idx.Find(ref)
102102
if !ok {
103-
return ErrModelNotFound
103+
return "", nil, ErrModelNotFound
104104
}
105105

106106
// Remove manifest file
@@ -140,7 +140,7 @@ func (s *LocalStore) Delete(ref string) error {
140140

141141
idx = idx.Remove(model.ID)
142142

143-
return s.writeIndex(idx)
143+
return model.ID, model.Tags, s.writeIndex(idx)
144144
}
145145

146146
// AddTags adds tags to an existing model
@@ -160,15 +160,18 @@ func (s *LocalStore) AddTags(ref string, newTags []string) error {
160160
}
161161

162162
// RemoveTags removes tags from models
163-
func (s *LocalStore) RemoveTags(tags []string) error {
163+
func (s *LocalStore) RemoveTags(tags []string) ([]string, error) {
164164
index, err := s.readIndex()
165165
if err != nil {
166-
return fmt.Errorf("reading modelss index: %w", err)
166+
return nil, fmt.Errorf("reading modelss index: %w", err)
167167
}
168+
var tagRef name.Tag
169+
var tagRefs []string
168170
for _, tag := range tags {
169-
index = index.UnTag(tag)
171+
tagRef, index = index.UnTag(tag)
172+
tagRefs = append(tagRefs, tagRef.Name())
170173
}
171-
return s.writeIndex(index)
174+
return tagRefs, s.writeIndex(index)
172175
}
173176

174177
// Version returns the store version

pkg/distribution/internal/store/store_test.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,13 @@ func TestStoreAPI(t *testing.T) {
151151

152152
// Test RemoveTags
153153
t.Run("RemoveTags", func(t *testing.T) {
154-
err := s.RemoveTags([]string{"api-model:api-v1.0"})
154+
tags, err := s.RemoveTags([]string{"api-model:api-v1.0"})
155155
if err != nil {
156156
t.Fatalf("RemoveTags failed: %v", err)
157157
}
158+
if tags[0] != "index.docker.io/library/api-model:api-v1.0" {
159+
t.Fatalf("Expected removed tag 'index.docker.io/library/api-model:api-v1.0', got '%s'", tags[0])
160+
}
158161

159162
// Verify tag was removed from list
160163
models, err := s.List()
@@ -178,7 +181,7 @@ func TestStoreAPI(t *testing.T) {
178181

179182
// Test Delete
180183
t.Run("Delete", func(t *testing.T) {
181-
err := s.Delete("api-model:latest")
184+
_, _, err := s.Delete("api-model:latest")
182185
if err != nil {
183186
t.Fatalf("Delete failed: %v", err)
184187
}
@@ -192,7 +195,7 @@ func TestStoreAPI(t *testing.T) {
192195

193196
// Test Delete Non Existent Model
194197
t.Run("Delete", func(t *testing.T) {
195-
err := s.Delete("non-existent-model:latest")
198+
_, _, err := s.Delete("non-existent-model:latest")
196199
if !errors.Is(err, store.ErrModelNotFound) {
197200
t.Fatalf("Expected ErrModelNotFound, got %v", err)
198201
}
@@ -242,7 +245,7 @@ func TestStoreAPI(t *testing.T) {
242245
}
243246

244247
// Delete the model
245-
if err := s.Delete("blob-test:latest"); err != nil {
248+
if _, _, err := s.Delete("blob-test:latest"); err != nil {
246249
t.Fatalf("Delete failed: %v", err)
247250
}
248251

@@ -320,7 +323,7 @@ func TestStoreAPI(t *testing.T) {
320323
}
321324

322325
// Delete the first model
323-
if err := s.Delete("shared-model-1:latest"); err != nil {
326+
if _, _, err := s.Delete("shared-model-1:latest"); err != nil {
324327
t.Fatalf("Delete first model failed: %v", err)
325328
}
326329

@@ -368,7 +371,7 @@ func TestStoreAPI(t *testing.T) {
368371
}
369372

370373
// Delete the second model
371-
if err := s.Delete("shared-model-2:latest"); err != nil {
374+
if _, _, err := s.Delete("shared-model-2:latest"); err != nil {
372375
t.Fatalf("Delete second model failed: %v", err)
373376
}
374377

0 commit comments

Comments
 (0)