Refactor into package structure
This commit is contained in:
56
internal/pkg/docker/helpers.go
Normal file
56
internal/pkg/docker/helpers.go
Normal file
@ -0,0 +1,56 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Extract the repository owner (if any), repository and tag (if any) from a docker image name
|
||||
func (lc LabeledContainer) SplitImageParts() (*string, string, *string) {
|
||||
name := lc.Container.Image
|
||||
|
||||
var repository string
|
||||
var owner *string
|
||||
var tag *string
|
||||
|
||||
slashIndex := strings.Index(name, "/")
|
||||
if slashIndex >= 0 {
|
||||
tmp := name[:slashIndex]
|
||||
owner = &tmp
|
||||
name = name[slashIndex+1:]
|
||||
}
|
||||
|
||||
colonIndex := strings.Index(name, ":")
|
||||
if colonIndex >= 0 {
|
||||
tmp := name[colonIndex+1:]
|
||||
tag = &tmp
|
||||
|
||||
repository = name[:colonIndex]
|
||||
} else {
|
||||
repository = name
|
||||
}
|
||||
|
||||
return owner, repository, tag
|
||||
}
|
||||
|
||||
func (lc LabeledContainer) GetName() string {
|
||||
if len(lc.Container.Names) >= 0 {
|
||||
// trim prefixed "/"
|
||||
return lc.Container.Names[0][1:]
|
||||
} else {
|
||||
return lc.Container.ID[:10]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func CombineImageParts(owner *string, repository string, tag *string) string {
|
||||
image := repository
|
||||
if owner != nil {
|
||||
image = fmt.Sprintf("%s/%s", *owner, image)
|
||||
}
|
||||
if tag != nil {
|
||||
image = fmt.Sprintf("%s:%s", image, *tag)
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
126
internal/pkg/docker/rpc.go
Normal file
126
internal/pkg/docker/rpc.go
Normal file
@ -0,0 +1,126 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func GetLabeledContainers(cli *client.Client) []LabeledContainer {
|
||||
out := make([]LabeledContainer, 0)
|
||||
|
||||
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
l.Logger.Infof("scanning running container labels")
|
||||
for _, container := range containers {
|
||||
l.Logger.Debugf("checking %s %s", container.ID[:10], container.Image)
|
||||
for k, v := range container.Labels {
|
||||
l.Logger.Debugf(` - "%s": "%s"`, k, v)
|
||||
if k == versioning.ModeLabel {
|
||||
mode := versioning.ParseMode(v)
|
||||
if mode == nil {
|
||||
l.Logger.Errorf(`Failed to parse "%s" as a versioning mode`, v)
|
||||
continue
|
||||
}
|
||||
|
||||
lc := LabeledContainer{
|
||||
container,
|
||||
*mode,
|
||||
}
|
||||
|
||||
out = append(out, lc)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (lc LabeledContainer) UpdateTo(cli *client.Client, tag registry.Tag) error {
|
||||
ctx := context.Background()
|
||||
|
||||
owner, repository, _ := lc.SplitImageParts()
|
||||
image := CombineImageParts(owner, repository, &tag.Name)
|
||||
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
|
||||
}
|
||||
|
||||
defer imageReader.Close()
|
||||
|
||||
loadResponse, err := cli.ImageLoad(ctx, imageReader, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer loadResponse.Body.Close()
|
||||
|
||||
fmt.Printf("Stopping container %s\n", lc.Container.ID)
|
||||
err = cli.ContainerStop(ctx, lc.Container.ID, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldContainer, err := cli.ContainerInspect(ctx, lc.Container.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := oldContainer.Name
|
||||
tmpOldName := fmt.Sprintf("%s.lb.old", name)
|
||||
|
||||
config := oldContainer.Config
|
||||
config.Image = image
|
||||
|
||||
hostConfig := oldContainer.HostConfig
|
||||
hostConfig.VolumesFrom = []string{tmpOldName}
|
||||
|
||||
l.Logger.Infof(`renaming container %s`, lc.Container.ID)
|
||||
err = cli.ContainerRename(ctx, lc.Container.ID, tmpOldName)
|
||||
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,
|
||||
}, name)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Logger.Infof("starting new container id: %s", new.ID)
|
||||
err = cli.ContainerStart(ctx, new.ID, types.ContainerStartOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Logger.Infof("removing old container")
|
||||
err = cli.ContainerRemove(ctx, oldContainer.ID, types.ContainerRemoveOptions{
|
||||
RemoveVolumes: false,
|
||||
RemoveLinks: false,
|
||||
Force: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
12
internal/pkg/docker/types.go
Normal file
12
internal/pkg/docker/types.go
Normal file
@ -0,0 +1,12 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"hulthe.net/lookbuilding/internal/pkg/versioning"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
type LabeledContainer struct {
|
||||
Container types.Container
|
||||
Mode versioning.Mode
|
||||
}
|
||||
7
internal/pkg/logging/logger.go
Normal file
7
internal/pkg/logging/logger.go
Normal file
@ -0,0 +1,7 @@
|
||||
package logging
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
var (
|
||||
Logger logrus.Logger = *logrus.New()
|
||||
)
|
||||
120
internal/pkg/registry/cache.go
Normal file
120
internal/pkg/registry/cache.go
Normal file
@ -0,0 +1,120 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"hulthe.net/lookbuilding/internal/pkg/semver"
|
||||
|
||||
"github.com/heroku/docker-registry-client/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type tagListReq struct {
|
||||
repository string
|
||||
responder chan<- tagListResp
|
||||
}
|
||||
|
||||
type tagListResp struct {
|
||||
Data []Tag
|
||||
Error error
|
||||
}
|
||||
|
||||
type digestReq struct {
|
||||
repository string
|
||||
tag string
|
||||
responder chan<- digestResp
|
||||
}
|
||||
|
||||
type digestResp struct {
|
||||
Data digest.Digest
|
||||
Error error
|
||||
}
|
||||
|
||||
type repoCache struct {
|
||||
Tags []Tag
|
||||
|
||||
// Map tags to digests
|
||||
Digests map[string]digest.Digest
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
TagListReq chan<- tagListReq
|
||||
DigestReq chan<- digestReq
|
||||
}
|
||||
|
||||
func newCache(registry registry.Registry) cache {
|
||||
tagListReq := make(chan tagListReq)
|
||||
digestReq := make(chan digestReq)
|
||||
|
||||
store := map[string]repoCache{}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case req := <-tagListReq:
|
||||
repo, isPresent := store[req.repository]
|
||||
|
||||
if isPresent {
|
||||
// Tag list was already in cache, just return it
|
||||
req.responder <- tagListResp{Data: repo.Tags}
|
||||
|
||||
} else {
|
||||
// tag list was not in cache, we have to fetch it
|
||||
tagNames, err := registry.Tags(req.repository)
|
||||
|
||||
if err != nil {
|
||||
req.responder <- tagListResp{
|
||||
Error: errors.Wrapf(err, `failed to list tags for registry repo "%s"`, req.repository),
|
||||
}
|
||||
}
|
||||
|
||||
// convert names to Tag{}
|
||||
var tags []Tag
|
||||
for _, tagName := range tagNames {
|
||||
tags = append(tags, Tag{
|
||||
Name: tagName,
|
||||
SemVer: semver.ParseTagAsSemVer(tagName),
|
||||
})
|
||||
}
|
||||
|
||||
// store result in cache
|
||||
store[req.repository] = repoCache{
|
||||
Tags: tags,
|
||||
Digests: map[string]digest.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,
|
||||
}
|
||||
}
|
||||
60
internal/pkg/registry/client.go
Normal file
60
internal/pkg/registry/client.go
Normal file
@ -0,0 +1,60 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
SemVer *semver.Tag
|
||||
repository string
|
||||
cache cache
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
cache cache
|
||||
}
|
||||
|
||||
func (tag Tag) GetDigest() (digest.Digest, error) {
|
||||
responseCh := make(chan digestResp)
|
||||
tag.cache.DigestReq <- digestReq{ tag.repository, tag.Name, responseCh }
|
||||
resp := <-responseCh
|
||||
return resp.Data, resp.Error
|
||||
}
|
||||
|
||||
func (client Client) GetRepoTags(maybeOwner *string, repository string) ([]Tag, error) {
|
||||
if maybeOwner != nil {
|
||||
repository = fmt.Sprintf("%s/%s", *maybeOwner, repository)
|
||||
}
|
||||
|
||||
responseCh := make(chan tagListResp)
|
||||
client.cache.TagListReq <- tagListReq { repository, responseCh }
|
||||
resp := <-responseCh
|
||||
|
||||
return resp.Data, resp.Error
|
||||
}
|
||||
|
||||
func AnonymousClient() (*Client, error) {
|
||||
url := "https://registry-1.docker.io/"
|
||||
username := "" // anonymous
|
||||
password := "" // anonymous
|
||||
|
||||
registry, err := registry.New(url, username, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
registry.Logf = l.Logger.Infof
|
||||
|
||||
client := Client{
|
||||
cache: newCache(*registry),
|
||||
}
|
||||
|
||||
return &client, nil
|
||||
}
|
||||
100
internal/pkg/versioning/mode.go
Normal file
100
internal/pkg/versioning/mode.go
Normal file
@ -0,0 +1,100 @@
|
||||
package versioning
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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
|
||||
}
|
||||
109
internal/pkg/worker/worker.go
Normal file
109
internal/pkg/worker/worker.go
Normal file
@ -0,0 +1,109 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"hulthe.net/lookbuilding/internal/pkg/docker"
|
||||
l "hulthe.net/lookbuilding/internal/pkg/logging"
|
||||
"hulthe.net/lookbuilding/internal/pkg/registry"
|
||||
"hulthe.net/lookbuilding/internal/pkg/semver"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
var (
|
||||
triggerCh = make(chan struct{})
|
||||
)
|
||||
|
||||
func TriggerScan() {
|
||||
triggerCh <- struct{}{}
|
||||
}
|
||||
|
||||
func Worker() {
|
||||
l.Logger.Debugf("background worker starting")
|
||||
|
||||
responseCh := make(chan struct{})
|
||||
|
||||
workerRunning := false
|
||||
triggerWaiting := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case _ = <-triggerCh:
|
||||
if workerRunning {
|
||||
triggerWaiting = true
|
||||
} else {
|
||||
workerRunning = true
|
||||
go func() {
|
||||
checkAndDoUpdate()
|
||||
responseCh <- struct{}{}
|
||||
}()
|
||||
}
|
||||
case _ = <-responseCh:
|
||||
if triggerWaiting {
|
||||
triggerWaiting = false
|
||||
go func() {
|
||||
checkAndDoUpdate()
|
||||
responseCh <- struct{}{}
|
||||
}()
|
||||
} else {
|
||||
workerRunning = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkAndDoUpdate() {
|
||||
l.Logger.Infof("starting scan")
|
||||
|
||||
cli, err := client.NewEnvClient()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
hub, err := registry.AnonymousClient()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
labeledContainers := docker.GetLabeledContainers(cli)
|
||||
|
||||
l.Logger.Infof("found %d valid containers", len(labeledContainers))
|
||||
|
||||
for _, lc := range labeledContainers {
|
||||
owner, repository, tag := lc.SplitImageParts()
|
||||
name := lc.GetName()
|
||||
imageName := docker.CombineImageParts(owner, repository, nil)
|
||||
|
||||
if tag == nil {
|
||||
l.Logger.Errorf(`no tag specified for container "%s", ignoring`, name)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Logger.Infof(`container "%s" image="%s" mode=%s tag="%s"`, name, imageName, lc.Mode.Label(), *tag)
|
||||
|
||||
repoTags, err := hub.GetRepoTags(owner, repository)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
l.Logger.Infof(`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)
|
||||
}
|
||||
|
||||
shouldUpdateTo := lc.Mode.ShouldUpdate(*tag, repoTags)
|
||||
if shouldUpdateTo != nil {
|
||||
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)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
l.Logger.Infof("no update available for container %s", name)
|
||||
}
|
||||
}
|
||||
l.Logger.Infof("all done")
|
||||
}
|
||||
Reference in New Issue
Block a user