diff --git a/daemon/images.go b/daemon/images.go index afaa75863..d74d0ed46 100644 --- a/daemon/images.go +++ b/daemon/images.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/weaveworks/flux" + "github.com/weaveworks/flux/cluster" "github.com/weaveworks/flux/policy" "github.com/weaveworks/flux/resource" "github.com/weaveworks/flux/update" @@ -40,46 +41,7 @@ func (d *Daemon) pollForNewImages(logger log.Logger) { return } - changes := &update.Automated{} - for _, workload := range workloads { - var p policy.Set - if resource, ok := candidateWorkloads[workload.ID]; ok { - p = resource.Policies() - } - containers: - for _, container := range workload.ContainersOrNil() { - currentImageID := container.Image - pattern := policy.GetTagPattern(p, container.Name) - repo := currentImageID.Name - logger := log.With(logger, "workload", workload.ID, "container", container.Name, "repo", repo, "pattern", pattern, "current", currentImageID) - - filteredImages := imageRepos.GetRepoImages(repo).FilterAndSort(pattern) - - if latest, ok := filteredImages.Latest(); ok && latest.ID != currentImageID { - if latest.ID.Tag == "" { - logger.Log("warning", "untagged image in available images", "action", "skip container") - continue containers - } - currentCreatedAt := "" - for _, info := range filteredImages { - if info.CreatedAt.IsZero() { - logger.Log("warning", "image with zero created timestamp", "image", info.ID, "action", "skip container") - continue containers - } - if info.ID == currentImageID { - currentCreatedAt = info.CreatedAt.String() - } - } - if currentCreatedAt == "" { - currentCreatedAt = "filtered out or missing" - logger.Log("warning", "current image not in filtered images", "action", "proceed anyway") - } - newImage := currentImageID.WithNewTag(latest.ID.Tag) - changes.Add(workload.ID, container, newImage) - logger.Log("info", "added update to automation run", "new", newImage, "reason", fmt.Sprintf("latest %s (%s) > current %s (%s)", latest.ID.Tag, latest.CreatedAt, currentImageID.Tag, currentCreatedAt)) - } - } - } + changes := calculateChanges(logger, candidateWorkloads, workloads, imageRepos) if len(changes.Changes) > 0 { d.UpdateManifests(ctx, update.Spec{Type: update.Auto, Spec: changes}) @@ -113,3 +75,41 @@ func (d *Daemon) getAllowedAutomatedResources(ctx context.Context) (resources, e } return result, nil } + +func calculateChanges(logger log.Logger, candidateWorkloads resources, workloads []cluster.Workload, imageRepos update.ImageRepos) *update.Automated { + changes := &update.Automated{} + + for _, workload := range workloads { + var p policy.Set + if resource, ok := candidateWorkloads[workload.ID]; ok { + p = resource.Policies() + } + containers: + for _, container := range workload.ContainersOrNil() { + currentImageID := container.Image + pattern := policy.GetTagPattern(p, container.Name) + repo := currentImageID.Name + logger := log.With(logger, "workload", workload.ID, "container", container.Name, "repo", repo, "pattern", pattern, "current", currentImageID) + + images := imageRepos.GetRepoImages(repo) + filteredImages := images.FilterAndSort(pattern) + + if latest, ok := filteredImages.Latest(); ok && latest.ID != currentImageID { + if latest.ID.Tag == "" { + logger.Log("warning", "untagged image in available images", "action", "skip container") + continue containers + } + current := images.FindWithRef(currentImageID) + if current.CreatedAt.IsZero() || latest.CreatedAt.IsZero() { + logger.Log("warning", "image with zero created timestamp", "current", fmt.Sprintf("%s (%s)", current.ID, current.CreatedAt), "latest", fmt.Sprintf("%s (%s)", latest.ID, latest.CreatedAt), "action", "skip container") + continue containers + } + newImage := currentImageID.WithNewTag(latest.ID.Tag) + changes.Add(workload.ID, container, newImage) + logger.Log("info", "added update to automation run", "new", newImage, "reason", fmt.Sprintf("latest %s (%s) > current %s (%s)", latest.ID.Tag, latest.CreatedAt, currentImageID.Tag, current.CreatedAt)) + } + } + } + + return changes +} diff --git a/daemon/images_test.go b/daemon/images_test.go new file mode 100644 index 000000000..2d2295f25 --- /dev/null +++ b/daemon/images_test.go @@ -0,0 +1,210 @@ +package daemon + +import ( + "github.com/weaveworks/flux/policy" + "testing" + "time" + + "github.com/go-kit/kit/log" + "github.com/weaveworks/flux" + "github.com/weaveworks/flux/cluster" + "github.com/weaveworks/flux/image" + "github.com/weaveworks/flux/registry" + registryMock "github.com/weaveworks/flux/registry/mock" + "github.com/weaveworks/flux/resource" + "github.com/weaveworks/flux/update" +) + +const ( + container1 = "container1" + container2 = "container2" + + currentContainer1Image = "container1/application:current" + newContainer1Image = "container1/application:new" + + currentContainer2Image = "container2/application:current" + newContainer2Image = "container2/application:new" + noTagContainer2Image = "container2/application" +) + +type candidate struct { + resourceID flux.ResourceID + policies policy.Set +} + +func (c candidate) ResourceID() flux.ResourceID { + return c.resourceID +} + +func (c candidate) Policies() policy.Set { + return c.policies +} + +func (candidate) Source() string { + return "" +} + +func (candidate) Bytes() []byte { + return []byte{} +} + +func TestCalculateChanges_Automated(t *testing.T) { + logger := log.NewNopLogger() + resourceID := flux.MakeResourceID(ns, "deployment", "application") + candidateWorkloads := resources{ + resourceID: candidate{ + resourceID: resourceID, + policies: policy.Set{ + policy.Automated: "true", + }, + }, + } + workloads := []cluster.Workload{ + cluster.Workload{ + ID: resourceID, + Containers: cluster.ContainersOrExcuse{ + Containers: []resource.Container{ + { + Name: container1, + Image: mustParseImageRef(currentContainer1Image), + }, + }, + }, + }, + } + var imageRegistry registry.Registry + { + current := makeImageInfo(currentContainer1Image, time.Now()) + new := makeImageInfo(newContainer1Image, time.Now().Add(1*time.Second)) + imageRegistry = ®istryMock.Registry{ + Images: []image.Info{ + current, + new, + }, + } + } + imageRepos, err := update.FetchImageRepos(imageRegistry, clusterContainers(workloads), logger) + if err != nil { + t.Fatal(err) + } + + changes := calculateChanges(logger, candidateWorkloads, workloads, imageRepos) + + if len := len(changes.Changes); len != 1 { + t.Errorf("Expected exactly 1 change, got %d changes", len) + } else if newImage := changes.Changes[0].ImageID.String(); newImage != newContainer1Image { + t.Errorf("Expected changed image to be %s, got %s", newContainer1Image, newImage) + } +} +func TestCalculateChanges_UntaggedImage(t *testing.T) { + logger := log.NewNopLogger() + resourceID := flux.MakeResourceID(ns, "deployment", "application") + candidateWorkloads := resources{ + resourceID: candidate{ + resourceID: resourceID, + policies: policy.Set{ + policy.Automated: "true", + }, + }, + } + workloads := []cluster.Workload{ + cluster.Workload{ + ID: resourceID, + Containers: cluster.ContainersOrExcuse{ + Containers: []resource.Container{ + { + Name: container1, + Image: mustParseImageRef(currentContainer1Image), + }, + { + Name: container2, + Image: mustParseImageRef(currentContainer2Image), + }, + }, + }, + }, + } + var imageRegistry registry.Registry + { + current1 := makeImageInfo(currentContainer1Image, time.Now()) + new1 := makeImageInfo(newContainer1Image, time.Now().Add(1*time.Second)) + current2 := makeImageInfo(currentContainer2Image, time.Now()) + noTag2 := makeImageInfo(noTagContainer2Image, time.Now().Add(1*time.Second)) + imageRegistry = ®istryMock.Registry{ + Images: []image.Info{ + current1, + new1, + current2, + noTag2, + }, + } + } + imageRepos, err := update.FetchImageRepos(imageRegistry, clusterContainers(workloads), logger) + if err != nil { + t.Fatal(err) + } + + changes := calculateChanges(logger, candidateWorkloads, workloads, imageRepos) + + if len := len(changes.Changes); len != 1 { + t.Errorf("Expected exactly 1 change, got %d changes", len) + } else if newImage := changes.Changes[0].ImageID.String(); newImage != newContainer1Image { + t.Errorf("Expected changed image to be %s, got %s", newContainer1Image, newImage) + } +} +func TestCalculateChanges_ZeroTimestamp(t *testing.T) { + logger := log.NewNopLogger() + resourceID := flux.MakeResourceID(ns, "deployment", "application") + candidateWorkloads := resources{ + resourceID: candidate{ + resourceID: resourceID, + policies: policy.Set{ + policy.Automated: "true", + }, + }, + } + workloads := []cluster.Workload{ + cluster.Workload{ + ID: resourceID, + Containers: cluster.ContainersOrExcuse{ + Containers: []resource.Container{ + { + Name: container1, + Image: mustParseImageRef(currentContainer1Image), + }, + { + Name: container2, + Image: mustParseImageRef(currentContainer2Image), + }, + }, + }, + }, + } + var imageRegistry registry.Registry + { + current1 := makeImageInfo(currentContainer1Image, time.Now()) + new1 := makeImageInfo(newContainer1Image, time.Now().Add(1*time.Second)) + zeroTimestampCurrent2 := image.Info{ID: mustParseImageRef(currentContainer2Image)} + new2 := makeImageInfo(newContainer2Image, time.Now().Add(1*time.Second)) + imageRegistry = ®istryMock.Registry{ + Images: []image.Info{ + current1, + new1, + zeroTimestampCurrent2, + new2, + }, + } + } + imageRepos, err := update.FetchImageRepos(imageRegistry, clusterContainers(workloads), logger) + if err != nil { + t.Fatal(err) + } + + changes := calculateChanges(logger, candidateWorkloads, workloads, imageRepos) + + if len := len(changes.Changes); len != 1 { + t.Errorf("Expected exactly 1 change, got %d changes", len) + } else if newImage := changes.Changes[0].ImageID.String(); newImage != newContainer1Image { + t.Errorf("Expected changed image to be %s, got %s", newContainer1Image, newImage) + } +}