243 lines
6.1 KiB
Go
243 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.iamthefij.com/iamthefij/slog"
|
|
dockerTypes "github.com/docker/docker/api/types"
|
|
dockerClient "github.com/docker/docker/client"
|
|
)
|
|
|
|
var (
|
|
// defaultRegistryBaseURL is the base URL of the docker registry
|
|
defaultRegistryBaseURL = "https://hub.docker.com"
|
|
// maxPages is the max number of pages to fetch from docker registry results
|
|
maxPages = 10
|
|
|
|
// Regexp used to extract tag information
|
|
tagRegexp = regexp.MustCompile(`(.*):[vV]{0,1}([0-9.]+)(-(.*)){0,1}`)
|
|
// version of tag checker
|
|
version = "dev"
|
|
)
|
|
|
|
// ImageTag is wraps an image and tag values for a container
|
|
type ImageTag struct {
|
|
ImageTag string
|
|
Registry string
|
|
Image string
|
|
TagDesc string
|
|
Version string
|
|
VersionParts []int
|
|
}
|
|
|
|
// IsComparable will return true if to images share the same base, description, and version resolution
|
|
func (thisTag ImageTag) IsComparable(otherTag ImageTag) bool {
|
|
return thisTag.Image == otherTag.Image && thisTag.TagDesc == otherTag.TagDesc && len(thisTag.VersionParts) == len(otherTag.VersionParts)
|
|
|
|
}
|
|
|
|
// IsNewerThan will return true if two tags are comparable and this tag is newer than the one passed in
|
|
func (thisTag ImageTag) IsNewerThan(otherTag ImageTag) bool {
|
|
return thisTag.CompareTo(otherTag) == 1
|
|
}
|
|
|
|
// CompareTo compares two ImageTags. It will return 0 if they are not-comparable or equal 1 if newer and -1 if less
|
|
func (thisTag ImageTag) CompareTo(otherTag ImageTag) int {
|
|
if !thisTag.IsComparable(otherTag) {
|
|
return 0
|
|
}
|
|
|
|
for i := range thisTag.VersionParts {
|
|
if thisTag.VersionParts[i] > otherTag.VersionParts[i] {
|
|
return 1
|
|
} else if thisTag.VersionParts[i] < otherTag.VersionParts[i] {
|
|
return -1
|
|
}
|
|
}
|
|
|
|
// Everything must be equal
|
|
return 0
|
|
}
|
|
|
|
// ParseImageTag parses an image and tag name to a struct
|
|
func ParseImageTag(imageTag string) (ImageTag, error) {
|
|
results := tagRegexp.FindStringSubmatch(imageTag)
|
|
if results == nil || results[0] == "" {
|
|
return ImageTag{}, fmt.Errorf("could not recognize versions in %s", imageTag)
|
|
}
|
|
|
|
// Extract image name with repo
|
|
image := results[1]
|
|
registry := ""
|
|
switch strings.Count(image, "/") {
|
|
case 0:
|
|
image = "library/" + image
|
|
case 2:
|
|
parts := strings.Split(image, "/")
|
|
if parts[0] != "docker.io" {
|
|
registry = parts[0]
|
|
}
|
|
image = strings.Join(parts[1:], "/")
|
|
}
|
|
|
|
// Extract version number
|
|
version := results[2]
|
|
versionParts := []int{}
|
|
var verPart int
|
|
var err error
|
|
for _, v := range strings.Split(version, ".") {
|
|
verPart, err = strconv.Atoi(v)
|
|
if err != nil {
|
|
return ImageTag{}, err
|
|
}
|
|
versionParts = append(versionParts, verPart)
|
|
}
|
|
|
|
return ImageTag{
|
|
ImageTag: imageTag,
|
|
Image: image,
|
|
Registry: registry,
|
|
Version: version,
|
|
VersionParts: versionParts,
|
|
TagDesc: results[4],
|
|
}, nil
|
|
}
|
|
|
|
func getJSON(url string, response interface{}) error {
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// defer func() { _ = resp.Body.Close() }()
|
|
defer resp.Body.Close()
|
|
decoder := json.NewDecoder(resp.Body)
|
|
err = decoder.Decode(response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func listTags(current ImageTag) ([]ImageTag, error) {
|
|
var err error
|
|
results := []ImageTag{}
|
|
|
|
type tagsResponse struct {
|
|
Count int
|
|
Next string
|
|
Previous string
|
|
Results []struct {
|
|
ID int
|
|
LastUpdated string `json:"last_updated"`
|
|
Name string
|
|
}
|
|
}
|
|
|
|
registryBaseURL := defaultRegistryBaseURL
|
|
if current.Registry != "" {
|
|
registryBaseURL = fmt.Sprintf("https://%s", current.Registry)
|
|
}
|
|
url := fmt.Sprintf("%s/v2/repositories/%s/tags", registryBaseURL, current.Image)
|
|
pageCount := 0
|
|
var response tagsResponse
|
|
var newTag ImageTag
|
|
for url != "" && pageCount <= maxPages {
|
|
err = getJSON(url, &response)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
|
|
for _, tag := range response.Results {
|
|
newTag, err = ParseImageTag(fmt.Sprintf("%s:%s", current.Image, tag.Name))
|
|
if err == nil {
|
|
results = append(results, newTag)
|
|
}
|
|
}
|
|
url = response.Next
|
|
pageCount++
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func getNewerTags(current ImageTag) ([]ImageTag, error) {
|
|
newerTags := []ImageTag{}
|
|
tags, err := listTags(current)
|
|
if err != nil {
|
|
return newerTags, err
|
|
}
|
|
for _, tag := range tags {
|
|
if tag.IsNewerThan(current) {
|
|
newerTags = append(newerTags, tag)
|
|
}
|
|
}
|
|
// Sort tags with newest first
|
|
sort.Slice(newerTags, func(i, j int) bool { return newerTags[i].CompareTo(newerTags[j]) == 1 })
|
|
return newerTags, nil
|
|
}
|
|
|
|
func main() {
|
|
flag.StringVar(&defaultRegistryBaseURL, "registry-url", defaultRegistryBaseURL, "base url of the registry you want to check against")
|
|
flag.IntVar(&maxPages, "max-pages", maxPages, "max number of pages to retrieve from registry")
|
|
var showVersion = flag.Bool("version", false, "display the version and exit")
|
|
flag.BoolVar(&slog.DebugLevel, "debug", false, "show debug logs")
|
|
flag.Parse()
|
|
|
|
// Print version if asked
|
|
if *showVersion {
|
|
fmt.Println("version:", version)
|
|
os.Exit(0)
|
|
}
|
|
dockerClient, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv)
|
|
if err != nil {
|
|
fmt.Println("Could not initialize docker client")
|
|
panic(err)
|
|
}
|
|
|
|
containers, err := dockerClient.ContainerList(context.Background(), dockerTypes.ContainerListOptions{})
|
|
if err != nil {
|
|
fmt.Println("Could list container from docker client")
|
|
panic(err)
|
|
}
|
|
|
|
hasUpdate := false
|
|
images := map[string]bool{}
|
|
for _, container := range containers {
|
|
images[container.Image] = true
|
|
}
|
|
for image := range images {
|
|
slog.Debug("[%s] Checking for updates...", image)
|
|
it, err := ParseImageTag(image)
|
|
if err != nil {
|
|
slog.Debug("[%s] Can't parse tag: %v", image, err)
|
|
continue
|
|
}
|
|
newerTags, err := getNewerTags(it)
|
|
slog.PanicOnErr(err, "[%s] failed getting new tags", image)
|
|
if len(newerTags) == 0 {
|
|
slog.Debug("[%s] No newer versions found", image)
|
|
continue
|
|
}
|
|
hasUpdate = true
|
|
if slog.DebugLevel {
|
|
slog.Info("[%s] Newer version found! Recommended update to %s", image, newerTags[0].ImageTag)
|
|
} else {
|
|
fmt.Printf("[%s] Newer version found! Recommended update to %s\n", image, newerTags[0].ImageTag)
|
|
}
|
|
}
|
|
|
|
if hasUpdate {
|
|
os.Exit(10)
|
|
}
|
|
}
|