mirror of
https://github.com/crazy-max/diun.git
synced 2025-12-21 21:33:22 +01:00
Initial commit
This commit is contained in:
17
.dockerignore
Normal file
17
.dockerignore
Normal 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
18
.editorconfig
Normal 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
2
.github/FUNDING.yml
vendored
Normal 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
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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
29
.github/SUPPORT.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Support [](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
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/.idea
|
||||||
|
/*.iml
|
||||||
|
|
||||||
|
/.dev
|
||||||
|
/bin
|
||||||
|
/dist
|
||||||
41
.goreleaser.yml
Normal file
41
.goreleaser.yml
Normal 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'
|
||||||
14
.res/compose/docker-compose.yml
Normal file
14
.res/compose/docker-compose.yml
Normal 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
BIN
.res/diun.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
.res/paypal-donate.png
Normal file
BIN
.res/paypal-donate.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
48
.travis.yml
Normal file
48
.travis.yml
Normal 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
5
CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 0.1.0 (2019/06/04)
|
||||||
|
|
||||||
|
* Initial version
|
||||||
46
Dockerfile
Normal file
46
Dockerfile
Normal 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
21
LICENSE
Normal 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
207
README.md
Normal 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:!
|
||||||
|
|
||||||
|
[](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
100
build.sh
Normal 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
94
cmd/main.go
Normal 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
40
go.mod
Normal 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
171
go.sum
Normal 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
203
internal/app/diun.go
Normal 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 := ®istry.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 := ®istry.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
142
internal/config/config.go
Normal 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
80
internal/db/client.go
Normal 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
|
||||||
|
}
|
||||||
39
internal/logging/logger.go
Normal file
39
internal/logging/logger.go
Normal 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
20
internal/model/app.go
Normal 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
6
internal/model/db.go
Normal 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
12
internal/model/flags.go
Normal 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
13
internal/model/item.go
Normal 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
14
internal/model/mail.go
Normal 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
16
internal/model/notif.go
Normal 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"`
|
||||||
|
}
|
||||||
7
internal/model/regcred.go
Normal file
7
internal/model/regcred.go
Normal 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
6
internal/model/watch.go
Normal 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
10
internal/model/webhook.go
Normal 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
46
internal/notif/client.go
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/notif/mail/client.go
Normal file
118
internal/notif/mail/client.go
Normal 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)
|
||||||
|
}
|
||||||
505
internal/notif/mail/theme.go
Normal file
505
internal/notif/mail/theme.go
Normal 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>
|
||||||
|
`
|
||||||
|
}
|
||||||
16
internal/notif/notifier/notifier.go
Normal file
16
internal/notif/notifier/notifier.go
Normal 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
|
||||||
|
}
|
||||||
81
internal/notif/webhook/client.go
Normal file
81
internal/notif/webhook/client.go
Normal 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
15
internal/utl/utl.go
Normal 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
71
pkg/registry/client.go
Normal 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
69
pkg/registry/image.go
Normal 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
66
pkg/registry/inspect.go
Normal 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
26
pkg/registry/tags.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user