Initial commit

This commit is contained in:
CrazyMax
2019-06-04 22:11:54 +02:00
commit 8513d49cc9
42 changed files with 2475 additions and 0 deletions

17
.dockerignore Normal file
View File

@@ -0,0 +1,17 @@
/.idea
/*.iml
/.dev
/.git
/.github
/.res
/bin
/dist
/.editorconfig
/.gitignore
/.goreleaser.yml
/.travis.yml
/build.sh
/CHANGELOG.md
/LICENSE
/README.md

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
# This file is for unifying the coding style for different editors and IDEs.
# More information at http://editorconfig.org
root = true
[*]
charset = utf-8
indent_size = 2
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.go]
indent_style = tab
[*.md]
trim_trailing_whitespace = false

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: crazy-max
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=X2NYRW7D9KL4E

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
---
### Behaviour
#### Steps to reproduce this issue
1.
2.
3.
#### Expected behaviour
> Tell me what should happen
#### Actual behaviour
> Tell me what happens instead
### Configuration
* Diun version :
* Platform (windows/linux) :
```yml
# paste your YAML configuration file here and remove sensitive data
```
### Logs
```
# paste logs here (set log level to debug first)
```

29
.github/SUPPORT.md vendored Normal file
View File

@@ -0,0 +1,29 @@
# Support [![](https://isitmaintained.com/badge/resolution/crazy-max/diun.svg)](https://isitmaintained.com/project/crazy-max/diun)
## Reporting an issue
Please do a search in [open issues](https://github.com/crazy-max/diun/issues?utf8=%E2%9C%93&q=) to see if the issue or feature request has already been filed.
If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment.
:+1: - upvote
:-1: - downvote
If you cannot find an existing issue that describes your bug or feature, submit an issue using the guidelines below.
## Writing good bug reports and feature requests
File a single issue per problem and feature request.
* Do not enumerate multiple bugs or feature requests in the same issue.
* Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes.
The more information you can provide, the more likely someone will be successful reproducing the issue and finding a fix.
You are now ready to [create a new issue](https://github.com/crazy-max/diun/issues/new/choose)!
## Closure policy
* Issues that don't have the information requested above (when applicable) will be closed immediately and the poster directed to the support guidelines.
* Issues that go a week without a response from original poster are subject to closure at my discretion.

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/.idea
/*.iml
/.dev
/bin
/dist

41
.goreleaser.yml Normal file
View File

@@ -0,0 +1,41 @@
before:
hooks:
- go mod download
builds:
-
main: ./cmd/main.go
ldflags:
- -s -w -X main.version={{.Version}}
env:
- CGO_ENABLED=0
goos:
- darwin
- linux
- freebsd
- windows
goarch:
- 386
- amd64
- arm
- arm64
goarm:
- 6
- 7
ignore:
- goos: freebsd
goarch: arm
- goos: freebsd
goarch: arm64
archive:
replacements:
386: i386
amd64: x86_64
format_overrides:
- goos: windows
format: zip
files:
- LICENSE
- README.md
- CHANGELOG.md
checksum:
name_template: 'checksums.txt'

View File

@@ -0,0 +1,14 @@
version: "3.2"
services:
diun:
image: crazymax/diun:latest
container_name: diun
volumes:
- "./diun.yml:/diun.yml:ro"
environment:
- "TZ=Europe/Paris"
- "LOG_LEVEL=info"
- "LOG_JSON=false"
- "RUN_ONCE=false"
restart: always

BIN
.res/diun.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
.res/paypal-donate.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

48
.travis.yml Normal file
View File

@@ -0,0 +1,48 @@
language: go
go:
- 1.12.x
services:
- docker
addons:
apt:
packages:
- docker-ce
env:
global:
- GO111MODULE=on
- DOCKER_LOGIN=crazymax
- DOCKER_USERNAME=crazymax
- DOCKER_REPONAME=diun
- QUAY_LOGIN=crazymax
- QUAY_USERNAME=crazymax
- QUAY_REPONAME=diun
- secure: rn4yR3+WxNN+XAjHqq91GYcimHYMac4Z14zkrPkjD1HiSap8lkEyEzpVFIHAKcRxUauurSD3InB+CYKNXnFNy8l25vYTGsJE4v5gcYeM7xLAArFXAX/fgJte3X5Y985KA8LEj2iL23eKQP/stdHz7Q6532fKNAWpIn/AXMPCcq/OfgTL/IDvub0CdQ+ujB/r4fge45V/ayUcwGybXSfCE9sgfpejF53i1BB4KUTXsTpANuaiQ4P7T2MhFlAZm7PKERZrBqDZ96naGfGMUMbnOVg1G7vixRvicKYrEYfCcY3ynyH7fOAXBUlcRul7k+AnuMCzCzqHfwl79v4vvDRPbKiQW+M3AesTxH20URaJEgopN5AVlN9hgziV9Btcy57QPWuSPq5vJuR359QOu9xQCVzto7y0xCJiynZf8ekWRu6DVUvI9RomFZLNioK2U8nHcCuadkGI2cYSnv9NxnAuvUrdzZhTDeD9OkbYM6r9pjk2UwoxwD/IJOqcHICynOUeZa7zd/HvO+HgEuRalcEwAXfALlljKJWedAJRtQ2GIHAO7I17Y20zipbYR94iJoNc5m5ED60eKGFkVhPPeE3n46D3rex6fl8KVVMexK0QTEenBOgnWW8VJcKCkLvSJ5kvIQlgFBSw2qCCLBSZ84fPgVPCWSOLnTF5/Sh4azyA3yI= # DOCKER_PASSWORD
- secure: KpHMIy36vZDVf9/cOg7USbQmlEjBlz7+PuIC1P2jPSMo1yp+hAhPbqZwQBh3FZXCtaFbaZEVnnm+4jnKgPirWgqFsMLcP3v4KcP5AW70wVn4fymWeveeDRSnAmgaXhJxxzZJaB6Ps+B9ULtZ8vymrJt9vsiaJ6CO79BeCoQZsTJulSiKlNVgZSMGHfylTt/RAGfWJoLAnS9ROFPpOShXyuSjNzxBopOngWsUxvgQIm3U4qg33DqBcxrrxrKwYIAY9NKCh5yLAohVDzZ+iew/bNmQ/8vqfQQlk4773RYKYa5CtTuHhW/a+Vxsk0CBY5Pu4aZBF2uRFal1IiWiwnFA8GgMeCLAea9uu2vOOqQ2nr/N+GVRP9hMLb7lsEO1eUu1951YgbsUy7naPSdT75aW+6XGbRfFMCx1uCDwK7D3VhY5unUSwn1M0E+jexTbkd1dxy5STUPp3kkh3CvD9W3meGN/ASW+Ky79wjOoVyUOwx7i7jRKHP2ZFOQQvV6Ce/Nb1qNJcul60QpS8Me3XbbQn0GbaHEJvYyNjDDMKJ8vQQJTEIJHiUA2m6a11QmSN00qH6NFM17z60LEnOErNiD3oyBErm2JE4cgClpn3DCtF7FqPA/tjzfiDnVSO/rzvEJ9W/34/lmS1BYlStJuEltPcbpmPUDYsqvAwP0MMWZEgvw= # QUAY_PASSWORD
before_install:
- sudo apt-get update
- docker --version
install:
- curl -sL https://git.io/goreleaser | head -n -2 | bash
- tar -xf /tmp/goreleaser.tar.gz -C $GOPATH/bin
- goreleaser -v
script: ./build.sh
deploy:
provider: releases
api_key:
secure: NkexB33JizM5kiqqxS7qo5xk6vsKp5+jRQT/gpSVt91XBrzOEnTksVdeoIFoJJ5pFfZfmDyV3e7MsGM9hnf6HUe6NKiSpOAjN7egTs3PqfwxOynFG8IyOIrUO6Q8EceVfl3/UeSH2hr86SUkO8tCsI4g7wTlnoIjsqNs0o5MAX24e2TAr9aFpoTAbDMOijE5Oqjgm/dIAik/N2MS+v8kqZP/KRx8/4hcUUhV5jt0D7LyxjqK3uAZN7bg4e3rltEMnfvKJJ/2s1RnJYzT9GqrATTL6CVZZisspFMgUjBT0y8fsiqqCQVAU/XoftiDIoJm1QOricDchqy7MrGTDjmR+IfVvEqlkp/bWaCEJURRESpsqnFU1g6ega1hpllGQsSCzymTA1SGs0ArUurSQY8S4anz3SiZI4mNusCIXO8w1rrAOXJya++6dTDws8svcPzuYbL7F1/Wh2ejyl9po9p8D69YPv3eVTMDjOQC/2PMNfQuTHku3ozToyvIG4hEHIudpTFvcn2IJXlMeYGoc08fAXSR95SFUVpUUgIsrGwCArw4fi7S5qBT27Dux2HDtGCtR8rqMrqxjHy2aME0oFsVq+tXvMkoDYXukMA50QHBuoMHhBGwzXMXj7ujW/eyLShu1hGikFSgiSTeX+Nnw/lixnsRClsf748CjdkJ86DPciw=
file_glob: true
file:
- dist/checksums.txt
- dist/*.tar.gz
- dist/*.zip
skip_cleanup: true
draft: true
on:
tags: true

5
CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
# Changelog
## 0.1.0 (2019/06/04)
* Initial version

46
Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
FROM golang:1.12.4 as builder
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
WORKDIR /app
COPY go.mod .
COPY go.sum .
RUN go version
RUN go mod download
COPY . ./
RUN cp /usr/local/go/lib/time/zoneinfo.zip ./ \
&& CGO_ENABLED=0 GOOS=linux go build \
-ldflags "-w -s -X 'main.version=${VERSION}'" \
-v -o diun cmd/main.go
FROM alpine:latest
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
LABEL maintainer="CrazyMax" \
org.label-schema.build-date=$BUILD_DATE \
org.label-schema.name="Diun" \
org.label-schema.description="Docker image update notifier" \
org.label-schema.version=$VERSION \
org.label-schema.url="https://github.com/crazy-max/diun" \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vcs-url="https://github.com/crazy-max/diun" \
org.label-schema.vendor="CrazyMax" \
org.label-schema.schema-version="1.0"
RUN apk --update --no-cache add \
ca-certificates \
libressl \
tzdata \
&& rm -rf /tmp/* /var/cache/apk/*
COPY --from=builder /app/diun /usr/local/bin/diun
COPY --from=builder /app/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip
VOLUME [ "/data" ]
CMD [ "diun", "--config", "/diun.yml", "--docker" ]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 CrazyMax
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

207
README.md Normal file
View File

@@ -0,0 +1,207 @@
<p align="center"><a href="https://github.com/crazy-max/diun" target="_blank"><img height="128"src="https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png"></a></p>
<p align="center">
<a href="https://github.com/crazy-max/diun/releases/latest"><img src="https://img.shields.io/github/release/crazy-max/diun.svg?style=flat-square" alt="GitHub release"></a>
<a href="https://github.com/crazy-max/diun/releases/latest"><img src="https://img.shields.io/github/downloads/crazy-max/diun/total.svg?style=flat-square" alt="Total downloads"></a>
<a href="https://hub.docker.com/r/crazymax/diun/"><img src="https://img.shields.io/badge/dynamic/json.svg?label=tag&query=$.results[1].name&url=https://hub.docker.com/v2/repositories/crazymax/diun/tags&style=flat-square" alt="Latest Version"></a>
<a href="https://travis-ci.com/crazy-max/diun"><img src="https://img.shields.io/travis/com/crazy-max/diun/master.svg?style=flat-square" alt="Build Status"></a>
<a href="https://hub.docker.com/r/crazymax/diun/"><img src="https://img.shields.io/docker/stars/crazymax/diun.svg?style=flat-square" alt="Docker Stars"></a>
<a href="https://hub.docker.com/r/crazymax/diun/"><img src="https://img.shields.io/docker/pulls/crazymax/diun.svg?style=flat-square" alt="Docker Pulls"></a>
<br /><a href="https://quay.io/repository/crazymax/diun"><img src="https://quay.io/repository/crazymax/diun/status?style=flat-square" alt="Docker Repository on Quay"></a>
<a href="https://goreportcard.com/report/github.com/crazy-max/diun"><img src="https://goreportcard.com/badge/github.com/crazy-max/diun?style=flat-square" alt="Go Report"></a>
<a href="https://www.codacy.com/app/crazy-max/diun"><img src="https://img.shields.io/codacy/grade/93db381dca8b441cb69b45b75f5e10ed.svg?style=flat-square" alt="Code Quality"></a>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=X2NYRW7D9KL4E"><img src="https://img.shields.io/badge/donate-paypal-7057ff.svg?style=flat-square" alt="Donate Paypal"></a>
</p>
## About
**Diun** :bell: is a CLI application written in [Go](https://golang.org/) to receive notifications :inbox_tray: when a Docker :whale: image is updated. With Go, this app can be used across many platforms :game_die: and architectures. This support includes Linux, FreeBSD, macOS and Windows on architectures like amd64, i386, ARM and others.
## Features
* Allow to watch a full Docker repository and report new tags
* Include and exclude filters with regular expression for tags
* Internal cron implementation through go routines
* Beautiful email report
* Webhook notification
* Enhanced logging
* Timezone can be changed
* :whale: Official [Docker image available](#docker)
## Download
Diun binaries are available in [releases](https://github.com/crazy-max/diun/releases) page.
Choose the archive matching the destination platform and extract diun:
```
$ cd /opt
$ wget -qO- https://github.com/crazy-max/diun/releases/download/v0.1.0/diun_0.1.0_linux_x86_64.tar.gz | tar -zxvf - diun
```
After getting the binary, it can be tested with `./diun --help` or moved to a permanent location.
```
$ ./diun --help
usage: diun --config=CONFIG [<flags>]
Docker image update notifier. More info on https://github.com/crazy-max/diun
Flags:
--help Show context-sensitive help (also try --help-long and
--help-man).
--config=CONFIG Diun configuration file.
--timezone="UTC" Timezone assigned to Diun.
--log-level="info" Set log level.
--log-json Enable JSON logging output.
--run-once Run once on startup.
--docker Enable Docker mode.
--version Show application version.
```
## Usage
`diun --config=CONFIG [<flags>]`
* `--help` : Show help text and exit. _Optional_.
* `--version` : Show version and exit. _Optional_.
* `--config <path>` : Diun YAML configuration file. **Required**. (example: `diun.yml`).
* `--timezone <timezone>` : Timezone assigned to Diun. _Optional_. (default: `UTC`).
* `--log-level <level>` : Log level output. _Optional_. (default: `info`).
* `--log-json` : Enable JSON logging output. _Optional_. (default: `false`).
* `--run-once` : Run once on startup. _Optional_. (default: `false`).
## Configuration
Before running Diun, you must create your first configuration file. Here is a YAML structure example :
```yml
db:
path: diun.db
watch:
schedule: 0 */30 * * * *
notif:
mail:
enable: false
host: localhost
port: 25
ssl: false
insecure_skip_verify: false
username:
password:
from:
to:
webhook:
enable: false
endpoint: http://webhook.foo.com/sd54qad89azd5a
method: GET
headers:
Content-Type: application/json
Authorization: Token123456
timeout: 10
reg_creds:
aregistrycred:
username: foo
password: bar
another:
username: foo2
password: bar2
items:
-
image: docker.io/crazymax/nextcloud:latest
reg_cred_id: aregistrycred
-
image: jfrog-docker-reg2.bintray.io/jfrog/artifactory-oss:4.0.0
reg_cred_id: another
-
image: quay.io/coreos/hyperkube
-
image: crazymax/swarm-cronjob
watch_repo: true
include_tags:
- ^1.2.*
```
* `db`
* `path`: Path to Bolt database file where images analysis are stored. Flag `--docker` force this path to `/data/diun.db` (default: `diun.db`).
* `watch`
* `schedule`: [CRON expression](https://godoc.org/github.com/crazy-max/cron#hdr-CRON_Expression_Format) to schedule Diun watcher. _Optional_. (default: `0 */30 * * * *`).
* `notif`
* `mail`
* `enable`: Enable email reports (default: `false`).
* `host`: SMTP server host (default: `localhost`). **required**
* `port`: SMTP server port (default: `25`). **required**
* `ssl`: SSL defines whether an SSL connection is used. Should be false in most cases since the auth mechanism should use STARTTLS (default: `false`).
* `insecure_skip_verify`: Controls whether a client verifies the server's certificate chain and host name (default: `false`).
* `username`: SMTP username.
* `password`: SMTP password.
* `from`: Sender email address. **required**
* `to`: Recipient email address. **required**
* `webhook`
* `enable`: Enable webhook notification (default: `false`).
* `endpoint`: URL of the HTTP request. **required**
* `method`: HTTP method (default: `GET`). **required**
* `headers`: Map of additional headers to be sent.
* `timeout`: Timeout specifies a time limit for the request to be made. (default: `10`).
* `reg_creds`: Map of registry credentials to use with items. Key is the ID and value is a struct with the following fields:
* `username`: Registry username.
* `password`: Registry password.
* `items`: Slice of items to watch with the following fields:
* `image`: Docker image to watch using `registry/path:tag` format. If registry is omitted, `docker.io` will be used. If tag is omitted, `latest` will be used. **required**
* `reg_cred_id`: Registry credential ID from `reg_creds` to use.
* `insecure_tls`: Allow contacting docker registries over HTTP, or HTTPS with failed TLS verification (default: `false`).
* `watch_repo`: Watch all tags of this `image` repository (default: `false`).
* `include_tags`: List of regular expressions to include tags. Can be useful if you use `watch_repo`.
* `exclude_tags`: List of regular expressions to exclude tags. Can be useful if you use `watch_repo`.
* `timeout`: Timeout is the maximum amount of time for the TCP connection to establish (default: `5`).
## Docker
Diun provides automatically updated Docker :whale: images within [Docker Hub](https://hub.docker.com/r/crazymax/diun) and [Quay](https://quay.io/repository/crazymax/diun). It is possible to always use the latest stable tag or to use another service that handles updating Docker images.
Environment variables can be used within your container :
* `TZ` : Timezone assigned
* `LOG_LEVEL` : Log level output (default `info`)
* `LOG_JSON`: Enable JSON logging output (default `false`)
* `RUN_ONCE`: Run once on startup (default `false`)
Docker compose is the recommended way to run this image. Copy the content of folder [.res/compose](.res/compose) in `/opt/diun/` on your host for example. Edit the compose and config file with your preferences and run the following commands :
```bash
docker-compose up -d
docker-compose logs -f
```
Or use the following command :
```bash
$ docker run -d --name diun \
-e "TZ=Europe/Paris" \
-e "LOG_LEVEL=info" \
-e "LOG_JSON=false" \
-e "RUN_ONCE=false" \
-v "$(pwd)/diun.yml:/diun.yml:ro" \
crazymax/diun:latest
```
## TODO
* [ ] Scan Dockerfile
* [ ] Watch images from Docker daemon
## How can I help ?
All kinds of contributions are welcome :raised_hands:!<br />
The most basic way to show your support is to star :star2: the project, or to raise issues :speech_balloon:<br />
But we're not gonna lie to each other, I'd rather you buy me a beer or two :beers:!
[![Paypal](.res/paypal-donate.png)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=X2NYRW7D9KL4E)
## License
MIT. See `LICENSE` for more details.

100
build.sh Normal file
View File

@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -e
PROJECT=diun
VERSION=${TRAVIS_TAG:-dev}
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
BUILD_TAG=docker_build
BUILD_WORKINGDIR=${BUILD_WORKINGDIR:-.}
DOCKERFILE=${DOCKERFILE:-Dockerfile}
VCS_REF=${TRAVIS_COMMIT::8}
PUSH_LATEST=${PUSH_LATEST:-true}
DOCKER_USERNAME=${DOCKER_USERNAME:-crazymax}
DOCKER_LOGIN=${DOCKER_LOGIN:-crazymax}
DOCKER_REPONAME=${DOCKER_REPONAME:-diun}
QUAY_USERNAME=${QUAY_USERNAME:-crazymax}
QUAY_LOGIN=${QUAY_LOGIN:-crazymax}
QUAY_REPONAME=${QUAY_REPONAME:-diun}
# Check dev or travis
BRANCH=${TRAVIS_BRANCH:-local}
if [[ ${TRAVIS_PULL_REQUEST} == "true" ]]; then
BRANCH=${TRAVIS_PULL_REQUEST_BRANCH}
fi
DOCKER_TAG=${BRANCH:-local}
if [[ "$BRANCH" == "local" ]]; then
BUILD_DATE=
else
DOCKER_TAG=latest
VERSION=${VERSION#v}
fi
echo "PROJECT=${PROJECT}"
echo "VERSION=${VERSION}"
echo "BUILD_DATE=${BUILD_DATE}"
echo "BUILD_TAG=${BUILD_TAG}"
echo "BUILD_WORKINGDIR=${BUILD_WORKINGDIR}"
echo "DOCKERFILE=${DOCKERFILE}"
echo "VCS_REF=${VCS_REF}"
echo "PUSH_LATEST=${PUSH_LATEST}"
echo "DOCKER_LOGIN=${DOCKER_LOGIN}"
echo "DOCKER_USERNAME=${DOCKER_USERNAME}"
echo "DOCKER_REPONAME=${DOCKER_REPONAME}"
echo "QUAY_LOGIN=${QUAY_LOGIN}"
echo "QUAY_USERNAME=${QUAY_USERNAME}"
echo "QUAY_REPONAME=${QUAY_REPONAME}"
echo "TRAVIS_BRANCH=${TRAVIS_BRANCH}"
echo "TRAVIS_PULL_REQUEST=${TRAVIS_PULL_REQUEST}"
echo "BRANCH=${BRANCH}"
echo "DOCKER_TAG=${DOCKER_TAG}"
echo
echo "### Goreleaser"
if [[ -n "$TRAVIS_TAG" ]]; then
goreleaser release --skip-publish --rm-dist
else
goreleaser release --snapshot --rm-dist
fi
echo "### Docker build"
docker build \
--build-arg BUILD_DATE=${BUILD_DATE} \
--build-arg VCS_REF=${VCS_REF} \
--build-arg VERSION=${VERSION} \
-t ${BUILD_TAG} -f ${DOCKERFILE} ${BUILD_WORKINGDIR}
echo
if [ "${VERSION}" == "dev" -o "${TRAVIS_PULL_REQUEST}" == "true" ]; then
echo "INFO: This is a PR or an untagged build, skipping push..."
exit 0
fi
if [[ ! -z ${DOCKER_PASSWORD} ]]; then
echo "### Push to Docker Hub..."
echo "$DOCKER_PASSWORD" | docker login --username "$DOCKER_LOGIN" --password-stdin > /dev/null 2>&1
if [ "${DOCKER_TAG}" == "latest" -a "${PUSH_LATEST}" == "true" ]; then
docker tag ${BUILD_TAG} ${DOCKER_USERNAME}/${DOCKER_REPONAME}:${DOCKER_TAG}
fi
if [[ "${VERSION}" != "latest" ]]; then
docker tag ${BUILD_TAG} ${DOCKER_USERNAME}/${DOCKER_REPONAME}:${VERSION}
fi
docker push ${DOCKER_USERNAME}/${DOCKER_REPONAME}
if [[ ! -z ${MICROBADGER_HOOK} ]]; then
echo "Call MicroBadger hook"
curl -X POST ${MICROBADGER_HOOK}
echo
fi
echo
fi
if [[ ! -z ${QUAY_PASSWORD} ]]; then
echo "### Push to Quay..."
echo "$QUAY_PASSWORD" | docker login quay.io --username "$QUAY_LOGIN" --password-stdin > /dev/null 2>&1
if [ "${DOCKER_TAG}" == "latest" -a "${PUSH_LATEST}" == "true" ]; then
docker tag ${BUILD_TAG} quay.io/${QUAY_USERNAME}/${QUAY_REPONAME}:${DOCKER_TAG}
fi
if [[ "${VERSION}" != "latest" ]]; then
docker tag ${BUILD_TAG} quay.io/${QUAY_USERNAME}/${QUAY_REPONAME}:${VERSION}
fi
docker push quay.io/${QUAY_USERNAME}/${QUAY_REPONAME}
echo
fi

94
cmd/main.go Normal file
View File

@@ -0,0 +1,94 @@
package main
import (
"os"
"os/signal"
"runtime"
"syscall"
"time"
"github.com/crazy-max/cron"
"github.com/crazy-max/diun/internal/app"
"github.com/crazy-max/diun/internal/config"
"github.com/crazy-max/diun/internal/logging"
"github.com/crazy-max/diun/internal/model"
"github.com/rs/zerolog/log"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
diun *app.Diun
flags model.Flags
c *cron.Cron
version = "dev"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
// Parse command line
kingpin.Flag("config", "Diun configuration file.").Envar("CONFIG").Required().StringVar(&flags.Cfgfile)
kingpin.Flag("timezone", "Timezone assigned to Diun.").Envar("TZ").Default("UTC").StringVar(&flags.Timezone)
kingpin.Flag("log-level", "Set log level.").Envar("LOG_LEVEL").Default("info").StringVar(&flags.LogLevel)
kingpin.Flag("log-json", "Enable JSON logging output.").Envar("LOG_JSON").Default("false").BoolVar(&flags.LogJson)
kingpin.Flag("run-once", "Run once on startup.").Envar("RUN_ONCE").Default("false").BoolVar(&flags.RunOnce)
kingpin.Flag("docker", "Enable Docker mode.").Envar("DOCKER").Default("false").BoolVar(&flags.Docker)
kingpin.UsageTemplate(kingpin.CompactUsageTemplate).Version(version).Author("CrazyMax")
kingpin.CommandLine.Name = "diun"
kingpin.CommandLine.Help = `Docker image update notifier. More info on https://github.com/crazy-max/diun`
kingpin.Parse()
// Load timezone location
location, err := time.LoadLocation(flags.Timezone)
if err != nil {
log.Panic().Err(err).Msgf("Cannot load timezone %s", flags.Timezone)
}
// Init
logging.Configure(&flags, location)
log.Info().Msgf("Starting Diun %s", version)
// Handle os signals
channel := make(chan os.Signal)
signal.Notify(channel, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-channel
if c != nil {
c.Stop()
}
diun.Close()
log.Warn().Msgf("Caught signal %v", sig)
os.Exit(0)
}()
// Load and check configuration
cfg, err := config.Load(flags, version)
if err != nil {
log.Fatal().Err(err).Msg("Cannot load configuration")
}
if err := cfg.Check(); err != nil {
cfg.Display()
log.Fatal().Err(err).Msg("Improper configuration")
}
cfg.Display()
// Init
if diun, err = app.New(cfg); err != nil {
log.Fatal().Err(err).Msg("Cannot initialize Diun")
}
// Run once
if flags.RunOnce {
diun.Run()
}
// Start scheduler
c = cron.NewWithLocation(location)
log.Info().Msgf("Start watcher with schedule %s", cfg.Watch.Schedule)
if err := c.AddJob(cfg.Watch.Schedule, diun); err != nil {
log.Fatal().Err(err).Msg("Cannot create cron task")
}
c.Start()
select {}
}

