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) } }