Browse Source

Merge pull request #1084 from weaveworks/913-images-summary

Add ListImages summary fields
Aaron Kirkbride 1 year ago
parent
commit
883fcabeae
No account linked to committer's email address

+ 18
- 5
api/v6/api.go View File

@@ -42,10 +42,19 @@ type ControllerStatus struct {
42 42
 }
43 43
 
44 44
 type Container struct {
45
-	Name           string
46
-	Current        image.Info
47
-	Available      []image.Info
48
-	AvailableError string `json:",omitempty"`
45
+	Name           string     `json:",omitempty"`
46
+	Current        image.Info `json:",omitempty"`
47
+	LatestFiltered image.Info `json:",omitempty"`
48
+
49
+	// All available images (ignoring tag filters)
50
+	Available               []image.Info `json:",omitempty"`
51
+	AvailableError          string       `json:",omitempty"`
52
+	AvailableImagesCount    int          `json:",omitempty"`
53
+	NewAvailableImagesCount int          `json:",omitempty"`
54
+
55
+	// Filtered available images (matching tag filters)
56
+	FilteredImagesCount    int `json:",omitempty"`
57
+	NewFilteredImagesCount int `json:",omitempty"`
49 58
 }
50 59
 
51 60
 // --- config types
@@ -66,13 +75,17 @@ type Deprecated interface {
66 75
 	SyncNotify(context.Context) error
67 76
 }
68 77
 