40
go.mod Normal file
View File

@@ -0,0 +1,40 @@
module github.com/crazy-max/diun
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Microsoft/go-winio v0.4.12 // indirect
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc // indirect
github.com/containers/image v1.5.1
github.com/containers/storage v1.12.8 // indirect
github.com/crazy-max/cron v1.2.2
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1 // indirect
github.com/docker/docker-credential-helpers v0.6.2 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
github.com/google/go-cmp v0.3.0 // indirect
github.com/gorilla/mux v1.7.2 // indirect
github.com/hako/durafmt v0.0.0-20180520121703-7b7ae1e72ead
github.com/imdario/mergo v0.3.7
github.com/matcornic/hermes/v2 v2.0.2
github.com/opencontainers/go-digest v1.0.0-rc1
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v0.1.1 // indirect
github.com/prometheus/client_golang v0.9.3 // indirect
github.com/rs/zerolog v1.14.3
github.com/sirupsen/logrus v1.4.2 // indirect
github.com/stretchr/testify v1.3.0 // indirect
go.etcd.io/bbolt v1.3.2
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/yaml.v2 v2.2.2
gotest.tools v2.2.0+incompatible // indirect
)
replace github.com/docker/docker => github.com/docker/engine v0.0.0-20180718150940-a3ef7e9a9bda

