Implement same_tag versioning mode

This commit is contained in:
2020-11-11 15:12:59 +01:00
parent c01d192e08
commit 5984349df3
12 changed files with 278 additions and 196 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

View File

@ -11,7 +11,7 @@ RUN go mod download
# Copy source and build app # Copy source and build app
COPY . /app COPY . /app
RUN go build . RUN go build hulthe.net/lookbuilding/cmd/lookbuilding
FROM alpine FROM alpine

View File

@ -12,6 +12,8 @@ import (
const EnvAddr = "LOOKBUILDING_ADDR" const EnvAddr = "LOOKBUILDING_ADDR"
func main() { func main() {
//l.Logger.Level = logrus.DebugLevel
addr, isPresent := os.LookupEnv(EnvAddr) addr, isPresent := os.LookupEnv(EnvAddr)
if !isPresent { if !isPresent {
addr = "0.0.0.0:8000" addr = "0.0.0.0:8000"

View File

@ -42,7 +42,6 @@ func (lc LabeledContainer) GetName() string {
} }
} }
func CombineImageParts(owner *string, repository string, tag *string) string { func CombineImageParts(owner *string, repository string, tag *string) string {
image := repository image := repository
if owner != nil { if owner != nil {

126
internal/pkg/docker/mode.go Normal file
View File

@ -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
}

View File

@ -3,17 +3,19 @@ package docker
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
l "hulthe.net/lookbuilding/internal/pkg/logging" l "hulthe.net/lookbuilding/internal/pkg/logging"
"hulthe.net/lookbuilding/internal/pkg/registry" "hulthe.net/lookbuilding/internal/pkg/registry"
"hulthe.net/lookbuilding/internal/pkg/versioning"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/network"
"github.com/docker/docker/client" "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) out := make([]LabeledContainer, 0)
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{}) 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") l.Logger.Infof("scanning running container labels")
for _, container := range containers { 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 { for k, v := range container.Labels {
l.Logger.Debugf(` - "%s": "%s"`, k, v) l.Logger.Debugf(` - "%s": "%s"`, k, v)
if k == versioning.ModeLabel {
mode := versioning.ParseMode(v) if k == VersioningModeLabel {
mode := ParseMode(v)
if mode == nil { 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 continue
} }
lc.Mode = *mode
lc := LabeledContainer{ inspect, _, err := cli.ImageInspectWithRaw(context.Background(), container.ImageID)
container, if err != nil {
*mode, 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) 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 { 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) canonicalImage := fmt.Sprintf("docker.io/%s", image)
l.Logger.Infof(`pulling image "%s"`, canonicalImage) l.Logger.Infof(`pulling image "%s"`, canonicalImage)
//containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
imageReader, err := cli.ImagePull(ctx, canonicalImage, types.ImagePullOptions{}) imageReader, err := cli.ImagePull(ctx, canonicalImage, types.ImagePullOptions{})
if err != nil { 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) loadResponse, err := cli.ImageLoad(ctx, imageReader, false)
if err != nil { 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) err = cli.ContainerStop(ctx, lc.Container.ID, nil)
if err != 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) oldContainer, err := cli.ContainerInspect(ctx, lc.Container.ID)
if err != nil { if err != nil {
return err return errors.Wrapf(err, `failed to inspect container "%s"`, lc.GetName())
} }
name := oldContainer.Name oldTmpName := fmt.Sprintf("%s.lb.old", lc.GetName())
tmpOldName := fmt.Sprintf("%s.lb.old", name)
config := oldContainer.Config config := *oldContainer.Config
config.Image = image config.Image = image
hostConfig := oldContainer.HostConfig hostConfig := *oldContainer.HostConfig
hostConfig.VolumesFrom = []string{tmpOldName} hostConfig.VolumesFrom = []string{oldTmpName}
l.Logger.Infof(`renaming container %s`, lc.Container.ID) l.Logger.Infof(`renaming container %s to %s`, lc.GetName(), oldTmpName)
err = cli.ContainerRename(ctx, lc.Container.ID, tmpOldName) err = cli.ContainerRename(ctx, lc.Container.ID, oldTmpName)
if err != nil { 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") l.Logger.Infof("creating new container %s", lc.GetName())
new, err := cli.ContainerCreate(ctx, oldContainer.Config, hostConfig, &network.NetworkingConfig{ new, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{
EndpointsConfig: oldContainer.NetworkSettings.Networks, EndpointsConfig: oldContainer.NetworkSettings.Networks,
}, name) }, lc.GetName())
if err != nil { 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{}) err = cli.ContainerStart(ctx, new.ID, types.ContainerStartOptions{})
if err != nil { if err != nil {
return err return err
} }
l.Logger.Infof("removing old container") l.Logger.Infof("removing old container %s", oldTmpName)
err = cli.ContainerRemove(ctx, oldContainer.ID, types.ContainerRemoveOptions{ err = cli.ContainerRemove(ctx, oldContainer.ID, types.ContainerRemoveOptions{
RemoveVolumes: false, RemoveVolumes: false,
RemoveLinks: false, RemoveLinks: false,
@ -123,4 +151,3 @@ func (lc LabeledContainer) UpdateTo(cli *client.Client, tag registry.Tag) error
return nil return nil
} }

View File

@ -1,12 +1,12 @@
package docker package docker
import ( import (
"hulthe.net/lookbuilding/internal/pkg/versioning"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
d "github.com/opencontainers/go-digest"
) )
type LabeledContainer struct { type LabeledContainer struct {
Container types.Container Container types.Container
Mode versioning.Mode Mode VersioningMode
ImageDigest d.Digest
} }

View File

@ -1,31 +1,35 @@
package registry package registry
import ( import (
"fmt"
"net/http"
l "hulthe.net/lookbuilding/internal/pkg/logging"
"hulthe.net/lookbuilding/internal/pkg/semver" "hulthe.net/lookbuilding/internal/pkg/semver"
"github.com/heroku/docker-registry-client/registry" "github.com/heroku/docker-registry-client/registry"
"github.com/opencontainers/go-digest" d "github.com/opencontainers/go-digest"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type tagListReq struct { type tagListReq struct {
repository string repository string
responder chan<- tagListResp responder chan<- tagListResp
} }
type tagListResp struct { type tagListResp struct {
Data []Tag Data []Tag
Error error Error error
} }
type digestReq struct { type digestReq struct {
repository string repository string
tag string tag string
responder chan<- digestResp responder chan<- digestResp
} }
type digestResp struct { type digestResp struct {
Data digest.Digest Data d.Digest
Error error Error error
} }
@ -33,23 +37,68 @@ type repoCache struct {
Tags []Tag Tags []Tag
// Map tags to digests // Map tags to digests
Digests map[string]digest.Digest Digests map[string]d.Digest
} }
type cache struct { type cache struct {
TagListReq chan<- tagListReq TagListReq chan<- tagListReq
DigestReq chan<- digestReq DigestReq chan<- digestReq
} }
func newCache(registry registry.Registry) cache { func newCache(registry registry.Registry) cache {
tagListReq := make(chan tagListReq) tagListReq := make(chan tagListReq)
digestReq := make(chan digestReq) digestReq := make(chan digestReq)
cache := cache{
tagListReq,
digestReq,
}
store := map[string]repoCache{} store := map[string]repoCache{}
go func() { go func() {
for { for {
select { 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: case req := <-tagListReq:
repo, isPresent := store[req.repository] repo, isPresent := store[req.repository]
@ -71,50 +120,26 @@ func newCache(registry registry.Registry) cache {
var tags []Tag var tags []Tag
for _, tagName := range tagNames { for _, tagName := range tagNames {
tags = append(tags, Tag{ tags = append(tags, Tag{
Name: tagName, Name: tagName,
SemVer: semver.ParseTagAsSemVer(tagName), SemVer: semver.ParseTagAsSemVer(tagName),
repository: req.repository,
cache: cache,
}) })
} }
// store result in cache // store result in cache
store[req.repository] = repoCache{ store[req.repository] = repoCache{
Tags: tags, Tags: tags,
Digests: map[string]digest.Digest{}, Digests: map[string]d.Digest{},
} }
req.responder <- tagListResp{ req.responder <- tagListResp{
Data: tags, 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 { return cache
tagListReq,
digestReq,
}
} }

View File

@ -11,10 +11,10 @@ import (
) )
type Tag struct { type Tag struct {
Name string Name string
SemVer *semver.Tag SemVer *semver.Tag
repository string repository string
cache cache cache cache
} }
type Client struct { type Client struct {
@ -23,7 +23,7 @@ type Client struct {
func (tag Tag) GetDigest() (digest.Digest, error) { func (tag Tag) GetDigest() (digest.Digest, error) {
responseCh := make(chan digestResp) responseCh := make(chan digestResp)
tag.cache.DigestReq <- digestReq{ tag.repository, tag.Name, responseCh } tag.cache.DigestReq <- digestReq{tag.repository, tag.Name, responseCh}
resp := <-responseCh resp := <-responseCh
return resp.Data, resp.Error return resp.Data, resp.Error
} }
@ -34,9 +34,8 @@ func (client Client) GetRepoTags(maybeOwner *string, repository string) ([]Tag,
} }
responseCh := make(chan tagListResp) responseCh := make(chan tagListResp)
client.cache.TagListReq <- tagListReq { repository, responseCh } client.cache.TagListReq <- tagListReq{repository, responseCh}
resp := <-responseCh resp := <-responseCh
return resp.Data, resp.Error return resp.Data, resp.Error
} }

View File

@ -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
}

View File

@ -64,7 +64,10 @@ func checkAndDoUpdate() {
panic(err) 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)) l.Logger.Infof("found %d valid containers", len(labeledContainers))
@ -85,25 +88,24 @@ func checkAndDoUpdate() {
panic(err) 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 { for _, tag := range repoTags {
svt := semver.ParseTagAsSemVer(tag.Name) 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 { 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)
err = lc.UpdateTo(cli, *shouldUpdateTo) if err != nil {
if err != nil { l.Logger.Error(err)
l.Logger.Error(err) }
}
}()
} else { } else {
l.Logger.Infof("no update available for container %s", name) l.Logger.Infof("no update available for container %s", name)
} }
} }
l.Logger.Infof("all done") l.Logger.Infof("all done")
} }