Compare commits
5 Commits
c01d192e08
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
01f61b3f88
|
|||
| 86816daab1 | |||
| ee4c68cfba | |||
|
e016c6714d
|
|||
|
5984349df3
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.idea
|
||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
# lookbuilding
|
# lookbuilding
|
||||||
_WIP_
|
|
||||||
|
|
||||||
Scuffed lookalike of watchtower
|
Scuffed lookalike of watchtower
|
||||||
|
|
||||||
This project will, when triggered by an http request, scan the
|
This project will, when triggered by an http request, scan the
|
||||||
|
|||||||
@ -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"
|
||||||
@ -22,8 +24,10 @@ func main() {
|
|||||||
fmt.Fprintf(w, "OK")
|
fmt.Fprintf(w, "OK")
|
||||||
})
|
})
|
||||||
|
|
||||||
fs := http.FileServer(http.Dir("static/"))
|
http.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Handle("/static/", http.StripPrefix("/static/", fs))
|
// TODO: if the last scan errored, this should not return OK
|
||||||
|
fmt.Fprintf(w, "OK")
|
||||||
|
})
|
||||||
|
|
||||||
l.Logger.Infof(`listening on %s`, addr)
|
l.Logger.Infof(`listening on %s`, addr)
|
||||||
|
|
||||||
|
|||||||
@ -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
126
internal/pkg/docker/mode.go
Normal 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
|
||||||
|
}
|
||||||
@ -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,
|
return 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)
|
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,60 @@ 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
|
tmpName := fmt.Sprintf("%s.lb.new", 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{lc.GetName()}
|
||||||
|
|
||||||
l.Logger.Infof(`renaming container %s`, lc.Container.ID)
|
l.Logger.Infof("creating new container %s", tmpName)
|
||||||
err = cli.ContainerRename(ctx, lc.Container.ID, tmpOldName)
|
new, err := cli.ContainerCreate(ctx, &config, &hostConfig, &network.NetworkingConfig{
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Logger.Infof("creating new container")
|
|
||||||
new, err := cli.ContainerCreate(ctx, oldContainer.Config, hostConfig, &network.NetworkingConfig{
|
|
||||||
EndpointsConfig: oldContainer.NetworkSettings.Networks,
|
EndpointsConfig: oldContainer.NetworkSettings.Networks,
|
||||||
}, name)
|
}, tmpName)
|
||||||
|
|
||||||
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", tmpName, 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", lc.GetName())
|
||||||
err = cli.ContainerRemove(ctx, oldContainer.ID, types.ContainerRemoveOptions{
|
err = cli.ContainerRemove(ctx, oldContainer.ID, types.ContainerRemoveOptions{
|
||||||
RemoveVolumes: false,
|
RemoveVolumes: false,
|
||||||
RemoveLinks: false,
|
RemoveLinks: false,
|
||||||
@ -121,6 +143,11 @@ func (lc LabeledContainer) UpdateTo(cli *client.Client, tag registry.Tag) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l.Logger.Infof(`renaming container %s to %s`, tmpName, lc.GetName())
|
||||||
|
err = cli.ContainerRename(ctx, new.ID, lc.GetName())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, `failed to rename container "%s" to "%s"`, tmpName, lc.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user