From 5984349df3c492010603c10fe0ebc78983e8ce73 Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Wed, 11 Nov 2020 15:12:59 +0100 Subject: [PATCH] Implement same_tag versioning mode --- .gitignore | 1 + Dockerfile | 2 +- cmd/lookbuilding/main.go | 2 + internal/pkg/docker/helpers.go | 1 - internal/pkg/docker/mode.go | 126 ++++++++++++++++++++++++++++++++ internal/pkg/docker/rpc.go | 95 +++++++++++++++--------- internal/pkg/docker/types.go | 8 +- internal/pkg/logging/logger.go | 2 +- internal/pkg/registry/cache.go | 103 ++++++++++++++++---------- internal/pkg/registry/client.go | 11 ++- internal/pkg/versioning/mode.go | 99 ------------------------- internal/pkg/worker/worker.go | 24 +++--- 12 files changed, 278 insertions(+), 196 deletions(-) create mode 100644 .gitignore create mode 100644 internal/pkg/docker/mode.go delete mode 100644 internal/pkg/versioning/mode.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/Dockerfile b/Dockerfile index 705a941..9386fb1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ RUN go mod download # Copy source and build app COPY . /app -RUN go build . +RUN go build hulthe.net/lookbuilding/cmd/lookbuilding FROM alpine diff --git a/cmd/lookbuilding/main.go b/cmd/lookbuilding/main.go index 810ae0c..7b3d503 100644 --- a/cmd/lookbuilding/main.go +++ b/cmd/lookbuilding/main.go @@ -12,6 +12,8 @@ import ( const EnvAddr = "LOOKBUILDING_ADDR" func main() { + //l.Logger.Level = logrus.DebugLevel + addr, isPresent := os.LookupEnv(EnvAddr) if !isPresent { addr = "0.0.0.0:8000" diff --git a/internal/pkg/docker/helpers.go b/internal/pkg/docker/helpers.go index b140787..e27ca01 100644 --- a/internal/pkg/docker/helpers.go +++ b/internal/pkg/docker/helpers.go @@ -42,7 +42,6 @@ func (lc LabeledContainer) GetName() string { } } - func CombineImageParts(owner *string, repository string, tag *string) string { image := repository if owner != nil { diff --git a/internal/pkg/docker/mode.go b/internal/pkg/docker/mode.go new file mode 100644 index 0000000..5e950a6 --- /dev/null +++ b/internal/pkg/docker/mode.go @@ -0,0 +1,126 @@ +package docker + +import ( + "sort" + + l "hulthe.net/lookbuilding/internal/pkg/logging" + "hulthe.net/lookbuilding/internal/pkg/registry" + "hulthe.net/lookbuilding/internal/pkg/semver" +) + +const VersioningModeLabel = "lookbuilding.mode" + +type VersioningMode interface { + Label() string + ShouldUpdate(container LabeledContainer, availableTags []registry.Tag) *registry.Tag +} + +type SameTag struct{} +type SemVerMajor struct{} +type SemVerMinor struct{} +type SemVerPatch struct{} + +var ( + AllModes = [...]VersioningMode{ + SameTag{}, + SemVerMajor{}, + SemVerMinor{}, + SemVerPatch{}, + } +) + +func (SameTag) Label() string { return "same_tag" } +func (SameTag) ShouldUpdate(container LabeledContainer, availableTags []registry.Tag) *registry.Tag { + _, _, currentTag := container.SplitImageParts() + if currentTag == nil { + l.Logger.Errorf("container %s has no tag, won't try to update", container.GetName()) + return nil + } + + for _, tag := range availableTags { + if tag.Name == *currentTag { + remoteDigest, err := tag.GetDigest() + if err != nil { + l.Logger.Errorf("failed to get digest for tag %s", tag.Name) + l.Logger.Error(err) + } + + l.Logger.Debug(remoteDigest.String(), container.ImageDigest) + if container.ImageDigest != remoteDigest { + return &tag + } + return nil + } + } + + return nil +} + +func semVerShouldUpdate(container LabeledContainer, availableTags []registry.Tag, isValid func(current, available semver.Tag) bool) *registry.Tag { + _, _, currentTag := container.SplitImageParts() + if currentTag == nil { + l.Logger.Errorf("container %s has no tag, won't try to update", container.GetName()) + return nil + } + + currentSemVer := semver.ParseTagAsSemVer(*currentTag) + if currentSemVer == nil { + return nil + } + + semverTags := make([]registry.Tag, 0) + + for _, tag := range availableTags { + if tag.SemVer != nil && isValid(*currentSemVer, *tag.SemVer) { + semverTags = append(semverTags, tag) + } + } + + if len(semverTags) == 0 { + return nil + } + + sort.Slice(semverTags, func(i, j int) bool { + a := semverTags[i].SemVer.Version + b := semverTags[j].SemVer.Version + return b.LessThan(a) + }) + + return &semverTags[0] +} + +func (SemVerMajor) Label() string { return "semver_major" } +func (SemVerMajor) ShouldUpdate(container LabeledContainer, availableTags []registry.Tag) *registry.Tag { + return semVerShouldUpdate(container, availableTags, func(current, available semver.Tag) bool { + // The new version should be greater + return current.Version.LessThan(available.Version) + }) +} + +func (SemVerMinor) Label() string { return "semver_minor" } +func (SemVerMinor) ShouldUpdate(container LabeledContainer, availableTags []registry.Tag) *registry.Tag { + return semVerShouldUpdate(container, availableTags, func(current, available semver.Tag) bool { + // The new version should be greater, but still the same major number + return current.Version.LessThan(available.Version) && + current.Version.Major == available.Version.Major + }) +} + +func (SemVerPatch) Label() string { return "semver_patch" } +func (SemVerPatch) ShouldUpdate(container LabeledContainer, availableTags []registry.Tag) *registry.Tag { + return semVerShouldUpdate(container, availableTags, func(current, available semver.Tag) bool { + // The new version should be greater, but still the same major & minor number + return current.Version.LessThan(available.Version) && + current.Version.Major == available.Version.Major && + current.Version.Minor == available.Version.Minor + }) +} + +func ParseMode(input string) *VersioningMode { + for _, mode := range AllModes { + if mode.Label() == input { + return &mode + } + } + return nil +} diff --git a/internal/pkg/docker/rpc.go b/internal/pkg/docker/rpc.go index 7079616..9aa90b8 100644 --- a/internal/pkg/docker/rpc.go +++ b/internal/pkg/docker/rpc.go @@ -3,17 +3,19 @@ package docker import ( "context" "fmt" + "strings" l "hulthe.net/lookbuilding/internal/pkg/logging" "hulthe.net/lookbuilding/internal/pkg/registry" - "hulthe.net/lookbuilding/internal/pkg/versioning" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" + d "github.com/opencontainers/go-digest" + "github.com/pkg/errors" ) -func GetLabeledContainers(cli *client.Client) []LabeledContainer { +func GetLabeledContainers(cli *client.Client) ([]LabeledContainer, error) { out := make([]LabeledContainer, 0) containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{}) @@ -23,19 +25,42 @@ func GetLabeledContainers(cli *client.Client) []LabeledContainer { l.Logger.Infof("scanning running container labels") for _, container := range containers { - l.Logger.Debugf("checking %s %s", container.ID[:10], container.Image) + lc := LabeledContainer{Container: container} + + if container.Image == container.ImageID { + l.Logger.Errorf("ignoring container %s which has an untagged image", lc.GetName()) + continue + } + + l.Logger.Debugf("checking %s %s", lc.GetName(), container.Image) for k, v := range container.Labels { l.Logger.Debugf(` - "%s": "%s"`, k, v) - if k == versioning.ModeLabel { - mode := versioning.ParseMode(v) + + if k == VersioningModeLabel { + mode := ParseMode(v) if mode == nil { - l.Logger.Errorf(`Failed to parse "%s" as a versioning mode`, v) + l.Logger.Errorf(`failed to parse "%s" as a versioning mode`, v) continue } + lc.Mode = *mode - lc := LabeledContainer{ - container, - *mode, + inspect, _, err := cli.ImageInspectWithRaw(context.Background(), container.ImageID) + if err != nil { + errors.Wrapf(err, "failed to inspect container %s", lc.GetName()) + } + + if len(inspect.RepoDigests) >= 2 { + // TODO: find out if having more than one digest could break same_tag version mode + l.Logger.Warnf("unexpected: container %s had more than one RepoDigest", lc.GetName()) + } else if len(inspect.RepoDigests) == 0 { + return nil, errors.Errorf("unexpected: container %s has no RepoDigests", lc.GetName()) + } + + imageDigest := inspect.RepoDigests[0] + atIndex := strings.Index(imageDigest, "@") + lc.ImageDigest, err = d.Parse(imageDigest[atIndex+1:]) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse image digest of running container %s", lc.GetName()) } out = append(out, lc) @@ -44,7 +69,7 @@ func GetLabeledContainers(cli *client.Client) []LabeledContainer { } } - return out + return out, nil } func (lc LabeledContainer) UpdateTo(cli *client.Client, tag registry.Tag) error { @@ -55,63 +80,66 @@ func (lc LabeledContainer) UpdateTo(cli *client.Client, tag registry.Tag) error canonicalImage := fmt.Sprintf("docker.io/%s", image) l.Logger.Infof(`pulling image "%s"`, canonicalImage) - //containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{}) imageReader, err := cli.ImagePull(ctx, canonicalImage, types.ImagePullOptions{}) if err != nil { - return err + return errors.Wrapf(err, `failed to pull image "%s"`, canonicalImage) } - defer imageReader.Close() - loadResponse, err := cli.ImageLoad(ctx, imageReader, false) if err != nil { - return err + return errors.Wrapf(err, `failed to load pulled image "%s"`, canonicalImage) } - defer loadResponse.Body.Close() + err = loadResponse.Body.Close() + if err != nil { + return errors.Wrapf(err, `failed to close rpc response when loading image "%s"`, canonicalImage) + } + err = imageReader.Close() + if err != nil { + return errors.Wrapf(err, `failed to close rpc response when pulling image "%s"`, canonicalImage) + } - fmt.Printf("Stopping container %s\n", lc.Container.ID) + l.Logger.Infof("stopping container %s", lc.GetName()) err = cli.ContainerStop(ctx, lc.Container.ID, nil) if err != nil { - return err + return errors.Wrapf(err, `failed to stop container "%s"`, lc.GetName()) } oldContainer, err := cli.ContainerInspect(ctx, lc.Container.ID) if err != nil { - return err + return errors.Wrapf(err, `failed to inspect container "%s"`, lc.GetName()) } - name := oldContainer.Name - tmpOldName := fmt.Sprintf("%s.lb.old", name) + oldTmpName := fmt.Sprintf("%s.lb.old", lc.GetName()) - config := oldContainer.Config + config := *oldContainer.Config config.Image = image - hostConfig := oldContainer.HostConfig - hostConfig.VolumesFrom = []string{tmpOldName} + hostConfig := *oldContainer.HostConfig + hostConfig.VolumesFrom = []string{oldTmpName} - l.Logger.Infof(`renaming container %s`, lc.Container.ID) - err = cli.ContainerRename(ctx, lc.Container.ID, tmpOldName) + l.Logger.Infof(`renaming container %s to %s`, lc.GetName(), oldTmpName) + err = cli.ContainerRename(ctx, lc.Container.ID, oldTmpName) if err != nil { - return err + return errors.Wrapf(err, `failed to rename container "%s" to "%s"`, lc.GetName(), oldTmpName) } - l.Logger.Infof("creating new container") - new, err := cli.ContainerCreate(ctx, oldContainer.Config, hostConfig, &network.NetworkingConfig{ + l.Logger.Infof("creating new container %s", lc.GetName()) + new, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{ EndpointsConfig: oldContainer.NetworkSettings.Networks, - }, name) + }, lc.GetName()) if err != nil { - return err + return errors.Wrapf(err, `failed to create container for new version of %s`, image) } - l.Logger.Infof("starting new container id: %s", new.ID) + l.Logger.Infof("starting new container %s with id %s", lc.GetName(), new.ID) err = cli.ContainerStart(ctx, new.ID, types.ContainerStartOptions{}) if err != nil { return err } - l.Logger.Infof("removing old container") + l.Logger.Infof("removing old container %s", oldTmpName) err = cli.ContainerRemove(ctx, oldContainer.ID, types.ContainerRemoveOptions{ RemoveVolumes: false, RemoveLinks: false, @@ -123,4 +151,3 @@ func (lc LabeledContainer) UpdateTo(cli *client.Client, tag registry.Tag) error return nil } - diff --git a/internal/pkg/docker/types.go b/internal/pkg/docker/types.go index f92bfdd..c166f09 100644 --- a/internal/pkg/docker/types.go +++ b/internal/pkg/docker/types.go @@ -1,12 +1,12 @@ package docker import ( - "hulthe.net/lookbuilding/internal/pkg/versioning" - "github.com/docker/docker/api/types" + d "github.com/opencontainers/go-digest" ) type LabeledContainer struct { - Container types.Container - Mode versioning.Mode + Container types.Container + Mode VersioningMode + ImageDigest d.Digest } diff --git a/internal/pkg/logging/logger.go b/internal/pkg/logging/logger.go index 74bc52a..03b506b 100644 --- a/internal/pkg/logging/logger.go +++ b/internal/pkg/logging/logger.go @@ -4,4 +4,4 @@ import "github.com/sirupsen/logrus" var ( Logger logrus.Logger = *logrus.New() -) \ No newline at end of file +) diff --git a/internal/pkg/registry/cache.go b/internal/pkg/registry/cache.go index e84794e..ab89ac3 100644 --- a/internal/pkg/registry/cache.go +++ b/internal/pkg/registry/cache.go @@ -1,31 +1,35 @@ package registry import ( + "fmt" + "net/http" + + l "hulthe.net/lookbuilding/internal/pkg/logging" "hulthe.net/lookbuilding/internal/pkg/semver" "github.com/heroku/docker-registry-client/registry" - "github.com/opencontainers/go-digest" + d "github.com/opencontainers/go-digest" "github.com/pkg/errors" ) type tagListReq struct { repository string - responder chan<- tagListResp + responder chan<- tagListResp } type tagListResp struct { - Data []Tag + Data []Tag Error error } type digestReq struct { repository string - tag string - responder chan<- digestResp + tag string + responder chan<- digestResp } type digestResp struct { - Data digest.Digest + Data d.Digest Error error } @@ -33,23 +37,68 @@ type repoCache struct { Tags []Tag // Map tags to digests - Digests map[string]digest.Digest + Digests map[string]d.Digest } type cache struct { TagListReq chan<- tagListReq - DigestReq chan<- digestReq + DigestReq chan<- digestReq } func newCache(registry registry.Registry) cache { tagListReq := make(chan tagListReq) digestReq := make(chan digestReq) + cache := cache{ + tagListReq, + digestReq, + } store := map[string]repoCache{} go func() { for { select { + case req := <-digestReq: + repo, isPresent := store[req.repository] + if !isPresent { + req.responder <- digestResp{Error: errors.Errorf( + `repo "%s" not present in cache, can't fetch digest'`, req.repository, + )} + } + + digest, isPresent := repo.Digests[req.tag] + if isPresent { + req.responder <- digestResp{Data: digest} + } else { + url := fmt.Sprintf("%s/v2/%s/manifests/%s", registry.URL, req.repository, req.tag) + l.Logger.Infof("registry.manifest.head url=%s repository=%s reference=%s", url, req.repository, req.tag) + + httpReq, _ := http.NewRequest("HEAD", url, nil) + httpReq.Header.Add("Accept", "application/vnd.docker.distribution.manifest.v2+json") + resp, err := registry.Client.Do(httpReq) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + req.responder <- digestResp{Error: errors.Wrapf( + err, "failed to get digest for repo=%s tag=%s", req.repository, req.tag, + )} + } + digest, err := d.Parse(resp.Header.Get("Docker-Content-Digest")) + + /* + digest, err := registry.ManifestDigest(req.repository, req.tag) + if err != nil { + req.responder <- digestResp{Error: errors.Wrapf( + err, `failed to get digest for repo=%s tag=%s`, req.repository, req.tag, + )} + } + */ + + repo.Digests[req.tag] = digest + req.responder <- digestResp{Data: digest} + } + case req := <-tagListReq: repo, isPresent := store[req.repository] @@ -71,50 +120,26 @@ func newCache(registry registry.Registry) cache { var tags []Tag for _, tagName := range tagNames { tags = append(tags, Tag{ - Name: tagName, - SemVer: semver.ParseTagAsSemVer(tagName), + Name: tagName, + SemVer: semver.ParseTagAsSemVer(tagName), + repository: req.repository, + cache: cache, }) } // store result in cache store[req.repository] = repoCache{ Tags: tags, - Digests: map[string]digest.Digest{}, + Digests: map[string]d.Digest{}, } req.responder <- tagListResp{ Data: tags, } } - - case req := <-digestReq: - repo, isPresent := store[req.repository] - if !isPresent { - req.responder <- digestResp{Error: errors.Errorf( - `repo "%s" not present in cache, can't fetch digest'`, req.repository, - )} - } - - digest, isPresent := repo.Digests[req.tag] - if isPresent { - req.responder <- digestResp{Data: digest} - } else { - digest, err := registry.ManifestDigest(req.repository, req.tag) - if err != nil { - req.responder <- digestResp{Error: errors.Wrapf( - err, `failed to get digest for repo=%s tag=%s`, req.repository, req.tag, - )} - } - - repo.Digests[req.tag] = digest - req.responder <- digestResp{Data: digest} - } } } }() - return cache { - tagListReq, - digestReq, - } -} \ No newline at end of file + return cache +} diff --git a/internal/pkg/registry/client.go b/internal/pkg/registry/client.go index 74b624f..81ef738 100644 --- a/internal/pkg/registry/client.go +++ b/internal/pkg/registry/client.go @@ -11,10 +11,10 @@ import ( ) type Tag struct { - Name string - SemVer *semver.Tag + Name string + SemVer *semver.Tag repository string - cache cache + cache cache } type Client struct { @@ -23,7 +23,7 @@ type Client struct { func (tag Tag) GetDigest() (digest.Digest, error) { responseCh := make(chan digestResp) - tag.cache.DigestReq <- digestReq{ tag.repository, tag.Name, responseCh } + tag.cache.DigestReq <- digestReq{tag.repository, tag.Name, responseCh} resp := <-responseCh return resp.Data, resp.Error } @@ -34,9 +34,8 @@ func (client Client) GetRepoTags(maybeOwner *string, repository string) ([]Tag, } responseCh := make(chan tagListResp) - client.cache.TagListReq <- tagListReq { repository, responseCh } + client.cache.TagListReq <- tagListReq{repository, responseCh} resp := <-responseCh - return resp.Data, resp.Error } diff --git a/internal/pkg/versioning/mode.go b/internal/pkg/versioning/mode.go deleted file mode 100644 index 6a837aa..0000000 --- a/internal/pkg/versioning/mode.go +++ /dev/null @@ -1,99 +0,0 @@ -package versioning - -import ( - "sort" - - l "hulthe.net/lookbuilding/internal/pkg/logging" - "hulthe.net/lookbuilding/internal/pkg/registry" - "hulthe.net/lookbuilding/internal/pkg/semver" -) - -const ModeLabel = "lookbuilding.mode" - -type Mode interface { - Label() string - ShouldUpdate(currentTag string, availableTags []registry.Tag) *registry.Tag -} - -type SameTag struct{} -type SemVerMajor struct{} -type SemVerMinor struct{} -type SemVerPatch struct{} - -var ( - AllModes = [...]Mode{ - SameTag{}, - SemVerMajor{}, - SemVerMinor{}, - SemVerPatch{}, - } -) - -func (SameTag) Label() string { return "same_tag" } -func (SameTag) ShouldUpdate(currentTag string, availableTags []registry.Tag) *registry.Tag { - l.Logger.Errorf("Not implemented: 'same_tag' versioning mode") - return nil // TODO: implement me -} - -func semVerShouldUpdate(currentTag string, availableTags []registry.Tag, isValid func(current, available semver.Tag) bool) *registry.Tag { - currentSemVer := semver.ParseTagAsSemVer(currentTag) - if currentSemVer == nil { - return nil - } - - semverTags := make([]registry.Tag, 0) - - for _, tag := range availableTags { - if tag.SemVer != nil && isValid(*currentSemVer, *tag.SemVer) { - semverTags = append(semverTags, tag) - } - } - - if len(semverTags) == 0 { - return nil - } - - sort.Slice(semverTags, func(i, j int) bool { - a := semverTags[i].SemVer.Version - b := semverTags[j].SemVer.Version - return b.LessThan(a) - }) - - return &semverTags[0] -} - -func (SemVerMajor) Label() string { return "semver_major" } -func (SemVerMajor) ShouldUpdate(currentTag string, availableTags []registry.Tag) *registry.Tag { - return semVerShouldUpdate(currentTag, availableTags, func(current, available semver.Tag) bool { - // The new version should be greater - return current.Version.LessThan(available.Version) - }) -} - -func (SemVerMinor) Label() string { return "semver_minor" } -func (SemVerMinor) ShouldUpdate(currentTag string, availableTags []registry.Tag) *registry.Tag { - return semVerShouldUpdate(currentTag, availableTags, func(current, available semver.Tag) bool { - // The new version should be greater, but still the same major number - return current.Version.LessThan(available.Version) && - current.Version.Major == available.Version.Major - }) -} - -func (SemVerPatch) Label() string { return "semver_patch" } -func (SemVerPatch) ShouldUpdate(currentTag string, availableTags []registry.Tag) *registry.Tag { - return semVerShouldUpdate(currentTag, availableTags, func(current, available semver.Tag) bool { - // The new version should be greater, but still the same major & minor number - return current.Version.LessThan(available.Version) && - current.Version.Major == available.Version.Major && - current.Version.Minor == available.Version.Minor - }) -} - -func ParseMode(input string) *Mode { - for _, mode := range AllModes { - if mode.Label() == input { - return &mode - } - } - return nil -} diff --git a/internal/pkg/worker/worker.go b/internal/pkg/worker/worker.go index f5e3859..9108325 100644 --- a/internal/pkg/worker/worker.go +++ b/internal/pkg/worker/worker.go @@ -64,7 +64,10 @@ func checkAndDoUpdate() { panic(err) } - labeledContainers := docker.GetLabeledContainers(cli) + labeledContainers, err := docker.GetLabeledContainers(cli) + if err != nil { + panic(err) + } l.Logger.Infof("found %d valid containers", len(labeledContainers)) @@ -85,25 +88,24 @@ func checkAndDoUpdate() { panic(err) } - l.Logger.Infof(`tags in registry for "%s": %d`, name, len(repoTags)) + l.Logger.Debugf(`tags in registry for "%s": %d`, name, len(repoTags)) for _, tag := range repoTags { svt := semver.ParseTagAsSemVer(tag.Name) - l.Logger.Infof(`tag_name="%s" semver=%t`, tag.Name, svt != nil) + l.Logger.Debugf(`tag_name="%s" semver=%t`, tag.Name, svt != nil) } - shouldUpdateTo := lc.Mode.ShouldUpdate(*tag, repoTags) + shouldUpdateTo := lc.Mode.ShouldUpdate(lc, repoTags) if shouldUpdateTo != nil { - l.Logger.Infof(`updating %s from %s to: %s`, name, *tag, shouldUpdateTo.Name) + l.Logger.Infof(`updating %s from %s to %s`, name, *tag, shouldUpdateTo.Name) - go func() { - err = lc.UpdateTo(cli, *shouldUpdateTo) - if err != nil { - l.Logger.Error(err) - } - }() + err = lc.UpdateTo(cli, *shouldUpdateTo) + if err != nil { + l.Logger.Error(err) + } } else { l.Logger.Infof("no update available for container %s", name) } } + l.Logger.Infof("all done") }