171
go.sum Normal file
View File

@@ -0,0 +1,171 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY=
github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg=
github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containers/image v1.5.1 h1:ssEuj1c24uJvdMkUa2IrawuEFZBP12p6WzrjNBTQxE0=
github.com/containers/image v1.5.1/go.mod h1:8Vtij257IWSanUQKe1tAeNOm2sRVkSqQTVQ1IlwI3+M=
github.com/containers/storage v1.12.8 h1:5js4CV+oEW0E9pOA/cJcMsY2V7xLlMhIqFZmQlVpTuo=
github.com/containers/storage v1.12.8/go.mod h1:+RirK6VQAqskQlaTBrOG6ulDvn4si2QjFE1NZCn06MM=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/crazy-max/cron v1.2.2 h1:DQB06Nbb9Lah7UrFaRthTzJABto+qRThKcIZLlTOczA=
github.com/crazy-max/cron v1.2.2/go.mod h1:1VehsRAaLIq0DQZP1LSRFzKx2+ar2fCpysK7qoqQt1M=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
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-credential-helpers v0.6.2 h1:CrW9H1VMf3a4GrtyAi7IUJjkJVpwBBpX0+mvkvYJaus=
github.com/docker/docker-credential-helpers v0.6.2/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
github.com/docker/engine v0.0.0-20180718150940-a3ef7e9a9bda h1:v1VUX0+ILrFSsGTp2FUvfgHSiQ6wmI1NnCho1MQ9CYU=
github.com/docker/engine v0.0.0-20180718150940-a3ef7e9a9bda/go.mod h1:3CPr2caMgTHxxIAZgEMd3uLYPDlRvPqCpyeRf6ncPcY=
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-metrics v0.0.0-20181218153428-b84716841b82 h1:X0fj836zx99zFu83v/M79DuBn84IL/Syx1SY6Y5ZEMA=
github.com/docker/go-metrics v0.0.0-20181218153428-b84716841b82/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
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/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4=
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E=
github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/hako/durafmt v0.0.0-20180520121703-7b7ae1e72ead h1:Y9WOGZY2nw5ksbEf5AIpk+vK52Tdg/VN/rHFRfEeeGQ=
github.com/hako/durafmt v0.0.0-20180520121703-7b7ae1e72ead/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matcornic/hermes/v2 v2.0.2 h1:au/C9liIetFg0c8Zv+woDrFPkWk7UGLi9QQuO013/00=
github.com/matcornic/hermes/v2 v2.0.2/go.mod h1:iVsJWSIS4NtMNtgan22sy6lt7pImok7bATGPWCoaKNY=
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
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/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.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_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

