first version
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
/config.json
|
||||
54
build.sh
Executable file
54
build.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
TARGET_CURRENT=true
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTIONS]"
|
||||
echo "Options:"
|
||||
echo " --all-target Build for all target available"
|
||||
}
|
||||
|
||||
# Function to handle options and arguments
|
||||
handle_options() {
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--all-target)
|
||||
TARGET_CURRENT=false
|
||||
;;
|
||||
*)
|
||||
echo "Invalid option: $1" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
# Main script execution
|
||||
handle_options "$@"
|
||||
|
||||
if [ ! -d "./build" ]; then
|
||||
mkdir ./build
|
||||
fi
|
||||
|
||||
if [ "$TARGET_CURRENT" == "true" ]; then
|
||||
GOOS=$(go env GOOS)
|
||||
GOARCH=$(go env GOARCH)
|
||||
|
||||
echo "* Compiling for $GOOS/$GOARCH..."
|
||||
CGO_ENABLED=0 GOOS=$GOOS GOARCH=$GOARCH GORISCV64=rva22u64 GOAMD64=v3 GOARM64=v8.2 go build -o build/dockerupdater -a
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
platforms=("linux/amd64" "linux/arm64" "linux/riscv64" "linux/ppc64le" "windows/amd64")
|
||||
|
||||
for platform in "${platforms[@]}"; do
|
||||
echo "* Compiling for $platform..."
|
||||
platform_split=(${platform//\// })
|
||||
|
||||
CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} GORISCV64=rva22u64 GOAMD64=v3 GOARM64=v8.2 go build -o build/dockerupdater${platform_split[0]}_${platform_split[1]} -a
|
||||
done
|
||||
125
config/config.go
Normal file
125
config/config.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type (
|
||||
Configuration struct {
|
||||
GlobalContainerConfiguration ContainerConfiguration `json:"containers"`
|
||||
DaemonConfiguration DaemonConfiguration `json:"daemon"`
|
||||
IgnoreRunningUnspecifiedContainers bool `json:"ignore_running_unspecified_containers"`
|
||||
StrictValidation bool `json:"strict_validation"`
|
||||
}
|
||||
|
||||
DaemonConfiguration struct {
|
||||
PullInterval uint `json:"pull_interval"`
|
||||
}
|
||||
|
||||
ContainerConfiguration struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Schedule string `json:"schedule"`
|
||||
}
|
||||
)
|
||||
|
||||
func Load(configPath string) (Configuration, error) {
|
||||
file, err := os.OpenFile(configPath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
slog.Warn("no configuration found, loading the default one", "thread", "main", "path", configPath)
|
||||
return Default(), nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var c map[string]any
|
||||
d := json.NewDecoder(file)
|
||||
if err := d.Decode(&c); err != nil {
|
||||
return Configuration{}, fmt.Errorf("unable to parse the configuration file: %s", err)
|
||||
}
|
||||
|
||||
return parseConfiguration(c)
|
||||
}
|
||||
|
||||
func Default() Configuration {
|
||||
return Configuration{
|
||||
GlobalContainerConfiguration: ContainerConfiguration{
|
||||
Enabled: false,
|
||||
Schedule: "* * * * *",
|
||||
},
|
||||
DaemonConfiguration: DaemonConfiguration{
|
||||
PullInterval: 2,
|
||||
},
|
||||
IgnoreRunningUnspecifiedContainers: true,
|
||||
StrictValidation: false,
|
||||
}
|
||||
}
|
||||
|
||||
func parseConfiguration(currentConfig map[string]any) (Configuration, error) {
|
||||
c := Default()
|
||||
|
||||
if defaultContainerConfiguration, ok := currentConfig["containers"]; ok {
|
||||
if defaultContainerData, ok := defaultContainerConfiguration.(map[string]any); ok {
|
||||
if schedule, ok := defaultContainerData["schedule"]; ok {
|
||||
if data, ok := schedule.(string); ok {
|
||||
c.GlobalContainerConfiguration.Schedule = data
|
||||
} else {
|
||||
return Configuration{}, fmt.Errorf("configuration parsing: invalid containers.schedule value, expected a string")
|
||||
}
|
||||
}
|
||||
if enabled, ok := defaultContainerData["enabled"]; ok {
|
||||
if data, ok := enabled.(bool); ok {
|
||||
c.GlobalContainerConfiguration.Enabled = data
|
||||
} else {
|
||||
return Configuration{}, fmt.Errorf("configuration parsing: invalid containers.enabled value, expected a bool")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Configuration{}, fmt.Errorf("configuration parsing: invalid containers section, expected an object")
|
||||
}
|
||||
}
|
||||
|
||||
if daemonConfiguration, ok := currentConfig["daemon"]; ok {
|
||||
if daemonData, ok := daemonConfiguration.(map[string]any); ok {
|
||||
if pullInterval, ok := daemonData["pull_interval"]; ok {
|
||||
if data, ok := pullInterval.(uint); ok {
|
||||
c.DaemonConfiguration.PullInterval = data
|
||||
}
|
||||
} else {
|
||||
return Configuration{}, fmt.Errorf("configuration parsing: invalid daemon.pull_interval value, expected an unsigned integer")
|
||||
}
|
||||
} else {
|
||||
return Configuration{}, fmt.Errorf("configuration parsing: invalid daemon section, expected an object")
|
||||
}
|
||||
}
|
||||
|
||||
if ignoreRunningUnspecifiedContainers, ok := currentConfig["ignore_running_unspecified_containers"]; ok {
|
||||
if data, ok := ignoreRunningUnspecifiedContainers.(bool); ok {
|
||||
c.IgnoreRunningUnspecifiedContainers = data
|
||||
} else {
|
||||
return Configuration{}, fmt.Errorf("configuration parsing: invalid ignore_running_unspecified_containers value, expected a bool")
|
||||
}
|
||||
}
|
||||
|
||||
if strictValidation, ok := currentConfig["strict_validation"]; ok {
|
||||
if data, ok := strictValidation.(bool); ok {
|
||||
c.StrictValidation = data
|
||||
} else {
|
||||
return Configuration{}, fmt.Errorf("configuration parsing: invalid strict_validation value, expected a bool")
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c Configuration) Validate() error {
|
||||
_, err := cron.ParseStandard(c.GlobalContainerConfiguration.Schedule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate: invalid schedule: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
65
constant/constant.go
Normal file
65
constant/constant.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package constant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
Version struct {
|
||||
patch int
|
||||
minor int
|
||||
major int
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
patch string = "0"
|
||||
minor string = "0"
|
||||
major string = "0"
|
||||
|
||||
version Version
|
||||
)
|
||||
|
||||
func init() {
|
||||
patchInt, err := strconv.Atoi(patch)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to parse version number, this is a compilation error, try recompile the program"))
|
||||
}
|
||||
|
||||
minorInt, err := strconv.Atoi(minor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to parse version number, this is a compilation error, try recompile the program"))
|
||||
}
|
||||
|
||||
majorInt, err := strconv.Atoi(major)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to parse version number, this is a compilation error, try recompile the program"))
|
||||
}
|
||||
|
||||
version = Version{
|
||||
patch: patchInt,
|
||||
minor: minorInt,
|
||||
major: majorInt,
|
||||
}
|
||||
}
|
||||
|
||||
func ProgramVersion() Version {
|
||||
return version
|
||||
}
|
||||
|
||||
func (v Version) Patch() int {
|
||||
return v.patch
|
||||
}
|
||||
|
||||
func (v Version) Minor() int {
|
||||
return v.minor
|
||||
}
|
||||
|
||||
func (v Version) Major() int {
|
||||
return v.major
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch)
|
||||
}
|
||||
16
contextutil/contextutil.go
Normal file
16
contextutil/contextutil.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package contextutil
|
||||
|
||||
import "context"
|
||||
|
||||
func WithThreadName(ctx context.Context, threadName string) context.Context {
|
||||
return context.WithValue(ctx, "context_thread", threadName)
|
||||
}
|
||||
|
||||
func ThreadName(ctx context.Context) string {
|
||||
value := ctx.Value("context_thread")
|
||||
if value, ok := value.(string); ok {
|
||||
return value
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
5
docker-compose.yml
Normal file
5
docker-compose.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
runner:
|
||||
build: ./
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
||||
490
docker/helper/helper.go
Normal file
490
docker/helper/helper.go
Normal file
@@ -0,0 +1,490 @@
|
||||
package helper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"docker-updater/config"
|
||||
"docker-updater/constant"
|
||||
"docker-updater/contextutil"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/moby/moby/api/types/container"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type (
|
||||
EventType int
|
||||
EventContainerFunc func(ev ContainerEvent)
|
||||
|
||||
DockerHelper struct {
|
||||
c *client.Client
|
||||
w watcher
|
||||
config config.Configuration
|
||||
}
|
||||
|
||||
watcher struct {
|
||||
running bool
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
gracefulStop chan struct{}
|
||||
cache cache
|
||||
containersEventCallback EventContainerFunc
|
||||
}
|
||||
|
||||
cache struct {
|
||||
mu sync.Mutex
|
||||
containers map[string]cacheEntry
|
||||
}
|
||||
|
||||
cacheEntry struct {
|
||||
c Container
|
||||
}
|
||||
|
||||
Container struct {
|
||||
id string
|
||||
name string
|
||||
image Image
|
||||
status container.ContainerState
|
||||
labels Labels
|
||||
appliedConfiguration config.ContainerConfiguration
|
||||
}
|
||||
|
||||
Labels struct {
|
||||
labels map[string]string
|
||||
hash string
|
||||
}
|
||||
|
||||
Image struct {
|
||||
id string
|
||||
name string
|
||||
hash string
|
||||
}
|
||||
|
||||
ContainerEvent struct {
|
||||
ctx context.Context
|
||||
EventType EventType
|
||||
Data Container
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
NewContainer EventType = iota
|
||||
DeletedContainer
|
||||
UpdatedContainer
|
||||
)
|
||||
|
||||
const (
|
||||
ua string = "com.thelilfrog.docker-updater/%s"
|
||||
)
|
||||
|
||||
func Open(configuration config.Configuration) (*DockerHelper, error) {
|
||||
cli, err := client.New(client.FromEnv, client.WithUserAgent(fmt.Sprintf(ua, constant.ProgramVersion())))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to docker daemon: %s", err)
|
||||
}
|
||||
|
||||
dh := &DockerHelper{
|
||||
w: watcher{
|
||||
cache: cache{
|
||||
containers: make(map[string]cacheEntry),
|
||||
},
|
||||
running: false,
|
||||
gracefulStop: make(chan struct{}),
|
||||
containersEventCallback: func(ev ContainerEvent) {},
|
||||
},
|
||||
config: configuration,
|
||||
c: cli,
|
||||
}
|
||||
|
||||
return dh, nil
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) Close() error {
|
||||
if dh.w.running {
|
||||
dh.w.gracefulStop <- struct{}{}
|
||||
<-dh.w.ctx.Done()
|
||||
}
|
||||
return dh.c.Close()
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) StartWatcher(appCtx context.Context, interval uint) error {
|
||||
if dh.w.running {
|
||||
return fmt.Errorf("cannot start the watcher: already running")
|
||||
}
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
|
||||
dh.w.ctx = contextutil.WithThreadName(ctx, "watcher")
|
||||
dh.w.cancelFunc = cancelFunc
|
||||
|
||||
// watch a first time
|
||||
dh.w.Watch(appCtx, dh)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-dh.w.ctx.Done():
|
||||
{
|
||||
slog.Error("context: watcher closed", "thread", "watcher", "err", dh.w.ctx.Err())
|
||||
dh.w.running = false
|
||||
return
|
||||
}
|
||||
case <-dh.w.gracefulStop:
|
||||
{
|
||||
slog.Info("gracefully stopping the watcher", "thread", "watcher")
|
||||
dh.w.cancelFunc()
|
||||
return
|
||||
}
|
||||
case <-time.After(time.Duration(interval) * time.Second):
|
||||
{
|
||||
dh.w.Watch(dh.w.ctx, dh)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
dh.w.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *watcher) Watch(ctx context.Context, dh *DockerHelper) {
|
||||
w.cache.mu.Lock()
|
||||
defer w.cache.mu.Unlock()
|
||||
|
||||
runningContainers, err := dh.RunningContainers(ctx)
|
||||
if err != nil {
|
||||
slog.Error("cannot fetch the list of running containers", "thread", contextutil.ThreadName(ctx), "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, runningContainer := range runningContainers {
|
||||
if foundContainer, ok := dh.w.cache.containers[runningContainer.name]; ok {
|
||||
if runningContainer.labels.hash != foundContainer.c.labels.hash {
|
||||
foundContainer.c = runningContainer
|
||||
dh.w.cache.containers[runningContainer.name] = foundContainer
|
||||
|
||||
dh.w.containersEventCallback(ContainerEvent{
|
||||
ctx: ctx,
|
||||
EventType: UpdatedContainer,
|
||||
Data: runningContainer,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
dh.w.cache.containers[runningContainer.name] = cacheEntry{
|
||||
c: runningContainer,
|
||||
}
|
||||
|
||||
dh.w.containersEventCallback(ContainerEvent{
|
||||
ctx: ctx,
|
||||
EventType: NewContainer,
|
||||
Data: runningContainer,
|
||||
})
|
||||
}
|
||||
|
||||
notFound := make(map[string]Container)
|
||||
for containerName, containerData := range w.cache.containers {
|
||||
exists := slices.ContainsFunc(runningContainers, func(runningContainer Container) bool {
|
||||
return runningContainer.name == containerName
|
||||
})
|
||||
if !exists {
|
||||
notFound[containerName] = containerData.c
|
||||
}
|
||||
}
|
||||
|
||||
for containerName, containerData := range notFound {
|
||||
delete(w.cache.containers, containerName)
|
||||
w.containersEventCallback(ContainerEvent{
|
||||
ctx: ctx,
|
||||
EventType: DeletedContainer,
|
||||
Data: containerData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) RunningContainers(ctx context.Context) ([]Container, error) {
|
||||
containers, err := dh.c.ContainerList(context.Background(), client.ContainerListOptions{
|
||||
All: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get the list of running containers: %s", err)
|
||||
}
|
||||
|
||||
var res []Container
|
||||
for _, container := range containers.Items {
|
||||
c, err := dh.parseContainer(ctx, container)
|
||||
if err != nil {
|
||||
slog.Warn("the container metadata contains errors, skipping this container")
|
||||
continue
|
||||
}
|
||||
|
||||
res = append(res, c)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) RemoteImageMetadata(ctx context.Context, imageName string) (Image, error) {
|
||||
ref, err := name.ParseReference(imageName)
|
||||
if err != nil {
|
||||
return Image{}, fmt.Errorf("failed to parse image reference: %s", err)
|
||||
}
|
||||
|
||||
image, err := remote.Head(ref)
|
||||
if err != nil {
|
||||
return Image{}, fmt.Errorf("an error occured while getting the metadata of the image in the remote: %s", err)
|
||||
}
|
||||
|
||||
return Image{
|
||||
name: imageName,
|
||||
hash: image.Digest.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) Container(ctx context.Context, containerName string) (Container, error) {
|
||||
containers, err := dh.c.ContainerList(context.Background(), client.ContainerListOptions{
|
||||
All: true,
|
||||
})
|
||||
if err != nil {
|
||||
return Container{}, fmt.Errorf("unable to get the list of containers: %s", err)
|
||||
}
|
||||
|
||||
var res Container
|
||||
for _, container := range containers.Items {
|
||||
name := formatName(container.Names)
|
||||
if name != containerName {
|
||||
continue
|
||||
}
|
||||
|
||||
res, err = dh.parseContainer(ctx, container)
|
||||
if err != nil {
|
||||
return Container{}, fmt.Errorf("failed to get the container: %s", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) StopContainer(ctx context.Context, container Container) error {
|
||||
if _, err := dh.c.ContainerStop(ctx, container.id, client.ContainerStopOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to stop the container: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) StartContainer(ctx context.Context, container Container) error {
|
||||
if _, err := dh.c.ContainerStart(ctx, container.id, client.ContainerStartOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to start the container: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) PullImage(ctx context.Context, imageName string) error {
|
||||
resp, err := dh.c.ImagePull(ctx, imageName, client.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull the image: %s", err)
|
||||
}
|
||||
defer resp.Close()
|
||||
|
||||
var buf []byte
|
||||
for {
|
||||
_, err := resp.Read(buf)
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) ListSimilarContainers(ctx context.Context, containerName string) ([]Container, error) {
|
||||
targetContainer, err := dh.Container(ctx, containerName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get the target container: %s", err)
|
||||
}
|
||||
|
||||
containers, err := dh.RunningContainers(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get running containers: %s", err)
|
||||
}
|
||||
|
||||
var res []Container
|
||||
for _, container := range containers {
|
||||
if container.image.name == targetContainer.name {
|
||||
res = append(res, container)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c Container) ID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (c Container) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
func (c Container) Image() Image {
|
||||
return c.image
|
||||
}
|
||||
|
||||
func (c Container) Enabled() bool {
|
||||
return c.appliedConfiguration.Enabled
|
||||
}
|
||||
|
||||
func (c Container) Schedule() string {
|
||||
return c.appliedConfiguration.Schedule
|
||||
}
|
||||
|
||||
func (i Image) ID() string {
|
||||
return i.id
|
||||
}
|
||||
|
||||
func (i Image) Name() string {
|
||||
return i.name
|
||||
}
|
||||
|
||||
func (i Image) Hash() string {
|
||||
return i.hash
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) SetContainersEventCallback(fn EventContainerFunc) {
|
||||
dh.w.cache.mu.Lock()
|
||||
defer dh.w.cache.mu.Unlock()
|
||||
|
||||
dh.w.containersEventCallback = fn
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) parseContainer(ctx context.Context, container container.Summary) (Container, error) {
|
||||
name := formatName(container.Names)
|
||||
|
||||
config, err := dh.parseLocalConfiguration(container.Labels)
|
||||
if err != nil {
|
||||
slog.Warn("failed to get the local configuration from the labels",
|
||||
"thread", contextutil.ThreadName(ctx),
|
||||
"container", name,
|
||||
"container_id", container.ID,
|
||||
"image_name", container.Image,
|
||||
"image_id", container.ImageID,
|
||||
"labels", container.Labels)
|
||||
return Container{}, err
|
||||
}
|
||||
|
||||
imageMetadata, err := dh.c.ImageInspect(ctx, container.ImageID)
|
||||
if err != nil {
|
||||
slog.Warn("failed to get local image metadata",
|
||||
"thread", contextutil.ThreadName(ctx),
|
||||
"container", name,
|
||||
"container_id", container.ID,
|
||||
"image_name", container.Image,
|
||||
"image_id", container.ImageID)
|
||||
return Container{}, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(imageMetadata.RepoDigests) == 0:
|
||||
{
|
||||
slog.Warn("no remote digest found, ignoring",
|
||||
"thread", contextutil.ThreadName(ctx),
|
||||
"container", name,
|
||||
"container_id", container.ID,
|
||||
"image_name", container.Image,
|
||||
"image_id", container.ImageID)
|
||||
return Container{}, err
|
||||
}
|
||||
case len(imageMetadata.RepoDigests) > 1:
|
||||
{
|
||||
slog.Warn("ambigous remote image digest",
|
||||
"thread", contextutil.ThreadName(ctx),
|
||||
"container", name,
|
||||
"container_id", container.ID,
|
||||
"image_name", container.Image,
|
||||
"image_id", container.ImageID,
|
||||
"repo_digests_count", len(imageMetadata.RepoDigests),
|
||||
"repo_digests", imageMetadata.RepoDigests)
|
||||
return Container{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// get the first repo digest
|
||||
hashes := strings.Split(imageMetadata.RepoDigests[0], "@")
|
||||
if len(hashes) != 2 {
|
||||
slog.Warn("failed to parse remote hash for this image",
|
||||
"thread", contextutil.ThreadName(ctx),
|
||||
"container", name,
|
||||
"container_id", container.ID,
|
||||
"image_name", container.Image,
|
||||
"image_id", container.ImageID,
|
||||
"repo_digest", imageMetadata.RepoDigests[0])
|
||||
return Container{}, err
|
||||
}
|
||||
hash := hashes[1]
|
||||
|
||||
return Container{
|
||||
id: container.ID,
|
||||
name: name,
|
||||
status: container.State,
|
||||
labels: convertLabels(container.Labels),
|
||||
appliedConfiguration: config,
|
||||
image: Image{
|
||||
id: container.ImageID,
|
||||
name: container.Image,
|
||||
hash: hash,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ev ContainerEvent) Context() context.Context {
|
||||
return ev.ctx
|
||||
}
|
||||
|
||||
func formatName(names []string) string {
|
||||
name := strings.Join(names, "-")
|
||||
if after, ok := strings.CutPrefix(name, "/"); ok {
|
||||
name = after
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func convertLabels(labels map[string]string) Labels {
|
||||
var p string
|
||||
|
||||
for key, value := range labels {
|
||||
p += key + ":" + value
|
||||
}
|
||||
|
||||
md5 := crypto.MD5.New()
|
||||
hash := md5.Sum([]byte(p))
|
||||
|
||||
return Labels{
|
||||
labels: labels,
|
||||
hash: hex.EncodeToString(hash),
|
||||
}
|
||||
}
|
||||
|
||||
func (dh *DockerHelper) parseLocalConfiguration(labels map[string]string) (config.ContainerConfiguration, error) {
|
||||
c := dh.config.GlobalContainerConfiguration
|
||||
|
||||
if schedule, ok := labels["com.thelilfrog.image.update.schedule"]; ok {
|
||||
_, err := cron.ParseStandard(schedule)
|
||||
if err != nil {
|
||||
return config.ContainerConfiguration{}, fmt.Errorf("invalid schedule: %s", err)
|
||||
}
|
||||
c.Schedule = schedule
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
15
dockerfile
Normal file
15
dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM golang:1.25.7-alpine3.22 AS build
|
||||
|
||||
COPY . /src
|
||||
|
||||
RUN cd /src \
|
||||
&& go build -o dockerupdater \
|
||||
&& mkdir -p ./fs/var/opt/dockerupdater \
|
||||
&& mkdir -p ./fs/opt/dockerupdater \
|
||||
&& cp dockerupdater ./fs/opt/dockerupdater/dockerupdater
|
||||
|
||||
FROM scratch AS prod
|
||||
|
||||
COPY --from=build --chmod=755 /src/fs /
|
||||
|
||||
ENTRYPOINT [ "/opt/dockerupdater/dockerupdater" ]
|
||||
40
go.mod
Normal file
40
go.mod
Normal file
@@ -0,0 +1,40 @@
|
||||
module docker-updater
|
||||
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/google/go-containerregistry v0.20.7
|
||||
github.com/moby/moby/api v1.53.0
|
||||
github.com/moby/moby/client v0.2.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/cli v29.0.3+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/vbatts/tar-split v0.12.2 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.36.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
)
|
||||
89
go.sum
Normal file
89
go.sum
Normal file
@@ -0,0 +1,89 @@
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v29.0.3+incompatible h1:8J+PZIcF2xLd6h5sHPsp5pvvJA+Sr2wGQxHkRl53a1E=
|
||||
github.com/docker/cli v29.0.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=
|
||||
github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
|
||||
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=
|
||||
github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
59
main.go
Normal file
59
main.go
Normal file
@@ -0,0 +1,59 @@
|
||||
//go:build !debug
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"docker-updater/constant"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
|
||||
var settings []string
|
||||
for _, setting := range info.Settings {
|
||||
settings = append(settings, setting.Key+":"+setting.Value)
|
||||
}
|
||||
|
||||
slog.Error("a panic occured",
|
||||
"err", r,
|
||||
"os", runtime.GOOS,
|
||||
"arch", runtime.GOARCH,
|
||||
"num_cpu", runtime.NumCPU(),
|
||||
"num_cgo_call", runtime.NumCgoCall(),
|
||||
"num_goroutine", runtime.NumGoroutine(),
|
||||
"go_version", info.GoVersion,
|
||||
"build_settings", strings.Join(settings, ", "),
|
||||
)
|
||||
} else {
|
||||
slog.Error("a panic occured, no build info available",
|
||||
"err", r,
|
||||
"os", runtime.GOOS,
|
||||
"arch", runtime.GOARCH,
|
||||
"num_cpu", runtime.NumCPU(),
|
||||
"num_cgo_call", runtime.NumCgoCall(),
|
||||
"num_goroutine", runtime.NumGoroutine(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
debug.SetTraceback("none")
|
||||
|
||||
slog.Info("docker-updater", "version", constant.ProgramVersion().String(), "os", runtime.GOOS, "arch", runtime.GOARCH)
|
||||
|
||||
os.Exit(run(ctx, false))
|
||||
}
|
||||
26
main_debug.go
Normal file
26
main_debug.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build debug
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
func main() {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
defer cancelFunc()
|
||||
|
||||
debug.SetTraceback("all")
|
||||
|
||||
fmt.Printf("docker-updater -- debug (%s/%s)\n\n", runtime.GOOS, runtime.GOARCH)
|
||||
slog.Debug("debug mode enabled", "thread", "main")
|
||||
|
||||
os.Exit(run(ctx, true))
|
||||
}
|
||||
12
makefile
Normal file
12
makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
debug:
|
||||
go run -tags "debug" ./...
|
||||
|
||||
all: clean build
|
||||
|
||||
clean:
|
||||
echo "* Cleaning the output directory"
|
||||
rm -rf build
|
||||
|
||||
build:
|
||||
echo "* Building the binary"
|
||||
./build.sh
|
||||
90
runtime.go
Normal file
90
runtime.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"docker-updater/config"
|
||||
"docker-updater/contextutil"
|
||||
"docker-updater/docker/helper"
|
||||
"docker-updater/runtime"
|
||||
"flag"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
const (
|
||||
success int = 0
|
||||
failure int = 1
|
||||
usageError int = 2
|
||||
)
|
||||
|
||||
func run(ctx context.Context, debug bool) int {
|
||||
var configPath string
|
||||
var verbose bool
|
||||
flag.StringVar(&configPath, "config", "./config.json", "Specify the configuration file path")
|
||||
flag.BoolVar(&verbose, "verbose", false, "Set the log as verbose")
|
||||
flag.Parse()
|
||||
|
||||
ctx = contextutil.WithThreadName(ctx, "main")
|
||||
|
||||
if verbose {
|
||||
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||
}
|
||||
|
||||
config, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
slog.Error("failed to load the configuration", "thread", "main", "err", err)
|
||||
return failure
|
||||
}
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
slog.Error("failed to validate the configuration", "thread", "main", "err", err)
|
||||
return failure
|
||||
}
|
||||
|
||||
docker, err := helper.Open(config)
|
||||
if err != nil {
|
||||
slog.Error("unable to connect to the docker socket", "thread", "main", "err", err)
|
||||
return failure
|
||||
}
|
||||
defer docker.Close()
|
||||
|
||||
if err := docker.StartWatcher(ctx, config.DaemonConfiguration.PullInterval); err != nil {
|
||||
slog.Error("unable to start the docker watcher", "thread", "main", "err", err)
|
||||
}
|
||||
|
||||
el, err := runtime.NewEventLoop(ctx, docker)
|
||||
if err != nil {
|
||||
slog.Error("failed to init the event loop", "thread", "main", "err", err)
|
||||
return failure
|
||||
}
|
||||
defer el.Close()
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal, 10)
|
||||
signal.Notify(c)
|
||||
defer close(c)
|
||||
|
||||
for sig := range c {
|
||||
switch sig {
|
||||
case os.Interrupt:
|
||||
{
|
||||
slog.Info("the process received an interruption signal", "thread", "signal_watcher", "signal", sig.String())
|
||||
el.Close()
|
||||
}
|
||||
case os.Kill:
|
||||
{
|
||||
slog.Info("the process received a kill signal", "thread", "signal_watcher", "signal", sig.String())
|
||||
el.Close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
slog.Info("everything seems ok, starting the event loop", "thread", "main")
|
||||
el.Execute()
|
||||
|
||||
slog.Info("Need to go, byyye 👋", "thread", "main")
|
||||
return success
|
||||
}
|
||||
267
runtime/runtime.go
Normal file
267
runtime/runtime.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"docker-updater/contextutil"
|
||||
"docker-updater/docker/helper"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
type (
|
||||
EventLoop struct {
|
||||
mu sync.Mutex
|
||||
|
||||
docker *helper.DockerHelper
|
||||
|
||||
cr *cron.Cron
|
||||
executors map[string]cron.EntryID
|
||||
|
||||
ctx context.Context
|
||||
eventMu sync.Mutex
|
||||
eventBuffer []helper.ContainerEvent
|
||||
eventTrigger chan struct{}
|
||||
}
|
||||
)
|
||||
|
||||
func NewEventLoop(ctx context.Context, docker *helper.DockerHelper) (*EventLoop, error) {
|
||||
loop := &EventLoop{
|
||||
docker: docker,
|
||||
cr: cron.New(),
|
||||
ctx: contextutil.WithThreadName(context.Background(), "event_loop"),
|
||||
executors: make(map[string]cron.EntryID),
|
||||
}
|
||||
|
||||
if err := loop.firstRun(ctx); err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize the event loop: %s", err)
|
||||
}
|
||||
|
||||
docker.SetContainersEventCallback(loop.onEventReceived)
|
||||
go loop.startEventLoop()
|
||||
|
||||
return loop, nil
|
||||
}
|
||||
|
||||
func (el *EventLoop) Close() error {
|
||||
ctx := el.cr.Stop()
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (el *EventLoop) Execute() {
|
||||
el.cr.Run()
|
||||
slog.Info("event loop stopped", "thread", "main")
|
||||
}
|
||||
|
||||
func (el *EventLoop) firstRun(ctx context.Context) error {
|
||||
el.mu.Lock()
|
||||
defer el.mu.Unlock()
|
||||
|
||||
containers, err := el.docker.RunningContainers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get the list of running containers: %s", err)
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
if container.Enabled() {
|
||||
if err := el.register(container); err != nil {
|
||||
slog.Error("an error occured while registering a container in the scheduler",
|
||||
"thread", contextutil.ThreadName(ctx),
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"schedule", container.Schedule(),
|
||||
"err", err,
|
||||
)
|
||||
continue
|
||||
}
|
||||
slog.Info("a container was scheduled",
|
||||
"thread", contextutil.ThreadName(ctx),
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"schedule", container.Schedule(),
|
||||
)
|
||||
} else {
|
||||
slog.Debug("this container is disabled, ignoring",
|
||||
"thread", contextutil.ThreadName(ctx),
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (el *EventLoop) onEventReceived(ev helper.ContainerEvent) {
|
||||
el.eventMu.Lock()
|
||||
defer el.eventMu.Unlock()
|
||||
|
||||
slog.Debug("event from daemon", "thread", contextutil.ThreadName(ev.Context()), "event_type", ev.EventType, "container_name", ev.Data.Name())
|
||||
|
||||
wakeUpEventLoop := false
|
||||
if len(el.eventBuffer) == 0 {
|
||||
wakeUpEventLoop = true
|
||||
}
|
||||
|
||||
el.eventBuffer = append(el.eventBuffer, ev)
|
||||
|
||||
if len(el.eventBuffer) > 100 {
|
||||
slog.Warn("slow event processing", "thread", contextutil.ThreadName(ev.Context()), "buffer_size", len(el.eventBuffer))
|
||||
}
|
||||
|
||||
if wakeUpEventLoop {
|
||||
el.eventTrigger <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (el *EventLoop) register(container helper.Container) error {
|
||||
id, err := el.cr.AddFunc(container.Schedule(), func() {
|
||||
if err := el.updateImage(container); err != nil {
|
||||
slog.Error("failed to update the container image",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"image_name", container.Image().Name(),
|
||||
"image_id", container.Image().ID(),
|
||||
"err", err,
|
||||
)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("the registration of the container in the scheduler has failed: %s", err)
|
||||
}
|
||||
el.executors[container.ID()] = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (el *EventLoop) unregister(container helper.Container) {
|
||||
if id, ok := el.executors[container.ID()]; ok {
|
||||
el.cr.Remove(id)
|
||||
}
|
||||
delete(el.executors, container.ID())
|
||||
}
|
||||
|
||||
func (el *EventLoop) startEventLoop() {
|
||||
for {
|
||||
if len(el.eventBuffer) == 0 {
|
||||
<-el.eventTrigger
|
||||
}
|
||||
|
||||
el.eventMu.Lock()
|
||||
ev := el.eventBuffer[0]
|
||||
el.eventBuffer = append([]helper.ContainerEvent{}, el.eventBuffer[1:]...)
|
||||
el.eventMu.Unlock()
|
||||
|
||||
el.process(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (el *EventLoop) process(ev helper.ContainerEvent) {
|
||||
el.mu.Lock()
|
||||
defer el.mu.Unlock()
|
||||
|
||||
container := ev.Data
|
||||
switch ev.EventType {
|
||||
case helper.NewContainer:
|
||||
{
|
||||
if container.Enabled() {
|
||||
if err := el.register(container); err != nil {
|
||||
slog.Error("an error occured while registering a container in the scheduler",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"schedule", container.Schedule(),
|
||||
"err", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
slog.Info("a new container was scheduled",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"schedule", container.Schedule(),
|
||||
)
|
||||
} else {
|
||||
slog.Debug("receiving an event for a disabled container, ignoring",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
)
|
||||
}
|
||||
}
|
||||
case helper.DeletedContainer:
|
||||
{
|
||||
el.unregister(container)
|
||||
}
|
||||
case helper.UpdatedContainer:
|
||||
{
|
||||
el.unregister(container)
|
||||
if container.Enabled() {
|
||||
if err := el.register(container); err != nil {
|
||||
slog.Error("an error occured while updating a container in the scheduler",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"schedule", container.Schedule(),
|
||||
"err", err,
|
||||
)
|
||||
return
|
||||
}
|
||||
slog.Info("a container was updated",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"schedule", container.Schedule(),
|
||||
)
|
||||
} else {
|
||||
slog.Debug("a previously enabled container is now disabled, the container will not be updated in the future",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (el *EventLoop) updateImage(container helper.Container) error {
|
||||
image, err := el.docker.RemoteImageMetadata(el.ctx, container.Image().Name())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get metadata from the registry: %s", err)
|
||||
}
|
||||
|
||||
if image.Hash() == container.Image().Hash() {
|
||||
slog.Debug("the image is already up-to-date",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"image_name", container.Image().Name(),
|
||||
"image_id", container.Image().ID(),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := el.docker.StopContainer(el.ctx, container); err != nil {
|
||||
return fmt.Errorf("unable to stop the container to update the image: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := el.docker.StartContainer(el.ctx, container); err != nil {
|
||||
slog.Error("unable to restart the container after the update",
|
||||
"thread", "event_loop",
|
||||
"container_name", container.Name(),
|
||||
"container_id", container.ID(),
|
||||
"image_name", container.Image().Name(),
|
||||
"image_id", container.Image().ID(),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := el.docker.PullImage(el.ctx, container.Image().Name()); err != nil {
|
||||
return fmt.Errorf("failed to pull the image from the registry: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user