Compare commits
24 Commits
Author | SHA1 | Date |
---|---|---|
IamTheFij | ee3bc789ea | |
IamTheFij | 4ff321933b | |
IamTheFij | 5ba2fc721b | |
IamTheFij | 620640dada | |
IamTheFij | 2ad1a94f4f | |
IamTheFij | 1f8782a8f9 | |
IamTheFij | 17258861ba | |
IamTheFij | 2cfe9429b7 | |
IamTheFij | ba702d2ab9 | |
IamTheFij | 8f9c5b6a91 | |
IamTheFij | 4a173f4cb2 | |
IamTheFij | fb2262c83e | |
IamTheFij | e7ee16231b | |
IamTheFij | 50c46b1b6e | |
IamTheFij | a8d7407093 | |
IamTheFij | 016129b5ed | |
IamTheFij | 533f4963be | |
IamTheFij | bdb7a06c19 | |
IamTheFij | 13a2a599dc | |
IamTheFij | c6855a1b73 | |
IamTheFij | 1a89933feb | |
IamTheFij | 7ee081f9f0 | |
IamTheFij | e08a3cf14a | |
IamTheFij | cb0ffa3497 |
|
@ -0,0 +1 @@
|
|||
tags
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
kind: pipeline
|
||||
name: test
|
||||
|
||||
steps:
|
||||
# - name: test
|
||||
# image: golang:1.15
|
||||
# commands:
|
||||
# - make test
|
||||
|
||||
- name: check
|
||||
image: iamthefij/drone-pre-commit:personal
|
||||
commands:
|
||||
- make check
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: publish
|
||||
|
||||
depends_on:
|
||||
- test
|
||||
|
||||
trigger:
|
||||
event:
|
||||
- push
|
||||
- tag
|
||||
refs:
|
||||
- refs/heads/master
|
||||
- refs/tags/v*
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:1.17
|
||||
environment:
|
||||
VERSION: ${DRONE_TAG:-${DRONE_COMMIT}}
|
||||
commands:
|
||||
- make build-all-static
|
||||
|
||||
- name: gitea release
|
||||
image: plugins/gitea-release
|
||||
settings:
|
||||
title: ${DRONE_TAG}
|
||||
files: dist/*
|
||||
checksum:
|
||||
- md5
|
||||
- sha1
|
||||
- sha256
|
||||
- sha512
|
||||
base_url:
|
||||
from_secret: gitea_base_url
|
||||
api_key:
|
||||
from_secret: gitea_token
|
||||
when:
|
||||
event: tag
|
||||
|
||||
- name: push image - arm
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: iamthefij/tag-checker
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-arm
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
build_args:
|
||||
- ARCH=arm
|
||||
- REPO=arm32v7
|
||||
|
||||
- name: push image - arm64
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: iamthefij/tag-checker
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-arm64
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
build_args:
|
||||
- ARCH=arm64
|
||||
- REPO=arm64v8
|
||||
|
||||
- name: push image - amd64
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: iamthefij/tag-checker
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-amd64
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
- name: publish manifest
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
spec: manifest.tmpl
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
username:
|
||||
from_secret: docker_username # pragma: whitelist secret
|
||||
password:
|
||||
from_secret: docker_password # pragma: whitelist secret
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: notify
|
||||
|
||||
depends_on:
|
||||
- test
|
||||
- publish
|
||||
|
||||
trigger:
|
||||
status:
|
||||
- failure
|
||||
|
||||
steps:
|
||||
|
||||
- name: notify
|
||||
image: drillster/drone-email
|
||||
settings:
|
||||
host:
|
||||
from_secret: SMTP_HOST # pragma: whitelist secret
|
||||
username:
|
||||
from_secret: SMTP_USER # pragma: whitelist secret
|
||||
password:
|
||||
from_secret: SMTP_PASS # pragma: whitelist secret
|
||||
from: drone@iamthefij.com
|
|
@ -14,6 +14,4 @@
|
|||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
__pycache__/
|
||||
venv/
|
||||
dist/
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-merge-conflict
|
||||
- repo: https://github.com/dnephin/pre-commit-golang
|
||||
rev: v0.3.5
|
||||
hooks:
|
||||
- id: go-fmt
|
||||
- id: go-imports
|
||||
# - id: gometalinter
|
||||
# - id: golangci-lint
|
||||
- repo: https://github.com/IamTheFij/docker-pre-commit
|
||||
rev: v2.0.0
|
||||
hooks:
|
||||
- id: hadolint-system
|
16
Dockerfile
16
Dockerfile
|
@ -1,10 +1,12 @@
|
|||
FROM python:3
|
||||
# hadolint ignore=DL3007
|
||||
FROM alpine:latest as certs
|
||||
# hadolint ignore=DL3018
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
RUN mkdir -p /app
|
||||
WORKDIR /app
|
||||
FROM scratch
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
COPY ./requirements.txt /app/
|
||||
RUN pip install -r ./requirements.txt
|
||||
COPY . /app
|
||||
ARG ARCH=amd64
|
||||
COPY dist/tag-checker-linux-${ARCH} /tag-checker
|
||||
|
||||
CMD ["python", "./main.py"]
|
||||
ENTRYPOINT [ "/tag-checker" ]
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
ARG REPO=library
|
||||
FROM golang:1.17-alpine AS builder
|
||||
|
||||
# hadolint ignore=DL3018
|
||||
RUN apk add --no-cache git
|
||||
|
||||
# hadolint ignore=DL3059
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./go.mod ./go.sum /app/
|
||||
RUN go mod download
|
||||
|
||||
COPY ./main.go /app/
|
||||
|
||||
ARG ARCH=amd64
|
||||
ARG VERSION=dev
|
||||
ENV CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH}
|
||||
RUN go build -ldflags "-X main.version=${VERSION}" -a -installsuffix nocgo -o tag-checker .
|
||||
|
||||
# hadolint ignore=DL3007
|
||||
FROM alpine:latest as certs
|
||||
# hadolint ignore=DL3018
|
||||
RUN apk --no-cache add ca-certificates
|
||||
|
||||
FROM scratch
|
||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=builder /app/tag-checker /
|
||||
|
||||
ENTRYPOINT [ "/tag-checker" ]
|
|
@ -0,0 +1,132 @@
|
|||
NAME ?= tag-checker
|
||||
OUTPUT ?= dist/$(NAME)
|
||||
DOCKER_TAG ?= $(NAME)-dev-${USER}
|
||||
GIT_TAG_NAME := $(shell git tag -l --contains HEAD)
|
||||
GIT_SHA := $(shell git rev-parse HEAD)
|
||||
VERSION ?= $(if $(GIT_TAG_NAME),$(GIT_TAG_NAME),$(GIT_SHA))
|
||||
|
||||
GOFILES = *.go go.mod go.sum
|
||||
|
||||
.PHONY: default
|
||||
default: build
|
||||
|
||||
.PHONY: all
|
||||
all: check test itest
|
||||
|
||||
# Downloads dependencies into vendor directory
|
||||
vendor: $(GOFILES)
|
||||
go mod vendor
|
||||
|
||||
# Runs the application, useful while developing
|
||||
.PHONY: run
|
||||
run:
|
||||
go run .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -coverprofile=coverage.out
|
||||
go tool cover -func=coverage.out
|
||||
@go tool cover -func=coverage.out | awk -v target=75.0% \
|
||||
'/^total:/ { print "Total coverage: " $$3 " Minimum coverage: " target; if ($$3+0.0 >= target+0.0) print "ok"; else { print "fail"; exit 1; } }'
|
||||
|
||||
# Installs pre-commit hooks
|
||||
.PHONY: install-hooks
|
||||
install-hooks:
|
||||
pre-commit install --install-hooks
|
||||
|
||||
# Runs pre-commit checks on files
|
||||
.PHONY: check
|
||||
check:
|
||||
pre-commit run --all-files
|
||||
|
||||
# Output target
|
||||
$(OUTPUT): $(GOFILES)
|
||||
@echo Version: $(VERSION)
|
||||
go build -ldflags '-X "main.version=$(VERSION)"' -o $(OUTPUT)
|
||||
|
||||
# Alias for building
|
||||
.PHONY: build
|
||||
build: $(OUTPUT)
|
||||
|
||||
$(OUTPUT)-darwin-amd64: $(GOFILES)
|
||||
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 \
|
||||
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
|
||||
-o $(OUTPUT)-darwin-amd64
|
||||
|
||||
$(OUTPUT)-linux-amd64: $(GOFILES)
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
|
||||
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
|
||||
-o $(OUTPUT)-linux-amd64
|
||||
|
||||
$(OUTPUT)-linux-arm: $(GOFILES)
|
||||
GOOS=linux GOARCH=arm CGO_ENABLED=0 \
|
||||
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
|
||||
-o $(OUTPUT)-linux-arm
|
||||
|
||||
$(OUTPUT)-linux-arm64: $(GOFILES)
|
||||
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 \
|
||||
go build -ldflags '-X "main.version=$(VERSION)"' -a -installsuffix nocgo \
|
||||
-o $(OUTPUT)-linux-arm64
|
||||
|
||||
.PHONY: build-linux-static
|
||||
build-linux-static: $(OUTPUT)-linux-amd64 $(OUTPUT)-linux-arm $(OUTPUT)-linux-arm64
|
||||
|
||||
.PHONY: build-all-static
|
||||
build-all-static: $(OUTPUT)-darwin-amd64 build-linux-static
|
||||
|
||||
# Cleans all build artifacts
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f $(OUTPUT)
|
||||
rm -f $(OUTPUT)-linux-*
|
||||
|
||||
# Cleans vendor directory
|
||||
.PHONY: clean-vendor
|
||||
clean-vendor:
|
||||
rm -fr ./vendor
|
||||
|
||||
.PHONY: docker-build
|
||||
docker-build: $(OUTPUT)-linux-amd64
|
||||
docker build . -t $(DOCKER_TAG)-linux-amd64
|
||||
|
||||
# Cross build for arm architechtures
|
||||
.PHONY: docker-build-arm
|
||||
docker-build-arm: $(OUTPUT)-linux-arm
|
||||
docker build --build-arg REPO=arm32v7 --build-arg ARCH=arm . -t $(DOCKER_TAG)-linux-arm
|
||||
|
||||
.PHONY: docker-build-arm
|
||||
docker-build-arm64: $(OUTPUT)-linux-arm64
|
||||
docker build --build-arg REPO=arm64v8 --build-arg ARCH=arm64 . -t $(DOCKER_TAG)-linux-arm64
|
||||
|
||||
.PHONY: docker-run
|
||||
docker-run: docker-build
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run $(DOCKER_TAG)-linux-amd64
|
||||
|
||||
# Cross run on host architechture
|
||||
.PHONY: docker-run-arm
|
||||
docker-run-arm: docker-build-arm
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run $(DOCKER_TAG)-linux-arm
|
||||
|
||||
.PHONY: docker-run-arm64
|
||||
docker-run-arm64: docker-build-arm64
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock --name $(DOCKER_TAG)-run $(DOCKER_TAG)-linux-arm64
|
||||
|
||||
# Multi stage builds
|
||||
.PHONY: docker-staged-build
|
||||
docker-staged-build:
|
||||
docker build --build-arg VERSION=$(VERSION) \
|
||||
-t $(DOCKER_TAG)-linux-amd64 \
|
||||
-f Dockerfile.multi-stage .
|
||||
|
||||
# Cross build for arm architechtures
|
||||
.PHONY: docker-staged-build-arm
|
||||
docker-staged-build-arm:
|
||||
docker build --build-arg VERSION=$(VERSION) \
|
||||
--build-arg REPO=arm32v7 --build-arg ARCH=arm -t $(DOCKER_TAG)-linux-arm \
|
||||
-f Dockerfile.multi-stage .
|
||||
|
||||
.PHONY: docker-staged-build-arm
|
||||
docker-staged-build-arm64:
|
||||
docker build --build-arg VERSION=$(VERSION) \
|
||||
--build-arg REPO=arm64v8 --build-arg ARCH=arm64 -t $(DOCKER_TAG)-linux-arm64 \
|
||||
-f Dockerfile.multi-stage .
|
17
README.md
17
README.md
|
@ -1,8 +1,19 @@
|
|||
# docker-check-version-updates
|
||||
# tag-checker
|
||||
|
||||
Checks current running containers for newer tags according to semver
|
||||
|
||||
Usage:
|
||||
|
||||
python -m venv venv
|
||||
./venv/bin/python main.py
|
||||
tag-checker -h
|
||||
|
||||
-max-pages int
|
||||
max number of pages to retrieve from registry (default 10)
|
||||
-registry-url string
|
||||
base url of the registry you want to check against (default "https://registry.hub.docker.com")
|
||||
-version
|
||||
display the version of dockron and exit
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
You can either install the binary from the Releases tab or use the image hosted on Docker Hub at `iamthefij/tag-checker`.
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
version: "3.4"
|
||||
|
||||
services:
|
||||
main:
|
||||
build: .
|
||||
volumes:
|
||||
- "/var/run/docker.sock:/var/run/docker.sock"
|
|
@ -0,0 +1,32 @@
|
|||
module github.com/iamthefij/tag-checker
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
git.iamthefij.com/iamthefij/slog v1.3.0
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.0 // indirect
|
||||
github.com/containerd/containerd v1.4.3 // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.1 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
google.golang.org/grpc v1.33.2 // indirect
|
||||
gotest.tools v2.2.0+incompatible // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.4.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a // indirect
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
)
|
|
@ -0,0 +1,122 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
git.iamthefij.com/iamthefij/slog v1.3.0 h1:4Hu5PQvDrW5e3FrTS3q2iIXW0iPvhNY/9qJsqDR3K3I=
|
||||
git.iamthefij.com/iamthefij/slog v1.3.0/go.mod h1:1RUj4hcCompZkAxXCRfUX786tb3cM/Zpkn97dGfUfbg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU=
|
||||
github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/containerd v1.4.3 h1:ijQT13JedHSHrQGWFcGEwzcNKrAGIiZ+jSD5QQG07SY=
|
||||
github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
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/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible h1:SiUATuP//KecDjpOK2tvZJgeScYAklvyjfK8JZlU6fo=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1 h1:ZFgWrT+bLgsYPirOnRfKLYJLvssAegOj/hgyMFdJZe0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
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.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
@ -0,0 +1,247 @@
|
|||
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)
|
||||
}
|
||||
}
|
144
main.py
144
main.py
|
@ -1,144 +0,0 @@
|
|||
"""
|
||||
Checks to see if newer tagged versions of running images exist
|
||||
|
||||
When a newer tag based on semver is found, the tags will be printed and the script will exit with
|
||||
a non-zero code.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
|
||||
import docker # type: ignore
|
||||
import requests
|
||||
|
||||
|
||||
class NotComparableException(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageTag(object):
|
||||
image_tag: str
|
||||
image: str
|
||||
full_tag: str
|
||||
version: str
|
||||
tag_desc: str
|
||||
version_parts: List[int]
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, image_tag):
|
||||
image, _, full_tag = image_tag.partition(":")
|
||||
version, _, tag_desc = full_tag.partition("-")
|
||||
# Remove leading v
|
||||
if version[0].lower() == "v":
|
||||
version = version[1:]
|
||||
try:
|
||||
version_parts = [int(p) for p in version.split(".")]
|
||||
except ValueError:
|
||||
version_parts = []
|
||||
return ImageTag(
|
||||
image_tag=image_tag,
|
||||
image=image,
|
||||
full_tag=full_tag,
|
||||
version=version,
|
||||
tag_desc=tag_desc,
|
||||
version_parts=version_parts,
|
||||
)
|
||||
|
||||
def is_same_image(self, other):
|
||||
return self.image == other.image
|
||||
|
||||
def is_same_type(self, other):
|
||||
return self.tag_desc == other.tag_desc
|
||||
|
||||
def is_same_grain(self, other):
|
||||
return len(self.version_parts) == len(other.version_parts)
|
||||
|
||||
def is_comparable(self, other):
|
||||
return all(
|
||||
(
|
||||
self.is_same_image(other),
|
||||
self.is_same_type(other),
|
||||
self.is_same_grain(other),
|
||||
)
|
||||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not self.is_comparable(other):
|
||||
raise NotComparableException()
|
||||
|
||||
return self.version_parts == other.version_parts
|
||||
|
||||
def __lt__(self, other):
|
||||
if not self.is_comparable(other):
|
||||
raise NotComparableException()
|
||||
|
||||
for s, o in zip(self.version_parts, other.version_parts):
|
||||
if s < o:
|
||||
return True
|
||||
elif s > o:
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def is_newer_than(self, other):
|
||||
return self.is_comparable(other) and self > other
|
||||
|
||||
|
||||
def get_all_tags(image_name: str) -> Generator[ImageTag, None, None]:
|
||||
"""Generates all tags for a given image"""
|
||||
if "/" not in image_name:
|
||||
image_name = f"library/{image_name}"
|
||||
url = "https://registry.hub.docker.com/v2/repositories/{}/tags".format(
|
||||
image_name,
|
||||
)
|
||||
page_count = 0
|
||||
max_pages = 1000
|
||||
while url and page_count <= max_pages:
|
||||
data = requests.get(url).json()
|
||||
for tag in data["results"]:
|
||||
try:
|
||||
yield ImageTag.from_str(f"{image_name}:{tag['name']}")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
url = data["next"]
|
||||
page_count += 1
|
||||
|
||||
|
||||
def generate_message(current: ImageTag, newer_tags: List[ImageTag]) -> str:
|
||||
if not current.version_parts:
|
||||
return f"[{current.image_tag}] No numeric version recognized"
|
||||
if not newer_tags:
|
||||
return f"[{current.image_tag}] No newer tags found for image tag"
|
||||
else:
|
||||
newer_tags = list(reversed(sorted(newer_tags)))
|
||||
tags_list = ", ".join(tag.version for tag in newer_tags)
|
||||
latest_tag = newer_tags[0].image_tag
|
||||
return f"[{current.image_tag}] New versions found {tags_list}. Recommended update to {latest_tag}"
|
||||
|
||||
|
||||
def run() -> int:
|
||||
client = docker.from_env()
|
||||
running_images = {container.image.tags[0]
|
||||
for container in client.containers.list()}
|
||||
|
||||
has_update = False
|
||||
for image_name in running_images:
|
||||
current = ImageTag.from_str(image_name)
|
||||
|
||||
newer_tags: List[ImageTag] = []
|
||||
if current.version_parts:
|
||||
newer_tags = [
|
||||
tag for tag in get_all_tags(current.image) if tag.is_newer_than(current)
|
||||
]
|
||||
has_update |= bool(newer_tags)
|
||||
print(generate_message(current, newer_tags))
|
||||
|
||||
if has_update:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(run())
|
|
@ -0,0 +1,132 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTagParsing(t *testing.T) {
|
||||
cases := []struct {
|
||||
tag string
|
||||
expected ImageTag
|
||||
expectsError bool
|
||||
}{
|
||||
{
|
||||
"image",
|
||||
ImageTag{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"image:latest",
|
||||
ImageTag{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"user/image:v1",
|
||||
ImageTag{
|
||||
ImageTag: "user/image:v1",
|
||||
Image: "user/image",
|
||||
Version: "1",
|
||||
VersionParts: []int{1},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"image:v1",
|
||||
ImageTag{
|
||||
ImageTag: "image:v1",
|
||||
Image: "library/image",
|
||||
Version: "1",
|
||||
VersionParts: []int{1},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"image:v1.0",
|
||||
ImageTag{
|
||||
ImageTag: "image:v1.0",
|
||||
Image: "library/image",
|
||||
Version: "1.0",
|
||||
VersionParts: []int{1, 0},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"image:v1.0.0",
|
||||
ImageTag{
|
||||
ImageTag: "image:v1.0.0",
|
||||
Image: "library/image",
|
||||
Version: "1.0.0",
|
||||
VersionParts: []int{1, 0, 0},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"image:v1.0.0-desc",
|
||||
ImageTag{
|
||||
ImageTag: "image:v1.0.0-desc",
|
||||
Image: "library/image",
|
||||
TagDesc: "desc",
|
||||
Version: "1.0.0",
|
||||
VersionParts: []int{1, 0, 0},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"image:v1.0.0-desc@sha256:123abc",
|
||||
ImageTag{
|
||||
ImageTag: "image:v1.0.0-desc@sha256:123abc",
|
||||
Image: "library/image",
|
||||
TagDesc: "desc",
|
||||
TagSha: "sha256:123abc",
|
||||
Version: "1.0.0",
|
||||
VersionParts: []int{1, 0, 0},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"image:v1.0.0@sha256:123abc",
|
||||
ImageTag{
|
||||
ImageTag: "image:v1.0.0@sha256:123abc",
|
||||
Image: "library/image",
|
||||
TagSha: "sha256:123abc",
|
||||
Version: "1.0.0",
|
||||
VersionParts: []int{1, 0, 0},
|
||||
},
|
||||
false,
|
||||
},
|
||||
// Real world example that had caused issues
|
||||
{
|
||||
"matrixdotorg/synapse:v1.41.0@sha256:75f2b3c35c047693f4f4334d4ceb97951de1ba12bd3c6182c8a2ee624d7c5ab7",
|
||||
ImageTag{
|
||||
ImageTag: "matrixdotorg/synapse:v1.41.0@sha256:75f2b3c35c047693f4f4334d4ceb97951de1ba12bd3c6182c8a2ee624d7c5ab7",
|
||||
Image: "matrixdotorg/synapse",
|
||||
TagSha: "sha256:75f2b3c35c047693f4f4334d4ceb97951de1ba12bd3c6182c8a2ee624d7c5ab7",
|
||||
Version: "1.41.0",
|
||||
VersionParts: []int{1, 41, 0},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
result, err := ParseImageTag(c.tag)
|
||||
if c.expectsError {
|
||||
if err == nil {
|
||||
t.Errorf("ParseImageTag(%s): expected erro but didn't get one", c.tag)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("ParseImageTag(%s): unexpected error: %v", c.tag, err)
|
||||
}
|
||||
if !reflect.DeepEqual(result, c.expected) {
|
||||
t.Errorf(
|
||||
"ParseImageTag(%s): unexpected result. Actual: %+v Expected: %+v",
|
||||
c.tag,
|
||||
result,
|
||||
c.expected,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
image: iamthefij/tag-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
-
|
||||
image: iamthefij/tag-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
-
|
||||
image: iamthefij/tag-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
-
|
||||
image: iamthefij/tag-checker:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v7
|
|
@ -1,5 +0,0 @@
|
|||
pyls
|
||||
pyls-black
|
||||
ipython
|
||||
ipdb
|
||||
mypy
|
|
@ -1,2 +0,0 @@
|
|||
requests
|
||||
docker
|
Loading…
Reference in New Issue