203
internal/app/diun.go Normal file
View File

@@ -0,0 +1,203 @@
package app
import (
"fmt"
"sync/atomic"
"time"
"github.com/crazy-max/diun/internal/config"
"github.com/crazy-max/diun/internal/db"
"github.com/crazy-max/diun/internal/model"
"github.com/crazy-max/diun/internal/notif"
"github.com/crazy-max/diun/internal/utl"
"github.com/crazy-max/diun/pkg/registry"
"github.com/hako/durafmt"
"github.com/rs/zerolog/log"
)
// Diun represents an active diun object
type Diun struct {
cfg *config.Config
reg *registry.Client
db *db.Client
notif *notif.Client
locker uint32
}
// New creates new diun instance
func New(cfg *config.Config) (*Diun, error) {
// Registry client
regcli, err := registry.New()
if err != nil {
return nil, err
}
// DB client
dbcli, err := db.New(cfg.Db)
if err != nil {
return nil, err
}
// Notification client
notifcli, err := notif.New(cfg.Notif, cfg.App)
if err != nil {
return nil, err
}
return &Diun{
cfg: cfg,
reg: regcli,
db: dbcli,
notif: notifcli,
}, nil
}
// Run starts diun process
func (di *Diun) Run() {
if !atomic.CompareAndSwapUint32(&di.locker, 0, 1) {
log.Warn().Msg("Already running")
return
}
defer atomic.StoreUint32(&di.locker, 0)
defer di.trackTime(time.Now(), "Finished, total time spent: ")
// Iterate items
for _, item := range di.cfg.Items {
image, err := registry.ParseImage(item.Image)
if err != nil {
log.Error().Err(err).Str("image", item.Image).Msg("Cannot parse image")
continue
}
opts := &registry.Options{
Image: image,
Username: item.RegCred.Username,
Password: item.RegCred.Password,
InsecureTLS: item.InsecureTLS,
}
if err := di.analyzeImage(item, opts); err != nil {
log.Error().Err(err).Str("image", opts.Image.String()).Msg("Cannot analyze image")
continue
}
if item.WatchRepo {
di.analyzeRepo(item, opts)
}
}
}
func (di *Diun) analyzeImage(item model.Item, opts *registry.Options) error {
if !di.isIncluded(opts.Image.Tag, item.IncludeTags) {
log.Warn().Str("image", opts.Image.String()).Msgf("Tag %s not included", opts.Image.Tag)
return nil
} else if di.isExcluded(opts.Image.Tag, item.ExcludeTags) {
log.Warn().Str("image", opts.Image.String()).Msgf("Tag %s excluded", opts.Image.Tag)
return nil
}
log.Debug().Str("image", opts.Image.String()).Msgf("Analyzing")
liveAna, err := di.reg.Inspect(opts)
if err != nil {
return err
}
dbAna, err := di.db.GetAnalysis(opts.Image)
if err != nil {
return err
}
status := model.ImageStatusUnchange
if dbAna.Name == "" {
status = model.ImageStatusNew
log.Info().Str("image", opts.Image.String()).Msgf("New image found")
} else if !liveAna.Created.Equal(*dbAna.Created) {
status = model.ImageStatusUpdate
log.Info().Str("image", opts.Image.String()).Msgf("Image update found")
} else {
log.Debug().Str("image", opts.Image.String()).Msgf("No changes")
return nil
}
if err := di.db.PutAnalysis(opts.Image, liveAna); err != nil {
return err
}
log.Debug().Str("image", opts.Image.String()).Msg("Analysis saved to database")
di.notif.Send(model.NotifEntry{
Status: status,
Image: opts.Image,
Analysis: liveAna,
})
return nil
}
func (di *Diun) analyzeRepo(item model.Item, opts *registry.Options) {
tags, err := di.reg.Tags(opts)
if err != nil {
log.Error().Err(err).Str("image", opts.Image.String()).Msg("Cannot retrieve tags")
return
}
log.Debug().Str("image", opts.Image.String()).Msgf("%d tag(s) found", len(tags))
for _, tag := range tags {
if tag == opts.Image.Tag {
continue
}
simage := fmt.Sprintf("%s/%s:%s", opts.Image.Domain, opts.Image.Path, tag)
image, err := registry.ParseImage(simage)
if err != nil {
log.Error().Err(err).Str("image", simage).Msg("Cannot parse image")
continue
}
opts := &registry.Options{
Image: image,
Username: opts.Username,
Password: opts.Password,
InsecureTLS: opts.InsecureTLS,
}
if err := di.analyzeImage(item, opts); err != nil {
log.Error().Err(err).Str("image", image.String()).Msg("Cannot analyze image")
continue
}
}
}
// Close closes diun
func (di *Diun) Close() {
if err := di.db.Close(); err != nil {
log.Warn().Err(err).Msg("Cannot close database")
}
}
func (di *Diun) isIncluded(tag string, includes []string) bool {
if len(includes) == 0 {
return true
}
for _, include := range includes {
if utl.MatchString(include, tag) {
return true
}
}
return false
}
func (di *Diun) isExcluded(tag string, excludes []string) bool {
if len(excludes) == 0 {
return false
}
for _, exclude := range excludes {
if utl.MatchString(exclude, tag) {
return true
}
}
return false
}
func (di *Diun) trackTime(start time.Time, prefix string) {
log.Info().Msgf("%s%s", prefix, durafmt.ParseShort(time.Since(start)).String())
}