78
+type ListImagesOptions struct {
79
+	OverrideContainerFields []string
80
+}
81
+
69 82
 type NotDeprecated interface {
70 83
 	// from v5
71 84
 	Export(context.Context) ([]byte, error)
72 85
 
73 86
 	// v6
74 87
 	ListServices(ctx context.Context, namespace string) ([]ControllerStatus, error)
75
-	ListImages(context.Context, update.ResourceSpec) ([]ImageStatus, error)
88
+	ListImages(ctx context.Context, spec update.ResourceSpec, opts ListImagesOptions) ([]ImageStatus, error)
76 89
 	UpdateManifests(context.Context, update.Spec) (job.ID, error)
77 90
 	SyncStatus(ctx context.Context, ref string) ([]string, error)
78 91
 	JobStatus(context.Context, job.ID) (job.Status, error)

+ 1
- 1
cmd/fluxctl/list_images_cmd.go View File

@@ -67,7 +67,7 @@ func (opts *controllerShowOpts) RunE(cmd *cobra.Command, args []string) error {
67 67
 
68 68
 	ctx := context.Background()
69 69
 
70
-	controllers, err := opts.API.ListImages(ctx, resourceSpec)
70
+	controllers, err := opts.API.ListImages(ctx, resourceSpec, v6.ListImagesOptions{})
71 71
 	if err != nil {
72 72
 		return err
73 73
 	}

+ 106
- 30
daemon/daemon.go View File

@@ -70,35 +70,43 @@ func (d *Daemon) Export(ctx context.Context) ([]byte, error) {
70 70
 	return d.Cluster.Export()
71 71
 }
72 72
 
73
-func (d *Daemon) ListServices(ctx context.Context, namespace string) ([]v6.ControllerStatus, error) {
74
-	clusterServices, err := d.Cluster.AllControllers(namespace)
75
-	if err != nil {
76
-		return nil, errors.Wrap(err, "getting services from cluster")
77
-	}
78
-
73
+func (d *Daemon) getPolicyResourceMap(ctx context.Context) (policy.ResourceMap, v6.ReadOnlyReason, error) {
79 74
 	var services policy.ResourceMap
80 75
 	var globalReadOnly v6.ReadOnlyReason
81
-	err = d.WithClone(ctx, func(checkout *git.Checkout) error {
76
+	err := d.WithClone(ctx, func(checkout *git.Checkout) error {
82 77
 		var err error
83 78
 		services, err = d.Manifests.ServicesWithPolicies(checkout.ManifestDir())
84 79
 		return err
85 80
 	})
81
+
82
+	// Capture errors related to read-only repositories
86 83
 	switch {
87 84
 	case err == git.ErrNotReady:
88 85
 		globalReadOnly = v6.ReadOnlyNotReady
89 86
 	case err == git.ErrNoConfig:
90 87
 		globalReadOnly = v6.ReadOnlyNoRepo
91 88
 	case err != nil:
92
-		return nil, errors.Wrap(err, "getting service policies")
89
+		return nil, globalReadOnly, errors.Wrap(err, "getting service policies")
90
+	}
91
+
92
+	return services, globalReadOnly, nil
93
+}
94
+
95
+func (d *Daemon) ListServices(ctx context.Context, namespace string) ([]v6.ControllerStatus, error) {
96
+	clusterServices, err := d.Cluster.AllControllers(namespace)
97
+	if err != nil {
98
+		return nil, errors.Wrap(err, "getting services from cluster")
99
+	}
100
+
101
+	policyResourceMap, readOnly, err := d.getPolicyResourceMap(ctx)
102
+	if err != nil {
103
+		return nil, err
93 104
 	}
94 105
 
95 106
 	var res []v6.ControllerStatus
96 107
 	for _, service := range clusterServices {
97
-		var readOnly v6.ReadOnlyReason
98
-		policies, ok := services[service.ID]
108
+		policies, ok := policyResourceMap[service.ID]
99 109
 		switch {
100
-		case globalReadOnly != "":
101
-			readOnly = globalReadOnly
102 110
 		case !ok:
103 111
 			readOnly = v6.ReadOnlyMissing
104 112
 		case service.IsSystem:
@@ -130,7 +138,7 @@ func (cs clusterContainers) Containers(i int) []resource.Container {
130 138
 }
131 139
 
132 140
 // List the images available for set of services
133
-func (d *Daemon) ListImages(ctx context.Context, spec update.ResourceSpec) ([]v6.ImageStatus, error) {
141
+func (d *Daemon) ListImages(ctx context.Context, spec update.ResourceSpec, opts v6.ListImagesOptions) ([]v6.ImageStatus, error) {
134 142
 	var services []cluster.Controller
135 143
 	var err error
136 144
 	if spec == update.ResourceSpecAll {
@@ -143,17 +151,25 @@ func (d *Daemon) ListImages(ctx context.Context, spec update.ResourceSpec) ([]v6
143 151
 		services, err = d.Cluster.SomeControllers([]flux.ResourceID{id})
144 152
 	}
145 153
 
146
-	images, err := update.CollectAvailableImages(d.Registry, clusterContainers(services), d.Logger)
154
+	imageRepos, err := update.FetchImageRepos(d.Registry, clusterContainers(services), d.Logger)
147 155
 	if err != nil {
148 156
 		return nil, errors.Wrap(err, "getting images for services")
149 157
 	}
150 158
 
159
+	policyResourceMap, _, err := d.getPolicyResourceMap(ctx)
160
+	if err != nil {
161
+		return nil, err
162
+	}
163
+
151 164
 	var res []v6.ImageStatus
152 165
 	for _, service := range services {
153
-		containers := containersWithAvailable(service, images)
166
+		serviceContainers, err := getServiceContainers(service, imageRepos, policyResourceMap, opts.OverrideContainerFields)
167
+		if err != nil {
168
+			return nil, err
169
+		}
154 170
 		res = append(res, v6.ImageStatus{
155 171
 			ID:         service.ID,
156
-			Containers: containers,
172
+			Containers: serviceContainers,
157 173
 		})
158 174
 	}
159 175
 
@@ -544,23 +560,83 @@ func containers2containers(cs []resource.Container) []v6.Container {
544 560
 	return res
545 561
 }
546 562
 
547
-func containersWithAvailable(service cluster.Controller, images update.ImageMap) (res []v6.Container) {
563
+func getServiceContainers(service cluster.Controller, imageRepos update.ImageRepos, policyResourceMap policy.ResourceMap, fields []string) (res []v6.Container, err error) {
564
+	if len(fields) == 0 {
565
+		fields = []string{
566
+			"Name",
567
+			"Current",
568
+			"LatestFiltered",
569
+			"Available",
570
+			"AvailableError",
571
+			"AvailableImagesCount",
572
+			"NewAvailableImagesCount",
573
+			"FilteredImagesCount",
574
+			"NewFilteredImagesCount",
575
+		}
576
+	}
577
+
548 578
 	for _, c := range service.ContainersOrNil() {
549
-		available := images.Available(c.Image.Name)
550
-		availableErr := ""
551
-		if available == nil {
552
-			availableErr = registry.ErrNoImageData.Error()
579
+		var container v6.Container
580
+
581
+		imageRepo := c.Image.Name
582
+		tagPattern := getTagPattern(policyResourceMap, service.ID, c.Name)
583
+
584
+		images := imageRepos.GetRepoImages(imageRepo)
585
+		currentImage := images.FindWithRef(c.Image)
586
+
587
+		// All images
588
+		imagesCount := len(images)
589
+		imagesErr := ""
590
+		if images == nil {
591
+			imagesErr = registry.ErrNoImageData.Error()
553 592
 		}
554
-		res = append(res, v6.Container{
555
-			Name: c.Name,
556
-			Current: image.Info{
557
-				ID: c.Image,
558
-			},
559
-			Available:      available,
560
-			AvailableError: availableErr,
561
-		})
593
+		var newImages []image.Info
594
+		for _, img := range images {
595
+			if img.CreatedAt.After(currentImage.CreatedAt) {
596
+				newImages = append(newImages, img)
597
+			}
598
+		}
599
+		newImagesCount := len(newImages)
600
+
601
+		// Filtered images
602
+		filteredImages := images.Filter(tagPattern)
603
+		filteredImagesCount := len(filteredImages)
604
+		var newFilteredImages []image.Info
605
+		for _, img := range filteredImages {
606
+			if img.CreatedAt.After(currentImage.CreatedAt) {
607
+				newFilteredImages = append(newFilteredImages, img)
608
+			}
609
+		}
610
+		newFilteredImagesCount := len(newFilteredImages)
611
+
612
+		for _, field := range fields {
613
+			switch field {
614
+			case "Name":
615
+				container.Name = c.Name
616
+			case "Current":
617
+				container.Current = currentImage
618
+			case "LatestFiltered":
619
+				container.LatestFiltered, _ = filteredImages.Latest()
620
+			case "Available":
621
+				container.Available = images
622
+			case "AvailableError":
623
+				container.AvailableError = imagesErr
624
+			case "AvailableImagesCount":
625
+				container.AvailableImagesCount = imagesCount
626
+			case "NewAvailableImagesCount":
627
+				container.NewAvailableImagesCount = newImagesCount
628
+			case "FilteredImagesCount":
629
+				container.FilteredImagesCount = filteredImagesCount
630
+			case "NewFilteredImagesCount":
631
+				container.NewFilteredImagesCount = newFilteredImagesCount
632
+			default:
633
+				return nil, errors.Errorf("%s is an invalid field", field)
634
+			}
635
+		}
636
+		res = append(res, container)
562 637
 	}
563
-	return res
638
+
639
+	return res, nil
564 640
 }
565 641
 
566 642
 func policyCommitMessage(us policy.Updates, cause update.Cause) string {

+ 152
- 28
daemon/daemon_test.go View File

@@ -12,6 +12,7 @@ import (
12 12
 	"time"
13 13
 
14 14
 	"github.com/go-kit/kit/log"
15
+	"github.com/stretchr/testify/assert"
15 16
 
16 17
 	"github.com/weaveworks/flux"
17 18
 	"github.com/weaveworks/flux/api/v6"
@@ -40,6 +41,10 @@ const (
40 41
 	newHelloImage     = "quay.io/weaveworks/helloworld:2"
41 42
 	currentHelloImage = "quay.io/weaveworks/helloworld:master-a000001"
42 43
 
44
+	anotherSvc       = "another:deployment/service"
45
+	anotherContainer = "it-doesn't-matter"
46
+	anotherImage     = "another/service:latest"
47
+
43 48
 	invalidNS   = "adsajkfldsa"
44 49
 	testVersion = "test"
45 50
 )
@@ -137,26 +142,155 @@ func TestDaemon_ListImages(t *testing.T) {
137 142
 
138 143
 	ctx := context.Background()
139 144
 
140
-	// List all images for services
141
-	ss := update.ResourceSpec(update.ResourceSpecAll)
142
-	is, err := d.ListImages(ctx, ss)
143
-	if err != nil {
144
-		t.Fatalf("Error: %s", err.Error())
145
-	}
146
-	ids := imageIDs(is)
147
-	if 3 != len(ids) {
148
-		t.Fatalf("Expected %v but got %v", 3, len(ids))
145
+	specAll := update.ResourceSpec(update.ResourceSpecAll)
146
+
147
+	// Service 1
148
+	svcID, err := flux.ParseResourceID(svc)
149
+	assert.NoError(t, err)
150
+	currentImageRef, err := image.ParseRef(currentHelloImage)
151
+	assert.NoError(t, err)
152
+	newImageRef, err := image.ParseRef(newHelloImage)
153
+	assert.NoError(t, err)
154
+
155
+	// Service 2
156
+	anotherSvcID, err := flux.ParseResourceID(anotherSvc)
157
+	assert.NoError(t, err)
158
+	anotherImageRef, err := image.ParseRef(anotherImage)
159
+	assert.NoError(t, err)
160
+
161
+	tests := []struct {
162
+		name string
163
+		spec update.ResourceSpec
164
+		opts v6.ListImagesOptions
165
+
166
+		expectedImages    []v6.ImageStatus
167
+		expectedNumImages int
168
+		shouldError       bool
169
+	}{
170
+		{
171
+			name: "All services",
172
+			spec: specAll,
173
+			opts: v6.ListImagesOptions{},
174
+			expectedImages: []v6.ImageStatus{
175
+				{
176
+					ID: svcID,
177
+					Containers: []v6.Container{
178
+						{
179
+							Name:           container,
180
+							Current:        image.Info{ID: currentImageRef},
181
+							LatestFiltered: image.Info{ID: currentImageRef},
182
+							Available: []image.Info{
183
+								{ID: currentImageRef},
184
+								{ID: newImageRef},
185
+							},
186
+							AvailableImagesCount:    2,
187
+							NewAvailableImagesCount: 1,
188
+							FilteredImagesCount:     2,
189
+							NewFilteredImagesCount:  1,
190
+						},
191
+					},
192
+				},
193
+				{
194
+					ID: anotherSvcID,
195
+					Containers: []v6.Container{
196
+						{
197
+							Name:           anotherContainer,
198
+							Current:        image.Info{ID: anotherImageRef},
199
+							LatestFiltered: image.Info{},
200
+							Available: []image.Info{
201
+								{ID: anotherImageRef},
202
+							},
203
+							AvailableImagesCount:    1,
204
+							NewAvailableImagesCount: 0,
205
+							FilteredImagesCount:     0, // Excludes latest
206
+							NewFilteredImagesCount:  0,
207
+						},
208
+					},
209
+				},
210
+			},
211
+			shouldError: false,
212
+		},
213
+		{
214
+			name: "Specific service",
215
+			spec: update.ResourceSpec(svc),
216
+			opts: v6.ListImagesOptions{},
217
+			expectedImages: []v6.ImageStatus{
218
+				{
219
+					ID: svcID,
220
+					Containers: []v6.Container{
221
+						{
222
+							Name:           container,
223
+							Current:        image.Info{ID: currentImageRef},
224
+							LatestFiltered: image.Info{ID: currentImageRef},
225
+							Available: []image.Info{
226
+								{ID: currentImageRef},
227
+								{ID: newImageRef},
228
+							},
229
+							AvailableImagesCount:    2,
230
+							NewAvailableImagesCount: 1,
231
+							FilteredImagesCount:     2,
232
+							NewFilteredImagesCount:  1,
233
+						},
234
+					},
235
+				},
236
+			},
237
+			shouldError: false,
238
+		},
239
+		{
240
+			name: "Override container field selection",
241
+			spec: specAll,
242
+			opts: v6.ListImagesOptions{OverrideContainerFields: []string{"Name", "Current", "NewAvailableImagesCount"}},
243
+			expectedImages: []v6.ImageStatus{
244
+				{
245
+					ID: svcID,
246
+					Containers: []v6.Container{
247
+						{
248
+							Name:                    container,
249
+							Current:                 image.Info{ID: currentImageRef},
250
+							NewAvailableImagesCount: 1,
251
+						},
252
+					},
253
+				},
254
+				{
255
+					ID: anotherSvcID,
256
+					Containers: []v6.Container{
257
+						{
258
+							Name:                    anotherContainer,
259
+							Current:                 image.Info{ID: anotherImageRef},
260
+							NewAvailableImagesCount: 0,
261
+						},
262
+					},
263
+				},
264
+			},
265
+			shouldError: false,
266
+		},
267
+		{
268
+			name:           "Override container field selection with invalid field",
269
+			spec:           specAll,
270
+			opts:           v6.ListImagesOptions{OverrideContainerFields: []string{"InvalidField"}},
271
+			expectedImages: nil,
272
+			shouldError:    true,
273
+		},
149 274
 	}
150 275
 
151
-	// List images for specific service
152
-	ss = update.ResourceSpec(svc)
153
-	is, err = d.ListImages(ctx, ss)
154
-	if err != nil {
155
-		t.Fatalf("Error: %s", err.Error())
156
-	}
157
-	ids = imageIDs(is)
158
-	if 2 != len(ids) {
159
-		t.Fatalf("Expected %v but got %v", 2, len(ids))
276
+	for _, tt := range tests {
277
+		t.Run(tt.name, func(t *testing.T) {
278
+			is, err := d.ListImages(ctx, tt.spec, tt.opts)
279
+			assert.Equal(t, tt.shouldError, err != nil)
280
+
281
+			// Clear CreatedAt fields for testing
282
+			for ri, r := range is {
283
+				for ci, c := range r.Containers {
284
+					is[ri].Containers[ci].Current.CreatedAt = time.Time{}
285
+					is[ri].Containers[ci].LatestFiltered.CreatedAt = time.Time{}
286
+					for ai := range c.Available {
287
+						is[ri].Containers[ci].Available[ai].CreatedAt = time.Time{}
288
+					}
289
+				}
290
+			}
291
+
292
+			assert.Equal(t, tt.expectedImages, is)
293
+		})
160 294
 	}
161 295
 }
162 296
 
@@ -558,16 +692,6 @@ func (w *wait) ForSyncStatus(d *Daemon, rev string, expectedNumCommits int) []st
558 692
 	return revs
559 693
 }
560 694
 
561
-func imageIDs(status []v6.ImageStatus) []image.Info {
562
-	var availableImgs []image.Info
563
-	for _, i := range status {
564
-		for _, c := range i.Containers {
565
-			availableImgs = append(availableImgs, c.Available...)
566
-		}
567
-	}
568
-	return availableImgs
569
-}
570
-
571 695
 func updateImage(ctx context.Context, d *Daemon, t *testing.T) job.ID {
572 696
 	return updateManifest(ctx, t, d, update.Spec{
573 697
 		Type: update.Images,

+ 13
- 7
daemon/images.go View File

@@ -18,23 +18,23 @@ func (d *Daemon) pollForNewImages(logger log.Logger) {
18 18
 
19 19
 	ctx := context.Background()
20 20
 
21
-	candidateServices, err := d.unlockedAutomatedServices(ctx)
21
+	candidateServicesPolicyMap, err := d.getUnlockedAutomatedServicesPolicyMap(ctx)
22 22
 	if err != nil {
23 23
 		logger.Log("error", errors.Wrap(err, "getting unlocked automated services"))
24 24
 		return
25 25
 	}
26
-	if len(candidateServices) == 0 {
26
+	if len(candidateServicesPolicyMap) == 0 {
27 27
 		logger.Log("msg", "no automated services")
28 28
 		return
29 29
 	}
30 30
 	// Find images to check
31
-	services, err := d.Cluster.SomeControllers(candidateServices.ToSlice())
31
+	services, err := d.Cluster.SomeControllers(candidateServicesPolicyMap.ToSlice())
32 32
 	if err != nil {
33 33
 		logger.Log("error", errors.Wrap(err, "checking services for new images"))
34 34
 		return
35 35
 	}
36 36
 	// Check the latest available image(s) for each service
37
-	imageMap, err := update.CollectAvailableImages(d.Registry, clusterContainers(services), logger)
37
+	imageRepos, err := update.FetchImageRepos(d.Registry, clusterContainers(services), logger)
38 38
 	if err != nil {
39 39
 		logger.Log("error", errors.Wrap(err, "fetching image updates"))
40 40
 		return
@@ -51,11 +51,13 @@ func (d *Daemon) pollForNewImages(logger log.Logger) {
51 51
 				continue
52 52
 			}
53 53
 
54
-			pattern := getTagPattern(candidateServices, service.ID, container.Name)
54
+			pattern := getTagPattern(candidateServicesPolicyMap, service.ID, container.Name)
55 55
 			repo := currentImageID.Name
56 56
 			logger.Log("repo", repo, "pattern", pattern)
57 57
 
58
-			if latest, ok := imageMap.LatestImage(repo, pattern); ok && latest.ID != currentImageID {
58
+			filteredImages := imageRepos.GetRepoImages(repo).Filter(pattern)
59
+
60
+			if latest, ok := filteredImages.Latest(); ok && latest.ID != currentImageID {
59 61
 				if latest.ID.Tag == "" {
60 62
 					logger.Log("msg", "untagged image in available images", "action", "skip", "available", repo)
61 63
 					continue
@@ -73,6 +75,9 @@ func (d *Daemon) pollForNewImages(logger log.Logger) {
73 75
 }
74 76
 
75 77
 func getTagPattern(services policy.ResourceMap, service flux.ResourceID, container string) string {
78
+	if services == nil {
79
+		return "*"
80
+	}
76 81
 	policies := services[service]
77 82
 	if pattern, ok := policies.Get(policy.TagPrefix(container)); ok {
78 83
 		return strings.TrimPrefix(pattern, "glob:")
@@ -80,7 +85,8 @@ func getTagPattern(services policy.ResourceMap, service flux.ResourceID, contain
80 85
 	return "*"
81 86
 }
82 87
 
83
-func (d *Daemon) unlockedAutomatedServices(ctx context.Context) (policy.ResourceMap, error) {
88
+// getUnlockedAutomatedServicesPolicyMap returns a resource policy map for all unlocked automated services
89
+func (d *Daemon) getUnlockedAutomatedServicesPolicyMap(ctx context.Context) (policy.ResourceMap, error) {
84 90
 	var services policy.ResourceMap
85 91
 	err := d.WithClone(ctx, func(checkout *git.Checkout) error {
86 92
 		var err error

+ 58
- 0
daemon/images_test.go View File

@@ -0,0 +1,58 @@
1
+package daemon
2
+
3
+import (
4
+	"fmt"
5
+	"testing"
6
+
7
+	"github.com/stretchr/testify/assert"
8
+	"github.com/weaveworks/flux"
9
+	"github.com/weaveworks/flux/policy"
10
+)
11
+
12
+func Test_getTagPattern(t *testing.T) {
13
+	resourceID, err := flux.ParseResourceID("default:deployment/helloworld")
14
+	assert.NoError(t, err)
15
+	container := "helloContainer"
16
+
17
+	type args struct {
18
+		services  policy.ResourceMap
19
+		service   flux.ResourceID
20
+		container string
21
+	}
22
+	tests := []struct {
23
+		name string
24
+		args args
25
+		want string
26
+	}{
27
+		{
28
+			name: "Nil policies",
29
+			args: args{services: nil},
30
+			want: "*",
31
+		},
32
+		{
33
+			name: "No match",
34
+			args: args{services: policy.ResourceMap{}},
35
+			want: "*",
36
+		},
37
+		{
38
+			name: "Match",
39
+			args: args{
40
+				services: policy.ResourceMap{
41
+					resourceID: policy.Set{
42
+						policy.Policy(fmt.Sprintf("tag.%s", container)): "glob:master-*",
43
+					},
44
+				},
45
+				service:   resourceID,
46
+				container: container,
47
+			},
48
+			want: "master-*",
49
+		},
50
+	}
51
+	for _, tt := range tests {
52
+		t.Run(tt.name, func(t *testing.T) {
53
+			if got := getTagPattern(tt.args.services, tt.args.service, tt.args.container); got != tt.want {
54
+				t.Errorf("getTagPattern() = %v, want %v", got, tt.want)
55
+			}
56
+		})
57
+	}
58
+}

+ 3
- 3
http/client/client.go View File

@@ -57,9 +57,9 @@ func (c *Client) ListServices(ctx context.Context, namespace string) ([]v6.Contr
57 57
 	return res, err
58 58
 }
59 59
 
60
-func (c *Client) ListImages(ctx context.Context, s update.ResourceSpec) ([]v6.ImageStatus, error) {
60
+func (c *Client) ListImages(ctx context.Context, s update.ResourceSpec, opts v6.ListImagesOptions) ([]v6.ImageStatus, error) {
61 61
 	var res []v6.ImageStatus
62
-	err := c.Get(ctx, &res, transport.ListImages, "service", string(s))
62
+	err := c.Get(ctx, &res, transport.ListImages, "service", string(s), "containerFields", strings.Join(opts.OverrideContainerFields, ","))
63 63
 	return res, err
64 64
 }
65 65
 
@@ -210,7 +210,7 @@ func (c *Client) executeRequest(req *http.Request) (*http.Response, error) {
210 210
 			if err := json.Unmarshal(body, &niceError); err != nil {
211 211
 				return resp, errors.Wrap(err, "decoding response body of error")
212 212
 			}
213
-			 // just in case it's JSON but not one of our own errors
213
+			// just in case it's JSON but not one of our own errors
214 214
 			if niceError.Err != nil {
215 215
 				return resp, &niceError
216 216
 			}

+ 18
- 3
http/daemon/server.go View File

@@ -3,6 +3,7 @@ package daemon
3 3
 import (
4 4
 	"encoding/json"
5 5
 	"net/http"
6
+	"strings"
6 7
 
7 8
 	"github.com/gorilla/mux"
8 9
 	"github.com/pkg/errors"
@@ -11,6 +12,7 @@ import (
11 12
 	"github.com/weaveworks/flux"
12 13
 
13 14
 	"github.com/weaveworks/flux/api"
15
+	"github.com/weaveworks/flux/api/v6"
14 16
 	transport "github.com/weaveworks/flux/http"
15 17
 	"github.com/weaveworks/flux/job"
16 18
 	fluxmetrics "github.com/weaveworks/flux/metrics"
@@ -91,14 +93,27 @@ func (s HTTPServer) SyncStatus(w http.ResponseWriter, r *http.Request) {
91 93
 }
92 94
 
93 95
 func (s HTTPServer) ListImages(w http.ResponseWriter, r *http.Request) {
94
-	service := mux.Vars(r)["service"]
96
+	queryValues := r.URL.Query()
97
+
98
+	// service - Select services to update.
99
+	service := queryValues.Get("service")
100
+	if service == "" {
101
+		service = string(update.ResourceSpecAll)
102
+	}
95 103
 	spec, err := update.ParseResourceSpec(service)
96 104
 	if err != nil {
97 105
 		transport.WriteError(w, r, http.StatusBadRequest, errors.Wrapf(err, "parsing service spec %q", service))
98 106
 		return
99 107
 	}
100 108
 
101
-	d, err := s.server.ListImages(r.Context(), spec)
109
+	// containerFields - Override which fields to return in the container struct.
110
+	var opts v6.ListImagesOptions
111
+	containerFields := queryValues.Get("containerFields")
112
+	if containerFields != "" {
113
+		opts.OverrideContainerFields = strings.Split(containerFields, ",")
114
+	}
115
+
116
+	d, err := s.server.ListImages(r.Context(), spec, opts)
102 117
 	if err != nil {
103 118
 		transport.ErrorResponse(w, r, err)
104 119
 		return
@@ -122,7 +137,7 @@ func (s HTTPServer) UpdateManifests(w http.ResponseWriter, r *http.Request) {
122 137
 }
123 138
 
124 139
 func (s HTTPServer) ListServices(w http.ResponseWriter, r *http.Request) {
125
-	namespace := mux.Vars(r)["namespace"]
140
+	namespace := r.URL.Query().Get("namespace")
126 141
 	res, err := s.server.ListServices(r.Context(), namespace)
127 142
 	if err != nil {
128 143
 		transport.ErrorResponse(w, r, err)

+ 2
- 2
http/transport.go View File

@@ -29,8 +29,8 @@ func DeprecateVersions(r *mux.Router, versions ...string) {
29 29
 func NewAPIRouter() *mux.Router {
30 30
 	r := mux.NewRouter()
31 31
 
32
-	r.NewRoute().Name(ListServices).Methods("GET").Path("/v6/services").Queries("namespace", "{namespace}") // optional namespace!
33
-	r.NewRoute().Name(ListImages).Methods("GET").Path("/v6/images").Queries("service", "{service}")
32
+	r.NewRoute().Name(ListServices).Methods("GET").Path("/v6/services")
33
+	r.NewRoute().Name(ListImages).Methods("GET").Path("/v6/images")
34 34
 
35 35
 	r.NewRoute().Name(UpdateManifests).Methods("POST").Path("/v9/update-manifests")
36 36
 	r.NewRoute().Name(JobStatus).Methods("GET").Path("/v6/jobs").Queries("id", "{id}")

+ 4
- 4
image/image.go View File

@@ -225,16 +225,16 @@ func (i Ref) WithNewTag(t string) Ref {
225 225
 // from its registry.
226 226
 type Info struct {
227 227
 	// the reference to this image; probably a tagged image name
228
-	ID Ref
228
+	ID Ref `json:",omitempty"`
229 229
 	// the digest we got when fetching the metadata, which will be
230 230
 	// different each time a manifest is uploaded for the reference
231
-	Digest string
231
+	Digest string `json:",omitempty"`
232 232
 	// an identifier for the *image* this reference points to; this
233 233
 	// will be the same for references that point at the same image
234 234
 	// (but does not necessarily equal Docker's image ID)
235
-	ImageID string
235
+	ImageID string `json:",omitempty"`
236 236
 	// the time at which the image pointed at was created
237
-	CreatedAt time.Time
237
+	CreatedAt time.Time `json:",omitempty"`
238 238
 }
239 239
 
240 240
 // MarshalJSON returns the Info value in JSON (as bytes). It is

+ 2
- 2
registry/cache/memcached/integration_test.go View File

@@ -66,14 +66,14 @@ Loop:
66 66
 		case <-timeout.C:
67 67
 			t.Fatal("Cache timeout")
68 68
 		case <-tick.C:
69
-			_, err := r.GetRepository(id.Name)
69
+			_, err := r.GetSortedRepositoryImages(id.Name)
70 70
 			if err == nil {
71 71
 				break Loop
72 72
 			}
73 73
 		}
74 74
 	}
75 75
 
76
-	img, err := r.GetRepository(id.Name)
76
+	img, err := r.GetSortedRepositoryImages(id.Name)
77 77
 	if err != nil {
78 78
 		t.Fatal(err)
79 79
 	}

+ 2
- 2
registry/cache/registry.go View File

@@ -31,9 +31,9 @@ type Cache struct {
31 31
 	Reader Reader
32 32
 }
33 33
 
34
-// GetRepository returns the list of image manifests in an image
34
+// GetSortedRepositoryImages returns the list of image manifests in an image
35 35
 // repository (e.g,. at "quay.io/weaveworks/flux")
36
-func (c *Cache) GetRepository(id image.Name) ([]image.Info, error) {
36
+func (c *Cache) GetSortedRepositoryImages(id image.Name) ([]image.Info, error) {
37 37
 	repoKey := NewRepositoryKey(id.CanonicalName())
38 38
 	bytes, _, err := c.Reader.GetKey(repoKey)
39 39
 	if err != nil {

+ 1
- 1
registry/cache/warming_test.go View File

@@ -71,7 +71,7 @@ func TestWarm(t *testing.T) {
71 71
 	warmer.warm(context.TODO(), logger, repo, registry.NoCredentials())
72 72
 
73 73
 	registry := &Cache{Reader: c}
74
-	repoInfo, err := registry.GetRepository(ref.Name)
74
+	repoInfo, err := registry.GetSortedRepositoryImages(ref.Name)
75 75
 	if err != nil {
76 76
 		t.Error(err)
77 77
 	}

+ 1
- 1
registry/mock/mock.go View File

@@ -40,7 +40,7 @@ type Registry struct {
40 40
 	Err    error
41 41
 }
42 42
 
43
-func (m *Registry) GetRepository(id image.Name) ([]image.Info, error) {
43
+func (m *Registry) GetSortedRepositoryImages(id image.Name) ([]image.Info, error) {
44 44
 	var imgs []image.Info
45 45
 	for _, i := range m.Images {
46 46
 		// include only if it's the same repository in the same place

+ 2
- 2
registry/monitoring.go View File

@@ -46,9 +46,9 @@ func NewInstrumentedRegistry(next Registry) Registry {
46 46
 	}
47 47
 }
48 48
 
49
-func (m *instrumentedRegistry) GetRepository(id image.Name) (res []image.Info, err error) {
49
+func (m *instrumentedRegistry) GetSortedRepositoryImages(id image.Name) (res []image.Info, err error) {
50 50
 	start := time.Now()
51
-	res, err = m.next.GetRepository(id)
51
+	res, err = m.next.GetSortedRepositoryImages(id)
52 52
 	registryDuration.With(
53 53
 		fluxmetrics.LabelSuccess, strconv.FormatBool(err == nil),
54 54
 	).Observe(time.Since(start).Seconds())

+ 1
- 1
registry/registry.go View File

@@ -12,7 +12,7 @@ var (
12 12
 
13 13
 // Registry is a store of image metadata.
14 14
 type Registry interface {
15
-	GetRepository(image.Name) ([]image.Info, error)
15
+	GetSortedRepositoryImages(image.Name) ([]image.Info, error)
16 16
 	GetImage(image.Ref) (image.Info, error)
17 17
 }
18 18
 

+ 2
- 2
remote/logging.go View File

@@ -43,13 +43,13 @@ func (p *ErrorLoggingServer) ListServices(ctx context.Context, maybeNamespace st
43 43
 	return p.server.ListServices(ctx, maybeNamespace)
44 44
 }
45 45
 
46
-func (p *ErrorLoggingServer) ListImages(ctx context.Context, spec update.ResourceSpec) (_ []v6.ImageStatus, err error) {
46
+func (p *ErrorLoggingServer) ListImages(ctx context.Context, spec update.ResourceSpec, opts v6.ListImagesOptions) (_ []v6.ImageStatus, err error) {
47 47
 	defer func() {
48 48
 		if err != nil {
49 49
 			p.logger.Log("method", "ListImages", "error", err)
50 50
 		}
51 51
 	}()
52
-	return p.server.ListImages(ctx, spec)
52
+	return p.server.ListImages(ctx, spec, opts)
53 53
 }
54 54
 
55 55
 func (p *ErrorLoggingServer) JobStatus(ctx context.Context, jobID job.ID) (_ job.Status, err error) {

+ 2
- 2
remote/metrics.go View File

@@ -56,14 +56,14 @@ func (i *instrumentedServer) ListServices(ctx context.Context, namespace string)
56 56
 	return i.s.ListServices(ctx, namespace)
57 57
 }
58 58
 
59
-func (i *instrumentedServer) ListImages(ctx context.Context, spec update.ResourceSpec) (_ []v6.ImageStatus, err error) {
59
+func (i *instrumentedServer) ListImages(ctx context.Context, spec update.ResourceSpec, opts v6.ListImagesOptions) (_ []v6.ImageStatus, err error) {
60 60
 	defer func(begin time.Time) {
61 61
 		requestDuration.With(
62 62
 			fluxmetrics.LabelMethod, "ListImages",
63 63
 			fluxmetrics.LabelSuccess, fmt.Sprint(err == nil),
64 64
 		).Observe(time.Since(begin).Seconds())
65 65
 	}(time.Now())
66
-	return i.s.ListImages(ctx, spec)
66
+	return i.s.ListImages(ctx, spec, opts)
67 67
 }
68 68
 
69 69
 func (i *instrumentedServer) UpdateManifests(ctx context.Context, spec update.Spec) (_ job.ID, err error) {

+ 3
- 3
remote/mock.go View File

@@ -65,7 +65,7 @@ func (p *MockServer) ListServices(ctx context.Context, ns string) ([]v6.Controll
65 65
 	return p.ListServicesAnswer, p.ListServicesError
66 66
 }
67 67
 
68
-func (p *MockServer) ListImages(context.Context, update.ResourceSpec) ([]v6.ImageStatus, error) {
68
+func (p *MockServer) ListImages(context.Context, update.ResourceSpec, v6.ListImagesOptions) ([]v6.ImageStatus, error) {
69 69
 	return p.ListImagesAnswer, p.ListImagesError
70 70
 }
71 71
 
@@ -194,7 +194,7 @@ func ServerTestBattery(t *testing.T, wrap func(mock api.UpstreamServer) api.Upst
194 194
 		t.Error("expected error from ListServices, got nil")
195 195
 	}
196 196
 
197
-	ims, err := client.ListImages(ctx, update.ResourceSpecAll)
197
+	ims, err := client.ListImages(ctx, update.ResourceSpecAll, v6.ListImagesOptions{})
198 198
 	if err != nil {
199 199
 		t.Error(err)
200 200
 	}
@@ -202,7 +202,7 @@ func ServerTestBattery(t *testing.T, wrap func(mock api.UpstreamServer) api.Upst
202 202
 		t.Error(fmt.Errorf("expected:\n%#v\ngot:\n%#v", mock.ListImagesAnswer, ims))
203 203
 	}
204 204
 	mock.ListImagesError = fmt.Errorf("list images error")
205
-	if _, err = client.ListImages(ctx, update.ResourceSpecAll); err == nil {
205
+	if _, err = client.ListImages(ctx, update.ResourceSpecAll, v6.ListImagesOptions{}); err == nil {
206 206
 		t.Error("expected error from ListImages, got nil")
207 207
 	}
208 208
 

+ 1
- 1
remote/rpc/baseclient.go View File

@@ -33,7 +33,7 @@ func (bc baseClient) ListServices(context.Context, string) ([]v6.ControllerStatu
33 33
 	return nil, remote.UpgradeNeededError(errors.New("ListServices method not implemented"))
34 34
 }
35 35
 
36
-func (bc baseClient) ListImages(context.Context, update.ResourceSpec) ([]v6.ImageStatus, error) {
36
+func (bc baseClient) ListImages(context.Context, update.ResourceSpec, v6.ListImagesOptions) ([]v6.ImageStatus, error) {
37 37
 	return nil, remote.UpgradeNeededError(errors.New("ListImages method not implemented"))
38 38
 }
39 39
 

+ 1
- 1
remote/rpc/clientV6.go View File

@@ -99,7 +99,7 @@ func (p *RPCClientV6) ListServices(ctx context.Context, namespace string) ([]v6.
99 99
 	return services, err
100 100
 }
101 101
 
102
-func (p *RPCClientV6) ListImages(ctx context.Context, spec update.ResourceSpec) ([]v6.ImageStatus, error) {
102
+func (p *RPCClientV6) ListImages(ctx context.Context, spec update.ResourceSpec, opts v6.ListImagesOptions) ([]v6.ImageStatus, error) {
103 103
 	var images []v6.ImageStatus
104 104
 	if err := requireServiceSpecKinds(spec, supportedKindsV6); err != nil {
105 105
 		return images, remote.UpgradeNeededError(err)

+ 1
- 1
remote/rpc/clientV7.go View File

@@ -62,7 +62,7 @@ func (p *RPCClientV7) ListServices(ctx context.Context, namespace string) ([]v6.
62 62
 	return resp.Result, err
63 63
 }
64 64
 
65
-func (p *RPCClientV7) ListImages(ctx context.Context, spec update.ResourceSpec) ([]v6.ImageStatus, error) {
65
+func (p *RPCClientV7) ListImages(ctx context.Context, spec update.ResourceSpec, opts v6.ListImagesOptions) ([]v6.ImageStatus, error) {
66 66
 	var resp ListImagesResponse
67 67
 	if err := requireServiceSpecKinds(spec, supportedKindsV7); err != nil {
68 68
 		return resp.Result, remote.UpgradeNeededError(err)

+ 1
- 1
remote/rpc/clientV8.go View File

@@ -32,7 +32,7 @@ func NewClientV8(conn io.ReadWriteCloser) *RPCClientV8 {
32 32
 	return &RPCClientV8{NewClientV7(conn)}
33 33
 }
34 34
 
35
-func (p *RPCClientV8) ListImages(ctx context.Context, spec update.ResourceSpec) ([]v6.ImageStatus, error) {
35
+func (p *RPCClientV8) ListImages(ctx context.Context, spec update.ResourceSpec, opts v6.ListImagesOptions) ([]v6.ImageStatus, error) {
36 36
 	var resp ListImagesResponse
37 37
 	if err := requireServiceSpecKinds(spec, supportedKindsV8); err != nil {
38 38
 		return resp.Result, remote.UnsupportedResourceKind(err)

+ 1
- 1
remote/rpc/server.go View File

@@ -89,7 +89,7 @@ type ListImagesResponse struct {
89 89
 }
90 90
 
91 91
 func (p *RPCServer) ListImages(spec update.ResourceSpec, resp *ListImagesResponse) error {
92
-	v, err := p.s.ListImages(context.Background(), spec)
92
+	v, err := p.s.ListImages(context.Background(), spec, v6.ListImagesOptions{})
93 93
 	resp.Result = v
94 94
 	if err != nil {
95 95
 		if err, ok := errors.Cause(err).(*fluxerr.Error); ok {

+ 64
- 44
update/images.go View File

@@ -14,47 +14,67 @@ import (
14 14
 	"github.com/weaveworks/flux/resource"
15 15
 )
16 16
 
17
-type infoMap map[image.CanonicalName][]image.Info
17
+type imageReposMap map[image.CanonicalName]ImageInfos
18 18
 
19
-type ImageMap struct {
20
-	images infoMap
19
+// ImageRepos contains a map of image repositories to their images
20
+type ImageRepos struct {
21
+	imageRepos imageReposMap
21 22
 }
22 23
 
23
-// LatestImage returns the latest releasable image for a repository
24
-// for which the tag matches a given pattern. A releasable image is
25
-// one that is not tagged "latest". (Assumes the available images are
26
-// in descending order of latestness.) If no such image exists,
27
-// returns a zero value and `false`, and the caller can decide whether
28
-// that's an error or not.
29
-func (m ImageMap) LatestImage(repo image.Name, tagGlob string) (image.Info, bool) {
30
-	for _, available := range m.images[repo.CanonicalName()] {
31
-		tag := available.ID.Tag
24
+// GetRepoImages returns image.Info entries for all the images in the
25
+// named image repository.
26
+func (r ImageRepos) GetRepoImages(repo image.Name) ImageInfos {
27
+	if canon, ok := r.imageRepos[repo.CanonicalName()]; ok {
28
+		infos := make([]image.Info, len(canon))
29
+		for i := range canon {
30
+			infos[i] = canon[i]
31
+			infos[i].ID = repo.ToRef(infos[i].ID.Tag)
32
+		}
33
+		return infos
34
+	}
35
+	return nil
36
+}
37
+
38
+// ImageInfos is a list of image.Info which can be filtered.
39
+type ImageInfos []image.Info
40
+
41
+// Filter returns only the images which match the tagGlob.
42
+func (ii ImageInfos) Filter(tagGlob string) ImageInfos {
43
+	var filtered ImageInfos
44
+	for _, i := range ii {
45
+		tag := i.ID.Tag
32 46
 		// Ignore latest if and only if it's not what the user wants.
33 47
 		if !strings.EqualFold(tagGlob, "latest") && strings.EqualFold(tag, "latest") {
34 48
 			continue
35 49
 		}
36 50
 		if glob.Glob(tagGlob, tag) {
37 51
 			var im image.Info
38
-			im = available
39
-			im.ID = repo.ToRef(tag)
40
-			return im, true
52
+			im = i
53
+			filtered = append(filtered, im)
41 54
 		}
42 55
 	}
56
+	return filtered
57
+}
58
+
59
+// Latest returns the latest image from ImageInfos. If no such image exists,
60
+// returns a zero value and `false`, and the caller can decide whether
61
+// that's an error or not.
62
+func (ii ImageInfos) Latest() (image.Info, bool) {
63
+	if len(ii) > 0 {
64
+		return ii[0], true
65
+	}
43 66
 	return image.Info{}, false
44 67
 }
45 68
 
46
-// Available returns image.Info entries for all the images in the
47
-// named image repository.
48
-func (m ImageMap) Available(repo image.Name) []image.Info {
49
-	if canon, ok := m.images[repo.CanonicalName()]; ok {
50
-		infos := make([]image.Info, len(canon))
51
-		for i := range canon {
52
-			infos[i] = canon[i]
53
-			infos[i].ID = repo.ToRef(infos[i].ID.Tag)
69
+// FindWithRef returns image.Info given an image ref. If the image cannot be
70
+// found, it returns the image.Info with the ID provided.
71
+func (ii ImageInfos) FindWithRef(ref image.Ref) image.Info {
72
+	for _, img := range ii {
73
+		if img.ID == ref {
74
+			return img
54 75
 		}
55
-		return infos
56 76
 	}
57
-	return nil
77
+	return image.Info{ID: ref}
58 78
 }
59 79
 
60 80
 // containers represents a collection of things that have containers
@@ -73,50 +93,50 @@ func (cs controllerContainers) Containers(i int) []resource.Container {
73 93
 	return cs[i].Controller.ContainersOrNil()
74 94
 }
75 95
 
76
-// collectUpdateImages is a convenient shim to
77
-// `CollectAvailableImages`.
78
-func collectUpdateImages(registry registry.Registry, updateable []*ControllerUpdate, logger log.Logger) (ImageMap, error) {
79
-	return CollectAvailableImages(registry, controllerContainers(updateable), logger)
96
+// fetchUpdatableImageRepos is a convenient shim to
97
+// `FetchImageRepos`.
98
+func fetchUpdatableImageRepos(registry registry.Registry, updateable []*ControllerUpdate, logger log.Logger) (ImageRepos, error) {
99
+	return FetchImageRepos(registry, controllerContainers(updateable), logger)
80 100
 }
81 101
 
82
-// CollectAvailableImages finds all the known image metadata for
102
+// FetchImageRepos finds all the known image metadata for
83 103
 // containers in the controllers given.
84
-func CollectAvailableImages(reg registry.Registry, cs containers, logger log.Logger) (ImageMap, error) {
85
-	images := infoMap{}
104
+func FetchImageRepos(reg registry.Registry, cs containers, logger log.Logger) (ImageRepos, error) {
105
+	imageRepos := imageReposMap{}
86 106
 	for i := 0; i < cs.Len(); i++ {
87 107
 		for _, container := range cs.Containers(i) {
88
-			images[container.Image.CanonicalName()] = nil
108
+			imageRepos[container.Image.CanonicalName()] = nil
89 109
 		}
90 110
 	}
91
-	for name := range images {
92
-		imageRepo, err := reg.GetRepository(name.Name)
111
+	for repo := range imageRepos {
112
+		sortedRepoImages, err := reg.GetSortedRepositoryImages(repo.Name)
93 113
 		if err != nil {
94 114
 			// Not an error if missing. Use empty images.
95 115
 			if !fluxerr.IsMissing(err) {
96
-				logger.Log("err", errors.Wrapf(err, "fetching image metadata for %s", name))
116
+				logger.Log("err", errors.Wrapf(err, "fetching image metadata for %s", repo))
97 117
 				continue
98 118
 			}
99 119
 		}
100
-		images[name] = imageRepo
120
+		imageRepos[repo] = sortedRepoImages
101 121
 	}
102
-	return ImageMap{images}, nil
122
+	return ImageRepos{imageRepos}, nil
103 123
 }
104 124
 
105
-// Create a map of images. It will check that each image exists.
106
-func exactImages(reg registry.Registry, images []image.Ref) (ImageMap, error) {
107
-	m := infoMap{}
125
+// Create a map of image repos to images. It will check that each image exists.
126
+func exactImageRepos(reg registry.Registry, images []image.Ref) (ImageRepos, error) {
127
+	m := imageReposMap{}
108 128
 	for _, id := range images {
109 129
 		// We must check that the exact images requested actually exist. Otherwise we risk pushing invalid images to git.
110 130
 		exist, err := imageExists(reg, id)
111 131
 		if err != nil {
112
-			return ImageMap{}, errors.Wrap(image.ErrInvalidImageID, err.Error())
132
+			return ImageRepos{}, errors.Wrap(image.ErrInvalidImageID, err.Error())
113 133
 		}
114 134
 		if !exist {
115
-			return ImageMap{}, errors.Wrap(image.ErrInvalidImageID, fmt.Sprintf("image %q does not exist", id))
135
+			return ImageRepos{}, errors.Wrap(image.ErrInvalidImageID, fmt.Sprintf("image %q does not exist", id))
116 136
 		}
117 137
 		m[id.CanonicalName()] = []image.Info{{ID: id}}
118 138
 	}
119
-	return ImageMap{m}, nil
139
+	return ImageRepos{m}, nil
120 140
 }
121 141
 
122 142
 // Checks whether the given image exists in the repository.

+ 8
- 6
update/images_test.go View File

@@ -20,25 +20,27 @@ var (
20 20
 // names (e.g., `index.docker.io/library/alpine`), but we ask
21 21
 // questions in terms of everyday names (e.g., `alpine`).
22 22
 func TestDecanon(t *testing.T) {
23
-	m := ImageMap{infoMap{
23
+	m := ImageRepos{imageReposMap{
24 24
 		name: infos,
25 25
 	}}
26 26
 
27
-	latest, ok := m.LatestImage(mustParseName("weaveworks/helloworld"), "*")
27
+	filteredImages := m.GetRepoImages(mustParseName("weaveworks/helloworld")).Filter("*")
28
+	latest, ok := filteredImages.Latest()
28 29
 	if !ok {
29 30
 		t.Error("did not find latest image")
30 31
 	} else if latest.ID.Name != mustParseName("weaveworks/helloworld") {
31 32
 		t.Error("name did not match what was asked")
32 33
 	}
33 34
 
34
-	latest, ok = m.LatestImage(mustParseName("index.docker.io/weaveworks/helloworld"), "*")
35
+	filteredImages = m.GetRepoImages(mustParseName("index.docker.io/weaveworks/helloworld")).Filter("*")
36
+	latest, ok = filteredImages.Latest()
35 37
 	if !ok {
36 38
 		t.Error("did not find latest image")
37 39
 	} else if latest.ID.Name != mustParseName("index.docker.io/weaveworks/helloworld") {
38 40
 		t.Error("name did not match what was asked")
39 41
 	}
40 42
 
41
-	avail := m.Available(mustParseName("weaveworks/helloworld"))
43
+	avail := m.GetRepoImages(mustParseName("weaveworks/helloworld"))
42 44
 	if len(avail) != len(infos) {
43 45
 		t.Errorf("expected %d available images, got %d", len(infos), len(avail))
44 46
 	}
@@ -50,8 +52,8 @@ func TestDecanon(t *testing.T) {
50 52
 }
51 53
 
52 54
 func TestAvail(t *testing.T) {
53
-	m := ImageMap{infoMap{name: infos}}
54
-	avail := m.Available(mustParseName("weaveworks/goodbyeworld"))
55
+	m := ImageRepos{imageReposMap{name: infos}}
56
+	avail := m.GetRepoImages(mustParseName("weaveworks/goodbyeworld"))
55 57
 	if len(avail) > 0 {
56 58
 		t.Errorf("did not expect available images, but got %#v", avail)
57 59
 	}

+ 6
- 5
update/release.go View File

@@ -188,20 +188,20 @@ func (s ReleaseSpec) markSkipped(results Result) {
188 188
 // if not, it indicates there's likely some problem with the running
189 189
 // system vs the definitions given in the repo.)
190 190
 func (s ReleaseSpec) calculateImageUpdates(rc ReleaseContext, candidates []*ControllerUpdate, results Result, logger log.Logger) ([]*ControllerUpdate, error) {
191
-	// Compile an `ImageMap` of all relevant images
192
-	var images ImageMap
191
+	// Compile an `ImageRepos` of all relevant images
192
+	var imageRepos ImageRepos
193 193
 	var singleRepo image.CanonicalName
194 194
 	var err error
195 195
 
196 196
 	switch s.ImageSpec {
197 197
 	case ImageSpecLatest:
198
-		images, err = collectUpdateImages(rc.Registry(), candidates, logger)
198
+		imageRepos, err = fetchUpdatableImageRepos(rc.Registry(), candidates, logger)
199 199
 	default:
200 200
 		var ref image.Ref
201 201
 		ref, err = s.ImageSpec.AsRef()
202 202
 		if err == nil {
203 203
 			singleRepo = ref.CanonicalName()
204
-			images, err = exactImages(rc.Registry(), []image.Ref{ref})
204
+			imageRepos, err = exactImageRepos(rc.Registry(), []image.Ref{ref})
205 205
 		}
206 206
 	}
207 207
 
@@ -231,7 +231,8 @@ func (s ReleaseSpec) calculateImageUpdates(rc ReleaseContext, candidates []*Cont
231 231
 		for _, container := range containers {
232 232
 			currentImageID := container.Image
233 233
 
234
-			latestImage, ok := images.LatestImage(currentImageID.Name, "*")
234
+			filteredImages := imageRepos.GetRepoImages(currentImageID.Name).Filter("*")
235
+			latestImage, ok := filteredImages.Latest()
235 236
 			if !ok {
236 237
 				if currentImageID.CanonicalName() != singleRepo {
237 238
 					ignoredOrSkipped = ReleaseStatusIgnored

Loading…
Cancel
Save