tag-checker/main.go

224 lines
5.2 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"regexp"
"sort"
"strconv"
"strings"
dockerTypes "github.com/docker/docker/api/types"
dockerClient "github.com/docker/docker/client"
)
var registryBase = "https://registry.hub.docker.com"
var maxPages = 10
var tagRegexp = regexp.MustCompile(`(.*):[vV]{0,1}([0-9.]+)(-(.*)){0,1}`)
// ImageTag is wraps an image and tag values for a container
type ImageTag struct {
ImageTag 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]
if !strings.Contains(image, "/") {
image = "library/" + image
}
// 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,
Version: version,
VersionParts: versionParts,
TagDesc: results[4],
}, nil
}
// ImageTagSort is an interface for sorting ImageTags
type ImageTagSort []ImageTag
// Len gives the length of the image sorter interface
func (slice ImageTagSort) Len() int {
return len(slice)
}
// Less returns true if the first value is less than or equal to the second
func (slice ImageTagSort) Less(i, j int) bool {
return slice[i].CompareTo(slice[j]) == -1
}
// Swap two values in slice
func (slice ImageTagSort) Swap(i, j int) {
slice[i], slice[j] = slice[j], slice[i]
}
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
}
}
url := fmt.Sprintf("%s/v2/repositories/%s/tags", registryBase, 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.Sort(sort.Reverse(ImageTagSort(newerTags)))
return newerTags, nil
}
func main() {
dockerClient, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv)
if err != nil {
panic(err)
}
containers, err := dockerClient.ContainerList(context.Background(), dockerTypes.ContainerListOptions{})
if err != nil {
panic(err)
}
hasUpdate := false
images := map[string]bool{}
for _, container := range containers {
images[container.Image] = true
}
for image := range images {
fmt.Printf("[%s] Checking for updates...\n", image)
it, err := ParseImageTag(image)
if err != nil {
fmt.Printf("[%s] Can't parse tag: %v\n", image, err)
continue
}
newerTags, err := getNewerTags(it)
if err != nil {
fmt.Printf("[%s] Panic getting new tags: %v\n", image, err)
panic(err)
}
if len(newerTags) == 0 {
fmt.Printf("[%s] No newer versions found\n", image)
continue
}
hasUpdate = true
fmt.Printf("[%s] Newer version found. Recommend update to %s\n", image, newerTags[0].ImageTag)
}
if hasUpdate {
os.Exit(10)
}
}