142
internal/config/config.go Normal file
View File

@@ -0,0 +1,142 @@
package config
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/mail"
"os"
"path"
"regexp"
"github.com/crazy-max/diun/internal/model"
"github.com/imdario/mergo"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
)
// Config holds configuration details
type Config struct {
Flags model.Flags
App model.App
Db model.Db `yaml:"db,omitempty"`
Watch model.Watch `yaml:"watch,omitempty"`
Notif model.Notif `yaml:"notif,omitempty"`
RegCreds map[string]model.RegCred `yaml:"reg_creds,omitempty"`
Items []model.Item `yaml:"items,omitempty"`
}
// Load returns Configuration struct
func Load(fl model.Flags, version string) (*Config, error) {
var err error
var cfg = Config{
Flags: fl,
App: model.App{
ID: "diun",
Name: "Diun",
Desc: "Docker image update notifier",
URL: "https://github.com/crazy-max/diun",
Author: "CrazyMax",
Version: version,
},
Db: model.Db{
Path: "diun.db",
},
Watch: model.Watch{
Schedule: "0 */30 * * * *",
},
Notif: model.Notif{
Mail: model.Mail{
Enable: false,
Host: "localhost",
Port: 25,
SSL: false,
InsecureSkipVerify: false,
},
Webhook: model.Webhook{
Enable: false,
Method: "GET",
Timeout: 10,
},
},
}
if _, err = os.Lstat(fl.Cfgfile); err != nil {
return nil, fmt.Errorf("unable to open config file, %s", err)
}
bytes, err := ioutil.ReadFile(fl.Cfgfile)
if err != nil {
return nil, fmt.Errorf("unable to read config file, %s", err)
}
if err := yaml.Unmarshal(bytes, &cfg); err != nil {
return nil, fmt.Errorf("unable to decode into struct, %v", err)
}
return &cfg, nil
}
// Check verifies Config values
func (cfg *Config) Check() error {
if cfg.Flags.Docker {
cfg.Db.Path = "/data/diun.db"
}
if cfg.Db.Path == "" {
return errors.New("database path is required")
}
cfg.Db.Path = path.Clean(cfg.Db.Path)
for id, regCred := range cfg.RegCreds {
if regCred.Username == "" || regCred.Password == "" {
return fmt.Errorf("username and password required for registry credentials '%s'", id)
}
}
for key, item := range cfg.Items {
if item.RegCredID != "" {
regCred, found := cfg.RegCreds[item.RegCredID]
if !found {
return fmt.Errorf("registry credentials '%s' not found", item.RegCredID)
}
cfg.Items[key].RegCred = regCred
}
for _, includeTag := range item.IncludeTags {
if _, err := regexp.Compile(includeTag); err != nil {
return fmt.Errorf("include tag regex '%s' for '%s' image cannot compile, %v", item.Image, includeTag, err)
}
}
for _, excludeTag := range item.ExcludeTags {
if _, err := regexp.Compile(excludeTag); err != nil {
return fmt.Errorf("exclude tag regex '%s' for '%s' image cannot compile, %v", item.Image, excludeTag, err)
}
}
if err := mergo.Merge(&cfg.Items[key], model.Item{
Timeout: 5,
}); err != nil {
return err
}
}
if cfg.Notif.Mail.Enable {
if _, err := mail.ParseAddress(cfg.Notif.Mail.From); err != nil {
return fmt.Errorf("cannot parse sender mail address, %v", err)
}
if _, err := mail.ParseAddress(cfg.Notif.Mail.To); err != nil {
return fmt.Errorf("cannot parse recipient mail address, %v", err)
}
}
return nil
}
// Display logs configuration in a pretty JSON format
func (cfg *Config) Display() {
b, _ := json.MarshalIndent(cfg, "", " ")
log.Debug().Msg(string(b))
}

80
internal/db/client.go Normal file
View File

@@ -0,0 +1,80 @@
package db
import (
"encoding/json"
"fmt"
"time"
"github.com/crazy-max/diun/internal/model"
"github.com/crazy-max/diun/pkg/registry"
"github.com/rs/zerolog/log"
bolt "go.etcd.io/bbolt"
)
// Client represents an active db object
type Client struct {
*bolt.DB
cfg model.Db
}
const bucket = "analysis"
// New creates new db instance
func New(cfg model.Db) (*Client, error) {
db, err := bolt.Open(cfg.Path, 0600, &bolt.Options{
Timeout: 10 * time.Second,
})
if err != nil {
return nil, err
}
if err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
return err
}); err != nil {
return nil, err
}
if err = db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bucket))
stats := b.Stats()
log.Debug().Msgf("%d entries found in database", stats.KeyN)
return nil
}); err != nil {
return nil, fmt.Errorf("cannot count entries in database, %v", err)
}
return &Client{db, cfg}, nil
}
// Close closes db connection
func (c *Client) Close() error {
return c.DB.Close()
}
// GetAnalysis returns Docker image analysis
func (c *Client) GetAnalysis(image registry.Image) (registry.Inspect, error) {
var ana registry.Inspect
err := c.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bucket))
if entryBytes := b.Get([]byte(image.String())); entryBytes != nil {
return json.Unmarshal(entryBytes, &ana)
}
return nil
})
return ana, err
}
// PutAnalysis add Docker image analysis in db
func (c *Client) PutAnalysis(image registry.Image, analysis registry.Inspect) error {
entryBytes, _ := json.Marshal(analysis)
err := c.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(bucket))
return b.Put([]byte(image.String()), entryBytes)
})
return err
}

View File

@@ -0,0 +1,39 @@
package logging
import (
"io"
"os"
"time"
"github.com/crazy-max/diun/internal/model"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
// Configure configures logger
func Configure(fl *model.Flags, location *time.Location) {
var err error
var w io.Writer
zerolog.TimestampFunc = func() time.Time {
return time.Now().In(location)
}
if !fl.LogJson {
w = zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC1123,
}
} else {
w = os.Stdout
}
log.Logger = zerolog.New(w).With().Timestamp().Logger()
logLevel, err := zerolog.ParseLevel(fl.LogLevel)
if err != nil {
log.Fatal().Err(err).Msgf("Unknown log level")
} else {
zerolog.SetGlobalLevel(logLevel)
}
}

20
internal/model/app.go Normal file
View File

@@ -0,0 +1,20 @@
package model
// App holds application details
type App struct {
ID string
Name string
Desc string
URL string
Author string
Version string
}
const (
ImageStatusNew = ImageStatus("new")
ImageStatusUpdate = ImageStatus("update")
ImageStatusUnchange = ImageStatus("unchange")
)
// ImageStatus holds Docker image status analysis
type ImageStatus string

6
internal/model/db.go Normal file
View File

