You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
6.4 KiB
247 lines
6.4 KiB
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 |
|
// The general format is "(registry/namespace/image):v(version)-(description)@(sha)" |
|
tagRegexp = regexp.MustCompile(`([a-zA-Z0-9-_/.]+):[vV]{0,1}([0-9.]+)(-([a-zA-Z0-9_-]*)){0,1}(@([:0-9a-z]+)){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 |
|
TagSha 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], |
|
TagSha: results[6], |
|
}, 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 { |
|
slog.Errorf("[%s] Could not get JSON response from url %s: %v", current.ImageTag, url, err) |
|
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 { |
|
slog.Errorf("[%s] Could not list tags: %v", current.ImageTag, err) |
|
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.Debugf("[%s] Checking for updates...", image) |
|
it, err := ParseImageTag(image) |
|
if err != nil { |
|
slog.Debugf("[%s] Can't parse tag: %v", image, err) |
|
continue |
|
} |
|
newerTags, err := getNewerTags(it) |
|
slog.OnErrPanicf(err, "[%s] failed getting new tags", image) |
|
if len(newerTags) == 0 { |
|
slog.Debugf("[%s] No newer versions found", image) |
|
continue |
|
} |
|
hasUpdate = true |
|
if slog.DebugLevel { |
|
slog.Infof("[%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) |
|
} |
|
}
|
|
|