@@ -0,0 +1,6 @@
package model
// Db holds data necessary for database configuration
type Db struct {
Path string `yaml:"path,omitempty"`
}

12
internal/model/flags.go Normal file
View File

@@ -0,0 +1,12 @@
package model
// Flags holds flags from command line
type Flags struct {
Cfgfile string
Populate bool
Timezone string
LogLevel string
LogJson bool
RunOnce bool
Docker bool
}

13
internal/model/item.go Normal file
View File

@@ -0,0 +1,13 @@
package model
// Item holds item configuration for a Docker image
type Item struct {
Image string `yaml:"image,omitempty"`
RegCredID string `yaml:"reg_cred_id,omitempty"`
InsecureTLS bool `yaml:"insecure_tls,omitempty"`
WatchRepo bool `yaml:"watch_repo,omitempty"`
IncludeTags []string `yaml:"include_tags,omitempty"`
ExcludeTags []string `yaml:"exclude_tags,omitempty"`
Timeout int `yaml:"timeout,omitempty"`
RegCred RegCred `json:"-"`
}

14
internal/model/mail.go Normal file
View File

@@ -0,0 +1,14 @@
package model
// Mail holds mail notification configuration details
type Mail struct {
Enable bool `yaml:"enable,omitempty"`
Host string `yaml:"host,omitempty"`
Port int `yaml:"port,omitempty"`
SSL bool `yaml:"ssl,omitempty"`
InsecureSkipVerify bool `yaml:"insecure_skip_verify,omitempty"`
Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
From string `yaml:"from,omitempty"`
To string `yaml:"to,omitempty"`
}

16
internal/model/notif.go Normal file
View File

@@ -0,0 +1,16 @@
package model
import "github.com/crazy-max/diun/pkg/registry"
// Notif holds data necessary for notification configuration
type Notif struct {
Mail Mail `yaml:"mail,omitempty"`
Webhook Webhook `yaml:"webhook,omitempty"`
}
// NotifEntry represents a notification entry
type NotifEntry struct {
Status ImageStatus `json:"status,omitempty"`
Image registry.Image `json:"image,omitempty"`
Analysis registry.Inspect `json:"analysis,omitempty"`
}

View File

@@ -0,0 +1,7 @@
package model
// RegCred holds registry credential
type RegCred struct {
Username string `yaml:"username,omitempty"`
Password string `yaml:"password,omitempty"`
}

6
internal/model/watch.go Normal file
View File

@@ -0,0 +1,6 @@
package model
// Watch holds data necessary for watch configuration
type Watch struct {
Schedule string `yaml:"schedule,omitempty"`
}

10
internal/model/webhook.go Normal file
View File

@@ -0,0 +1,10 @@
package model
// Webhook holds webhook notification configuration details
type Webhook struct {
Enable bool `yaml:"enable,omitempty"`
Endpoint string `yaml:"endpoint,omitempty"`
Method string `yaml:"method,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
Timeout int `yaml:"timeout,omitempty"`
}

46
internal/notif/client.go Normal file
View File

@@ -0,0 +1,46 @@
package notif
import (
"github.com/crazy-max/diun/internal/model"
"github.com/crazy-max/diun/internal/notif/mail"
"github.com/crazy-max/diun/internal/notif/notifier"
"github.com/crazy-max/diun/internal/notif/webhook"
"github.com/rs/zerolog/log"
)
// Client represents an active webhook notification object
type Client struct {
cfg model.Notif
app model.App
notifiers []notifier.Notifier
}
// New creates a new notification instance
func New(config model.Notif, app model.App) (*Client, error) {
var c = &Client{
cfg: config,
app: app,
notifiers: []notifier.Notifier{},
}
// Add notifiers
if config.Mail.Enable {
c.notifiers = append(c.notifiers, mail.New(config.Mail, app))
}
if config.Webhook.Enable {
c.notifiers = append(c.notifiers, webhook.New(config.Webhook, app))
}
log.Debug().Msgf("%d notifier(s) created", len(c.notifiers))
return c, nil
}
// Send creates and sends notifications to notifiers
func (c *Client) Send(entry model.NotifEntry) {
for _, n := range c.notifiers {
log.Debug().Str("image", entry.Image.String()).Msgf("Sending %s notification...", n.Name())
if err := n.Send(entry); err != nil {
log.Error().Err(err).Str("image", entry.Image.String()).Msgf("%s notification failed", n.Name())
}
}
}

View File

@@ -0,0 +1,118 @@
package mail
import (
"bytes"
"crypto/tls"
"fmt"
"text/template"
"time"
"github.com/crazy-max/diun/internal/model"
"github.com/crazy-max/diun/internal/notif/notifier"
"github.com/go-gomail/gomail"
"github.com/matcornic/hermes/v2"
)
// Client represents an active mail notification object
type Client struct {
*notifier.Notifier
cfg model.Mail
app model.App
}
// New creates a new mail notification instance
func New(config model.Mail, app model.App) notifier.Notifier {
return notifier.Notifier{
Handler: &Client{
cfg: config,
app: app,
},
}
}
// Name returns notifier's name
func (c *Client) Name() string {
return "mail"
}
// Send creates and sends an email notification with an entry
func (c *Client) Send(entry model.NotifEntry) error {
h := hermes.Hermes{
Theme: new(Theme),
Product: hermes.Product{
Name: c.app.Name,
Link: "https://github.com/crazy-max/diun",
Logo: "https://raw.githubusercontent.com/crazy-max/diun/master/.res/diun.png",
Copyright: fmt.Sprintf("%s © %d %s %s",
c.app.Author,
time.Now().Year(),
c.app.Name,
c.app.Version),
},
}
// Subject
subject := fmt.Sprintf("Image update for %s", entry.Image.String())
if entry.Status == model.ImageStatusNew {
subject = fmt.Sprintf("New image %s has been added", entry.Image.String())
}
// Body
var emailBuf bytes.Buffer
emailTpl := template.Must(template.New("email").Parse(`
Docker 🐳 tag **{{ .Image.Domain }}/{{ .Image.Path }}:{{ .Image.Tag }}** which you subscribed to has been {{ if (eq .Status "new") }}newly added{{ else }}updated{{ end }}.
This image has been {{ if (eq .Status "new") }}created{{ else }}updated{{ end }} at <code>{{ .Analysis.Created }}</code> with digest <code>{{ .Analysis.Digest }}</code> for <code>{{ .Analysis.Os }}/{{ .Analysis.Architecture }}</code> platform.
Need help, or have questions? Go to https://github.com/crazy-max/diun and leave an issue.
`))
if err := emailTpl.Execute(&emailBuf, entry); err != nil {
return err
}
email := hermes.Email{
Body: hermes.Body{
Title: fmt.Sprintf("%s 🔔 notification", c.app.Name),
FreeMarkdown: hermes.Markdown(emailBuf.String()),
Signature: "Thanks for your support",
},
}
// Generate an HTML email with the provided contents (for modern clients)
htmlpart, err := h.GenerateHTML(email)
if err != nil {
return fmt.Errorf("hermes: %v", err)
}
// Generate the plaintext version of the e-mail (for clients that do not support xHTML)
textpart, err := h.GeneratePlainText(email)
if err != nil {
return fmt.Errorf("hermes: %v", err)
}
msg := gomail.NewMessage()
msg.SetHeader("From", fmt.Sprintf("%s <%s>", c.app.Name, c.cfg.From))
msg.SetHeader("To", c.cfg.To)
msg.SetHeader("Subject", subject)
msg.SetBody("text/plain", textpart)
msg.AddAlternative("text/html", htmlpart)
var tlsConfig *tls.Config
if c.cfg.InsecureSkipVerify {
tlsConfig = &tls.Config{
InsecureSkipVerify: c.cfg.InsecureSkipVerify,
}
}
dialer := &gomail.Dialer{
Host: c.cfg.Host,
Port: c.cfg.Port,
Username: c.cfg.Username,
Password: c.cfg.Password,
SSL: c.cfg.SSL,
TLSConfig: tlsConfig,
}
return dialer.DialAndSend(msg)
}

View File

@@ -0,0 +1,505 @@
package mail
// Theme is the theme for hermes
type Theme struct{}
// Name returns the name of the theme
func (t *Theme) Name() string {
return "diun"
}
// HTMLTemplate returns a Golang template that will generate an HTML email.
func (t *Theme) HTMLTemplate() string {
return `
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
*:not(br):not(tr):not(html) {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
body {
width: 100% !important;
height: 100%;
margin: 0;
line-height: 1.4;
background-color: #F2F4F6;
color: #74787E;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
code {
font-family: SFMono-Regular,Consolas,"Liberation Mono",Menlo,Courier,monospace;
font-size: 12px;
color: #c7254e;
padding: .2em .4em;
margin: 0;
font-size: 85%;
background-color: rgba(27,31,35,.05);
border-radius: 3px;
}
/* Layout ------------------------------ */
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
max-width: 400px;
border: 0;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
color: #2F3133;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
.email-logo {
max-height: 50px;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
padding: 0;
border-top: 1px solid #EDEFF2;
border-bottom: 1px solid #EDEFF2;
background-color: #FFF;
}
.email-body_inner {
width: 80%;
margin: 0 auto;
padding: 0;
}
.email-footer {
width: 80%;
margin: 0 auto;
padding: 0;
text-align: center;
}
.email-footer p {
color: #AEAEAE;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
text-align: center;
}
.body-dictionary {
width: 100%;
overflow: hidden;
margin: 20px auto 10px;
padding: 0;
}
.body-dictionary dd {
margin: 0 0 10px 0;
}
.body-dictionary dt {
clear: both;
color: #000;
font-weight: bold;
}
.body-dictionary dd {
margin-left: 0;
margin-bottom: 10px;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EDEFF2;
table-layout: fixed;
}
.body-sub a {
word-break: break-all;
}
.content-cell {
padding: 35px;
}
.align-right {
text-align: right;
}
/* Type ------------------------------ */
h1 {
margin-top: 0;
color: #2F3133;
font-size: 19px;
font-weight: bold;
}
h2 {
margin-top: 0;
color: #2F3133;
font-size: 16px;
font-weight: bold;
}
h3 {
margin-top: 0;
color: #2F3133;
font-size: 14px;
font-weight: bold;
}
blockquote {
margin: 1.7rem 0;
padding-left: 0.85rem;
border-left: 10px solid #F0F2F4;
}
blockquote p {
font-size: 1.1rem;
color: #999;
}
blockquote cite {
display: block;
text-align: right;
color: #666;
font-size: 1.2rem;
}
cite {
display: block;
font-size: 0.925rem;
}
cite:before {
content: "\2014 \0020";
}
p {
margin-top: 0;
color: #74787E;
font-size: 16px;
line-height: 1.5em;
}
p.sub {
font-size: 12px;
}
p.center {
text-align: center;
}
table {
width: 100%;
}
th {
padding: 0px 5px;
padding-bottom: 8px;
border-bottom: 1px solid #EDEFF2;
}
th p {
margin: 0;
color: #9BA2AB;
font-size: 12px;
}
td {
padding: 10px 5px;
color: #74787E;
font-size: 15px;
line-height: 18px;
}
.content {
align: center;
padding: 0;
}
/* Data table ------------------------------ */
.data-wrapper {
width: 100%;
margin: 0;
padding: 35px 0;
}
.data-table {
width: 100%;
margin: 0;
}
.data-table th {
text-align: left;
padding: 0px 5px;
padding-bottom: 8px;
border-bottom: 1px solid #EDEFF2;
}
.data-table th p {
margin: 0;
color: #9BA2AB;
font-size: 12px;
}
.data-table td {
padding: 10px 5px;
color: #74787E;
font-size: 15px;
line-height: 18px;
}
/* Buttons ------------------------------ */
.button {
display: inline-block;
width: 200px;
background-color: #3869D4;
border-radius: 3px;
color: #ffffff;
font-size: 15px;
line-height: 45px;
text-align: center;
text-decoration: none;
-webkit-text-size-adjust: none;
mso-hide: all;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
</style>
</head>
<body dir="{{.Hermes.TextDirection}}">
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="content">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0">
<!-- Logo -->
<tr>
<td class="email-masthead">
<a class="email-masthead_name" href="{{.Hermes.Product.Link}}" target="_blank">
{{ if .Hermes.Product.Logo }}
<img src="{{.Hermes.Product.Logo | url }}" class="email-logo" />
{{ else }}
{{ .Hermes.Product.Name }}
{{ end }}
</a>
</td>
</tr>
<!-- Email Body -->
<tr>
<td class="email-body" width="100%">
<table class="email-body_inner" align="center" width="570" cellpadding="0" cellspacing="0">
<!-- Body content -->
<tr>
<td class="content-cell">
<h1>{{if .Email.Body.Title }}{{ .Email.Body.Title }}{{ else }}{{ .Email.Body.Greeting }} {{ .Email.Body.Name }},{{ end }}</h1>
{{ with .Email.Body.Intros }}
{{ if gt (len .) 0 }}
{{ range $line := . }}
<p>{{ $line }}</p>
{{ end }}
{{ end }}
{{ end }}
{{ if (ne .Email.Body.FreeMarkdown "") }}
{{ .Email.Body.FreeMarkdown.ToHTML }}
{{ else }}
{{ with .Email.Body.Dictionary }}
{{ if gt (len .) 0 }}
<dl class="body-dictionary">
{{ range $entry := . }}
<dt>{{ $entry.Key }}:</dt>
<dd>{{ $entry.Value }}</dd>
{{ end }}
</dl>
{{ end }}
{{ end }}
<!-- Table -->
{{ with .Email.Body.Table }}
{{ $data := .Data }}
{{ $columns := .Columns }}
{{ if gt (len $data) 0 }}
<table class="data-wrapper" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td colspan="2">
<table class="data-table" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ $col := index $data 0 }}
{{ range $entry := $col }}
<th
{{ with $columns }}
{{ $width := index .CustomWidth $entry.Key }}
{{ with $width }}
width="{{ . }}"
{{ end }}
{{ $align := index .CustomAlignment $entry.Key }}
{{ with $align }}
style="text-align:{{ . }}"
{{ end }}
{{ end }}
>
<p>{{ $entry.Key }}</p>
</th>
{{ end }}
</tr>
{{ range $row := $data }}
<tr>
{{ range $cell := $row }}
<td
{{ with $columns }}
{{ $align := index .CustomAlignment $cell.Key }}
{{ with $align }}
style="text-align:{{ . }}"
{{ end }}
{{ end }}
>
{{ $cell.Value }}
</td>
{{ end }}
</tr>
{{ end }}
</table>
</td>
</tr>
</table>
{{ end }}
{{ end }}
<!-- Action -->
{{ with .Email.Body.Actions }}
{{ if gt (len .) 0 }}
{{ range $action := . }}
<p>{{ $action.Instructions }}</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<div>
<a href="{{ $action.Button.Link }}" class="button" style="background-color: {{ $action.Button.Color }}; color: {{ $action.Button.TextColor }};" target="_blank">
{{ $action.Button.Text }}
</a>
</div>
</td>
</tr>
</table>
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ with .Email.Body.Outros }}
{{ if gt (len .) 0 }}
{{ range $line := . }}
<p>{{ $line }}</p>
{{ end }}
{{ end }}
{{ end }}
<p>
{{.Email.Body.Signature}},
<br />
{{.Hermes.Product.Name}}
</p>
{{ if (eq .Email.Body.FreeMarkdown "") }}
{{ with .Email.Body.Actions }}
<table class="body-sub">
<tbody>
{{ range $action := . }}
<tr>
<td>
<p class="sub">{{$.Hermes.Product.TroubleText | replace "{ACTION}" $action.Button.Text}}</p>
<p class="sub"><a href="{{ $action.Button.Link }}">{{ $action.Button.Link }}</a></p>
</td>
</tr>
{{ end }}
</tbody>
</table>
{{ end }}
{{ end }}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0">
<tr>
<td class="content-cell">
<p class="sub center">
{{.Hermes.Product.Copyright}}
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`
}
// PlainTextTemplate returns a Golang template that will generate an plain text email.
func (t *Theme) PlainTextTemplate() string {
return `<h2>{{if .Email.Body.Title }}{{ .Email.Body.Title }}{{ else }}{{ .Email.Body.Greeting }} {{ .Email.Body.Name }},{{ end }}</h2>
{{ with .Email.Body.Intros }}
{{ range $line := . }}
<p>{{ $line }}</p>
{{ end }}
{{ end }}
{{ if (ne .Email.Body.FreeMarkdown "") }}
{{ .Email.Body.FreeMarkdown.ToHTML }}
{{ else }}
{{ with .Email.Body.Dictionary }}
<ul>
{{ range $entry := . }}
<li>{{ $entry.Key }}: {{ $entry.Value }}</li>
{{ end }}
</ul>
{{ end }}
{{ with .Email.Body.Table }}
{{ $data := .Data }}
{{ $columns := .Columns }}
{{ if gt (len $data) 0 }}
<table class="data-table" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ $col := index $data 0 }}
{{ range $entry := $col }}
<th>{{ $entry.Key }} </th>
{{ end }}
</tr>
{{ range $row := $data }}
<tr>
{{ range $cell := $row }}
<td>
{{ $cell.Value }}
</td>
{{ end }}
</tr>
{{ end }}
</table>
{{ end }}
{{ end }}
{{ with .Email.Body.Actions }}
{{ range $action := . }}
<p>{{ $action.Instructions }} {{ $action.Button.Link }}</p>
{{ end }}
{{ end }}
{{ end }}
{{ with .Email.Body.Outros }}
{{ range $line := . }}
<p>{{ $line }}<p>
{{ end }}
{{ end }}
<p>{{.Email.Body.Signature}},<br>{{.Hermes.Product.Name}} - {{.Hermes.Product.Link}}</p>
<p>{{.Hermes.Product.Copyright}}</p>
`
}

View File

@@ -0,0 +1,16 @@
package notifier
import (
"github.com/crazy-max/diun/internal/model"
)
// Handler is a notifier interface
type Handler interface {
Name() string
Send(entry model.NotifEntry) error
}
// Notifier represents an active notifier object
type Notifier struct {
Handler
}

View File

@@ -0,0 +1,81 @@
package webhook
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/crazy-max/diun/internal/model"
"github.com/crazy-max/diun/internal/notif/notifier"
"github.com/opencontainers/go-digest"
)
// Client represents an active webhook notification object
type Client struct {
*notifier.Notifier
cfg model.Webhook
app model.App
}
// New creates a new webhook notification instance
func New(config model.Webhook, app model.App) notifier.Notifier {
return notifier.Notifier{
Handler: &Client{
cfg: config,
app: app,
},
}
}
// Name returns notifier's name
func (c *Client) Name() string {
return "webhook"
}
// Send creates and sends a webhook notification with an entry
func (c *Client) Send(entry model.NotifEntry) error {
hc := http.Client{
Timeout: time.Duration(c.cfg.Timeout) * time.Second,
}
body, err := json.Marshal(struct {
Version string `json:"diun_version,omitempty"`
Status string `json:"status,omitempty"`
Image string `json:"image,omitempty"`
MIMEType string `json:"mime_type,omitempty"`
Digest digest.Digest `json:"digest,omitempty"`
Date *time.Time `json:"date,omitempty"`
Architecture string `json:"architecture,omitempty"`
Os string `json:"os,omitempty"`
}{
Version: c.app.Version,
Status: string(entry.Status),
Image: entry.Image.String(),
MIMEType: entry.Analysis.MIMEType,
Digest: entry.Analysis.Digest,
Date: entry.Analysis.Created,
Architecture: entry.Analysis.Architecture,
Os: entry.Analysis.Os,
})
if err != nil {
return err
}
req, err := http.NewRequest(c.cfg.Method, c.cfg.Endpoint, bytes.NewBuffer([]byte(body)))
if err != nil {
return err
}
if len(c.cfg.Headers) > 0 {
for key, value := range c.cfg.Headers {
req.Header.Add(key, value)
}
}
req.Header.Set("User-Agent", fmt.Sprintf("%s %s", c.app.Name, c.app.Version))
_, err = hc.Do(req)
return err
}

15
internal/utl/utl.go Normal file
View File

@@ -0,0 +1,15 @@
package utl
import (
"regexp"
)
// MatchString reports whether a string s
// contains any match of a regular expression.
func MatchString(exp string, s string) bool {
re, err := regexp.Compile(exp)
if err != nil {
return false
}
return re.MatchString(s)
}

71
pkg/registry/client.go Normal file
View File

@@ -0,0 +1,71 @@
package registry
import (
"context"
"fmt"
"strings"
"time"
"github.com/containers/image/docker"
"github.com/containers/image/types"
)
// Client represents an active registry object
type Client struct{}
type Options struct {
Image Image
Username string
Password string
InsecureTLS bool
Timeout time.Duration
}
// New creates new registry instance
func New() (*Client, error) {
return &Client{}, nil
}
func (c *Client) timeoutContext(timeout time.Duration) (context.Context, context.CancelFunc) {
ctx := context.Background()
var cancel context.CancelFunc = func() {}
if timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, timeout)
} else {
ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
}
return ctx, cancel
}
func (c *Client) newImage(ctx context.Context, opts *Options) (types.ImageCloser, *types.SystemContext, error) {
image := opts.Image.String()
if !strings.HasPrefix(opts.Image.String(), "//") {
image = fmt.Sprintf("//%s", opts.Image.String())
}
ref, err := docker.ParseReference(image)
if err != nil {
return nil, nil, fmt.Errorf("invalid image name %s: %v", image, err)
}
auth := &types.DockerAuthConfig{}
if opts.Username != "" {
auth = &types.DockerAuthConfig{
Username: opts.Username,
Password: opts.Password,
}
}
sys := &types.SystemContext{
DockerAuthConfig: auth,
DockerDaemonInsecureSkipTLSVerify: opts.InsecureTLS,
DockerInsecureSkipTLSVerify: types.NewOptionalBool(opts.InsecureTLS),
}
img, err := ref.NewImage(ctx, sys)
if err != nil {
return nil, nil, err
}
return img, sys, nil
}

69
pkg/registry/image.go Normal file
View File

@@ -0,0 +1,69 @@
// Source: https://github.com/genuinetools/reg/blob/master/registry/image.go
package registry
import (
"fmt"
"github.com/containers/image/docker/reference"
digest "github.com/opencontainers/go-digest"
)
// Image holds information about an image.
type Image struct {
Domain string
Path string
Tag string
Digest digest.Digest
named reference.Named
}
// String returns the string representation of an image.
func (i Image) String() string {
return i.named.String()
}
// Reference returns either the digest if it is non-empty or the tag for the image.
func (i Image) Reference() string {
if len(i.Digest.String()) > 1 {
return i.Digest.String()
}
return i.Tag
}
// WithDigest sets the digest for an image.
func (i *Image) WithDigest(digest digest.Digest) (err error) {
i.Digest = digest
i.named, err = reference.WithDigest(i.named, digest)
return err
}
// ParseImage returns an Image struct with all the values filled in for a given image.
func ParseImage(image string) (Image, error) {
// Parse the image name and tag.
named, err := reference.ParseNormalizedNamed(image)
if err != nil {
return Image{}, fmt.Errorf("parsing image %q failed: %v", image, err)
}
// Add the latest lag if they did not provide one.
named = reference.TagNameOnly(named)
i := Image{
named: named,
Domain: reference.Domain(named),
Path: reference.Path(named),
}
// Add the tag if there was one.
if tagged, ok := named.(reference.Tagged); ok {
i.Tag = tagged.Tag()
}
// Add the digest if there was one.
if canonical, ok := named.(reference.Canonical); ok {
i.Digest = canonical.Digest()
}
return i, nil
}

66
pkg/registry/inspect.go Normal file
View File

@@ -0,0 +1,66 @@
package registry
import (
"time"
"github.com/containers/image/manifest"
"github.com/opencontainers/go-digest"
)
type Inspect struct {
Name string
Tag string
MIMEType string
Digest digest.Digest
Created *time.Time
DockerVersion string
Labels map[string]string
Architecture string
Os string
Layers []string
}
// Inspect inspects a Docker image
func (c *Client) Inspect(opts *Options) (Inspect, error) {
ctx, cancel := c.timeoutContext(opts.Timeout)
defer cancel()
img, _, err := c.newImage(ctx, opts)
if err != nil {
return Inspect{}, err
}
defer img.Close()
rawManifest, _, err := img.Manifest(ctx)
if err != nil {
return Inspect{}, err
}
imgInspect, err := img.Inspect(ctx)
if err != nil {
return Inspect{}, err
}
imgDigest, err := manifest.Digest(rawManifest)
if err != nil {
return Inspect{}, err
}
imgTag := imgInspect.Tag
if imgTag == "" {
imgTag = opts.Image.Tag
}
return Inspect{
Name: img.Reference().DockerReference().Name(),
Tag: imgTag,
MIMEType: manifest.GuessMIMEType(rawManifest),
Digest: imgDigest,
Created: imgInspect.Created,
DockerVersion: imgInspect.DockerVersion,
Labels: imgInspect.Labels,
Architecture: imgInspect.Architecture,
Os: imgInspect.Os,
Layers: imgInspect.Layers,
}, nil
}

26
pkg/registry/tags.go Normal file
View File

@@ -0,0 +1,26 @@
package registry
import (
"github.com/containers/image/docker"
)
type Tags []string
// Tags returns tags of a Docker repository
func (c *Client) Tags(opts *Options) (Tags, error) {
ctx, cancel := c.timeoutContext(opts.Timeout)
defer cancel()
img, sys, err := c.newImage(ctx, opts)
if err != nil {
return nil, err
}
defer img.Close()
tags, err := docker.GetRepositoryTags(ctx, sys, img.Reference())
if err != nil {
return nil, err
}
return Tags(tags), err
}