mirror of
https://github.com/Quiq/docker-registry-ui.git
synced 2025-09-29 22:38:00 +00:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e1cd96ef12 | ||
|
65c9978bd1 | ||
|
b6398fa33c | ||
|
67d82c7d59 | ||
|
dc7b2e42fc | ||
|
41e74f70a2 | ||
|
905e760956 |
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,5 +1,20 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 0.9.0 (2020-02-19)
|
||||||
|
|
||||||
|
* Upgrade Go version to 1.13.7, alpine to 3.11 and other dependencies.
|
||||||
|
* Support Manifest List v2. This enables the proper display of multi-arch images,
|
||||||
|
such as those generated by Docker BuildX or manually (thanks to Christoph Honal @StarGate01).
|
||||||
|
So now we support the following formats: Manifest v2 schema 1, Manifest v2 schema 2, Manifest List v2 schema 2
|
||||||
|
and all their confusing combinations.
|
||||||
|
* Amend representation of the tag info page.
|
||||||
|
* Change logging library, add "-log-level" argument and put most of the logging into DEBUG mode.
|
||||||
|
* You can define timezone when running container by adding `TZ` env var, e.g. "-e TZ=America/Los_Angeles"
|
||||||
|
(thanks to @gminog).
|
||||||
|
* Fix initial ownership of /opt/data dir in Dockerfile.
|
||||||
|
* Hide repositories with 0 tags count.
|
||||||
|
* Compatibility fix with docker_auth v1.5.0.
|
||||||
|
|
||||||
### 0.8.2 (2019-07-30)
|
### 0.8.2 (2019-07-30)
|
||||||
|
|
||||||
* Add event_deletion_enabled option to the config, useful for master-master/cluster setups.
|
* Add event_deletion_enabled option to the config, useful for master-master/cluster setups.
|
||||||
|
14
Dockerfile
14
Dockerfile
@@ -1,12 +1,9 @@
|
|||||||
FROM golang:1.12.7-alpine3.9 as builder
|
FROM golang:1.13.7-alpine3.11 as builder
|
||||||
|
|
||||||
ENV GOPATH /opt
|
|
||||||
ENV GO111MODULE on
|
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add ca-certificates git bash gcc musl-dev
|
apk add ca-certificates git bash gcc musl-dev
|
||||||
|
|
||||||
WORKDIR /opt/src/github.com/quiq/docker-registry-ui
|
WORKDIR /opt/src
|
||||||
ADD events events
|
ADD events events
|
||||||
ADD registry registry
|
ADD registry registry
|
||||||
ADD *.go go.mod go.sum ./
|
ADD *.go go.mod go.sum ./
|
||||||
@@ -15,11 +12,12 @@ RUN go test -v ./registry && \
|
|||||||
go build -o /opt/docker-registry-ui *.go
|
go build -o /opt/docker-registry-ui *.go
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.9
|
FROM alpine:3.11
|
||||||
|
|
||||||
WORKDIR /opt
|
WORKDIR /opt
|
||||||
RUN apk add --no-cache ca-certificates && \
|
RUN apk add --no-cache ca-certificates tzdata && \
|
||||||
mkdir /opt/data
|
mkdir /opt/data && \
|
||||||
|
chown nobody /opt/data
|
||||||
|
|
||||||
ADD templates /opt/templates
|
ADD templates /opt/templates
|
||||||
ADD static /opt/static
|
ADD static /opt/static
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
Copyright 2017-2018 Quiq Inc.
|
Copyright 2017-2020 Quiq Inc.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
21
README.md
21
README.md
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
* Web UI for Docker Registry 2.6+
|
* Web UI for Docker Registry 2.6+
|
||||||
* Browse repositories and tags
|
* Browse repositories and tags
|
||||||
* Display Docker image details by layers including both manifests v1 and v2
|
* Display image details by layers
|
||||||
|
* Support Manifest v2 schema 1, Manifest v2 schema 2, Manifest List v2 schema 2 and their confusing combinations
|
||||||
* Fast and small, written on Go
|
* Fast and small, written on Go
|
||||||
* Automatically discover an authentication method (basic auth, token service etc.)
|
* Automatically discover an authentication method (basic auth, token service etc.)
|
||||||
* Caching the list of repositories, tag counts and refreshing in background
|
* Caching the list of repositories, tag counts and refreshing in background
|
||||||
@@ -36,8 +37,14 @@ To preserve sqlite db file with event notifications data, add to the command:
|
|||||||
|
|
||||||
-v /local/data:/opt/data
|
-v /local/data:/opt/data
|
||||||
|
|
||||||
|
Ensure /local/data is owner by nobody (alpine user id is 65534).
|
||||||
|
|
||||||
You can also run the container with `--read-only` option, however when using using event listener functionality
|
You can also run the container with `--read-only` option, however when using using event listener functionality
|
||||||
you need to ensure the sqlite db can be written, i.e. mount a folder as listed above.
|
you need to ensure the sqlite db can be written, i.e. mount a folder as listed above (rw mode).
|
||||||
|
|
||||||
|
To run with a custom TZ:
|
||||||
|
|
||||||
|
-e TZ=America/Los_Angeles
|
||||||
|
|
||||||
## Configure event listener on Docker Registry
|
## Configure event listener on Docker Registry
|
||||||
|
|
||||||
@@ -104,6 +111,16 @@ Note, the cron schedule format includes seconds! See https://godoc.org/github.co
|
|||||||
|
|
||||||
To increase http request verbosity, run container with `-e GOREQUEST_DEBUG=1`.
|
To increase http request verbosity, run container with `-e GOREQUEST_DEBUG=1`.
|
||||||
|
|
||||||
|
### About Docker image formats...
|
||||||
|
|
||||||
|
Docker image formats and their confusing combinations as supported by this UI:
|
||||||
|
|
||||||
|
* Manifest v2 schema 1 only: older format, e.g. created with Docker 1.9.
|
||||||
|
* Manifest v2 schema 1 + Manifest v2 schema 2: current format of a single image, the image history are coming from schema 1, should be referenced by repo:tag name.
|
||||||
|
* Manifest v2 schema 1 + Manifest List v2 schema 2: multi-arch image format containing digests of sub-images, the image history are coming from schema 1 (no idea from what sub-image it was picked up when created), should be referenced by repo:tag name.
|
||||||
|
* Manifest v2 schema 2: current image format referenced by its digest sha256, no image history.
|
||||||
|
* Manifest List v2 schema 2: multi-arch image format referenced by its digest sha256, no image history.
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||

|

|
||||||
|
@@ -8,8 +8,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hhkbp2/go-logging"
|
|
||||||
"github.com/quiq/docker-registry-ui/registry"
|
"github.com/quiq/docker-registry-ui/registry"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
// 🐒 patching of "database/sql".
|
// 🐒 patching of "database/sql".
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
@@ -37,7 +37,7 @@ type EventListener struct {
|
|||||||
databaseLocation string
|
databaseLocation string
|
||||||
retention int
|
retention int
|
||||||
eventDeletion bool
|
eventDeletion bool
|
||||||
logger logging.Logger
|
logger *logrus.Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
type eventData struct {
|
type eventData struct {
|
||||||
|
42
go.mod
42
go.mod
@@ -1,32 +1,20 @@
|
|||||||
module github.com/quiq/docker-registry-ui
|
module github.com/quiq/docker-registry-ui
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a // indirect
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||||
github.com/CloudyKit/jet v2.1.2+incompatible
|
github.com/CloudyKit/jet v2.1.2+incompatible
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 // indirect
|
||||||
github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a // indirect
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
github.com/go-sql-driver/mysql v1.4.1
|
github.com/labstack/echo/v4 v4.1.14
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||||
github.com/hhkbp2/go-logging v0.0.0-20171106042747-377ba05d9897
|
github.com/parnurzeal/gorequest v0.2.16
|
||||||
github.com/hhkbp2/go-strftime v0.0.0-20150709091403-d82166ec6782 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/hhkbp2/testify v0.0.0-20150512090439-112845ebc045 // indirect
|
github.com/robfig/cron v1.2.0
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/sirupsen/logrus v1.4.2
|
||||||
github.com/labstack/echo v3.3.10+incompatible
|
github.com/smartystreets/goconvey v1.6.4
|
||||||
github.com/labstack/gommon v0.2.8 // indirect
|
github.com/tidwall/gjson v1.5.0
|
||||||
github.com/mattn/go-colorable v0.1.0 // indirect
|
gopkg.in/yaml.v2 v2.2.8
|
||||||
github.com/mattn/go-isatty v0.0.4 // indirect
|
moul.io/http2curl v1.0.0 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.10.0
|
|
||||||
github.com/moul/http2curl v1.0.0 // indirect
|
|
||||||
github.com/parnurzeal/gorequest v0.2.15
|
|
||||||
github.com/pkg/errors v0.0.0-20180311214515-816c9085562c // indirect
|
|
||||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945
|
|
||||||
github.com/stretchr/testify v1.3.0 // indirect
|
|
||||||
github.com/tidwall/gjson v1.1.3
|
|
||||||
github.com/tidwall/match v1.0.1 // indirect
|
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
|
|
||||||
google.golang.org/appengine v1.3.0 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.2.2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
119
go.sum
119
go.sum
@@ -1,81 +1,98 @@
|
|||||||
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a h1:3SgJcK9l5uPdBC/X17wanyJAMxM33+4ZhEIV96MIH8U=
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
|
||||||
github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw=
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||||
github.com/CloudyKit/jet v2.1.2+incompatible h1:ybZoYzMBdoijK6I+Ke3vg9GZsmlKo/ZhKdNMWz0P26c=
|
github.com/CloudyKit/jet v2.1.2+incompatible h1:ybZoYzMBdoijK6I+Ke3vg9GZsmlKo/ZhKdNMWz0P26c=
|
||||||
github.com/CloudyKit/jet v2.1.2+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
|
github.com/CloudyKit/jet v2.1.2+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w=
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||||
github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a h1:A4wNiqeKqU56ZhtnzJCTyPZ1+cyu8jKtIchQ3TtxHgw=
|
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8=
|
||||||
github.com/elazarl/goproxy v0.0.0-20181111060418-2ce16c963a8a/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||||
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
|
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
|
||||||
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
|
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
|
||||||
github.com/hhkbp2/go-logging v0.0.0-20171106042747-377ba05d9897 h1:0vxLTAKJQ8n7revuQ11xssUZbuyGwMuDGMRdaxrviuM=
|
|
||||||
github.com/hhkbp2/go-logging v0.0.0-20171106042747-377ba05d9897/go.mod h1:zAp/KbVJna4DHUdeSPYGsRNn9c62x569NIr9ssBuZ/I=
|
|
||||||
github.com/hhkbp2/go-strftime v0.0.0-20150709091403-d82166ec6782 h1:Evl9i7wBY3bjJ3NqHs0ldhnKOdQL4Kaum9ve1JAmiCE=
|
|
||||||
github.com/hhkbp2/go-strftime v0.0.0-20150709091403-d82166ec6782/go.mod h1:x8/IOQ5qQ4DKfiTmD9wBhQ40edg5wh7gMRwdLg07mMw=
|
|
||||||
github.com/hhkbp2/testify v0.0.0-20150512090439-112845ebc045 h1:MmQwR3zANTXzs2yZexVBDY6qcH2vJXOl/2dZFkWVM7w=
|
|
||||||
github.com/hhkbp2/testify v0.0.0-20150512090439-112845ebc045/go.mod h1:8DUHF4igllRoOCbQKJsylsDqROcRtPTdb+SQUfjCYLo=
|
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/labstack/echo/v4 v4.1.14 h1:h8XP66UfB3tUm+L3QPw7tmwAu3pJaA/nyfHPCcz46ic=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/labstack/echo/v4 v4.1.14/go.mod h1:Q5KZ1vD3V5FEzjM79hjwVrC3ABr7F5IdM23bXQMRDGg=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
||||||
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
|
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||||
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.0 h1:v2XXALHHh6zHfYTJ+cSkwtyffnaOyR1MXaA91mTrb8o=
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
github.com/mattn/go-colorable v0.1.0/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
|
github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ=
|
||||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
|
||||||
github.com/parnurzeal/gorequest v0.2.15 h1:oPjDCsF5IkD4gUk6vIgsxYNaSgvAnIh1EJeROn3HdJU=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.0.0-20180311214515-816c9085562c h1:F5RoIh7F9wB47PvXvpP1+Ihq1TkyC8iRdvwfKkESEZQ=
|
|
||||||
github.com/pkg/errors v0.0.0-20180311214515-816c9085562c/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||||
|
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
|
||||||
|
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI=
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/tidwall/gjson v1.1.3 h1:u4mspaByxY+Qk4U1QYYVzGFI8qxN/3jtEV0ZDb2vRic=
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
github.com/tidwall/gjson v1.1.3/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/tidwall/gjson v1.5.0 h1:QCssIUI7J0RStkzIcI4A7O6P8rDA5wi5IPf70uqKSxg=
|
||||||
|
github.com/tidwall/gjson v1.5.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
|
||||||
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
||||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||||
|
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||||
|
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
|
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
|
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
|
||||||
|
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
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/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc=
|
||||||
|
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||||
|
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8 h1:JA8d3MPx/IToSyXZG/RhwYEtfrKO1Fxrqe8KrkiLXKM=
|
||||||
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384 h1:TFlARGu6Czu1z7q93HTxcP1P+/ZFC/IKythI5RzrnRg=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
google.golang.org/appengine v1.3.0 h1:FBSsiFRMz3LBeXIomRnVzrQwSDj4ibvcRexLG0LZGQk=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
|
||||||
|
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
|
||||||
|
88
main.go
88
main.go
@@ -10,11 +10,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/CloudyKit/jet"
|
"github.com/CloudyKit/jet"
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
"github.com/quiq/docker-registry-ui/events"
|
"github.com/quiq/docker-registry-ui/events"
|
||||||
"github.com/quiq/docker-registry-ui/registry"
|
"github.com/quiq/docker-registry-ui/registry"
|
||||||
"github.com/robfig/cron"
|
"github.com/robfig/cron"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
@@ -53,16 +54,23 @@ type apiClient struct {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var (
|
var (
|
||||||
a apiClient
|
a apiClient
|
||||||
configFile string
|
|
||||||
purgeTags bool
|
configFile, loggingLevel string
|
||||||
purgeDryRun bool
|
purgeTags, purgeDryRun bool
|
||||||
)
|
)
|
||||||
flag.StringVar(&configFile, "config-file", "config.yml", "path to the config file")
|
flag.StringVar(&configFile, "config-file", "config.yml", "path to the config file")
|
||||||
|
flag.StringVar(&loggingLevel, "log-level", "info", "logging level")
|
||||||
flag.BoolVar(&purgeTags, "purge-tags", false, "purge old tags instead of running a web server")
|
flag.BoolVar(&purgeTags, "purge-tags", false, "purge old tags instead of running a web server")
|
||||||
flag.BoolVar(&purgeDryRun, "dry-run", false, "dry-run for purging task, does not delete anything")
|
flag.BoolVar(&purgeDryRun, "dry-run", false, "dry-run for purging task, does not delete anything")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
if loggingLevel != "info" {
|
||||||
|
if level, err := logrus.ParseLevel(loggingLevel); err == nil {
|
||||||
|
logrus.SetLevel(level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read config file.
|
// Read config file.
|
||||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -209,11 +217,35 @@ func (a *apiClient) viewTagInfo(c echo.Context) error {
|
|||||||
repoPath = fmt.Sprintf("%s/%s", namespace, repo)
|
repoPath = fmt.Sprintf("%s/%s", namespace, repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve full image info from various versions of manifests
|
||||||
sha256, infoV1, infoV2 := a.client.TagInfo(repoPath, tag, false)
|
sha256, infoV1, infoV2 := a.client.TagInfo(repoPath, tag, false)
|
||||||
if infoV1 == "" || infoV2 == "" {
|
sha256list, manifests := a.client.ManifestList(repoPath, tag)
|
||||||
|
if (infoV1 == "" || infoV2 == "") && len(manifests) == 0 {
|
||||||
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo))
|
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
created := gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").String()
|
||||||
|
isDigest := strings.HasPrefix(tag, "sha256:")
|
||||||
|
if len(manifests) > 0 {
|
||||||
|
sha256 = sha256list
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather layers v2
|
||||||
|
var layersV2 []map[string]gjson.Result
|
||||||
|
for _, s := range gjson.Get(infoV2, "layers").Array() {
|
||||||
|
layersV2 = append(layersV2, s.Map())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather layers v1
|
||||||
|
var layersV1 []map[string]interface{}
|
||||||
|
for _, s := range gjson.Get(infoV1, "history.#.v1Compatibility").Array() {
|
||||||
|
m, _ := gjson.Parse(s.String()).Value().(map[string]interface{})
|
||||||
|
// Sort key in the map to show the ordered on UI.
|
||||||
|
m["ordered_keys"] = registry.SortedMapKeys(m)
|
||||||
|
layersV1 = append(layersV1, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count image size
|
||||||
var imageSize int64
|
var imageSize int64
|
||||||
if gjson.Get(infoV2, "layers").Exists() {
|
if gjson.Get(infoV2, "layers").Exists() {
|
||||||
for _, s := range gjson.Get(infoV2, "layers.#.size").Array() {
|
for _, s := range gjson.Get(infoV2, "layers.#.size").Array() {
|
||||||
@@ -225,35 +257,45 @@ func (a *apiClient) viewTagInfo(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var layersV2 []map[string]gjson.Result
|
// Count layers
|
||||||
for _, s := range gjson.Get(infoV2, "layers").Array() {
|
|
||||||
layersV2 = append(layersV2, s.Map())
|
|
||||||
}
|
|
||||||
|
|
||||||
var layersV1 []map[string]interface{}
|
|
||||||
for _, s := range gjson.Get(infoV1, "history.#.v1Compatibility").Array() {
|
|
||||||
m, _ := gjson.Parse(s.String()).Value().(map[string]interface{})
|
|
||||||
// Sort key in the map to show the ordered on UI.
|
|
||||||
m["ordered_keys"] = registry.SortedMapKeys(m)
|
|
||||||
layersV1 = append(layersV1, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
layersCount := len(layersV2)
|
layersCount := len(layersV2)
|
||||||
if layersCount == 0 {
|
if layersCount == 0 {
|
||||||
layersCount = len(gjson.Get(infoV1, "fsLayers").Array())
|
layersCount = len(gjson.Get(infoV1, "fsLayers").Array())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gather sub-image info of multi-arch image
|
||||||
|
var digestList []map[string]interface{}
|
||||||
|
for _, s := range manifests {
|
||||||
|
r, _ := gjson.Parse(s.String()).Value().(map[string]interface{})
|
||||||
|
if s.Get("mediaType").String() == "application/vnd.docker.distribution.manifest.v2+json" {
|
||||||
|
_, dInfoV1, _ := a.client.TagInfo(repoPath, s.Get("digest").String(), true)
|
||||||
|
var dSize int64
|
||||||
|
for _, d := range gjson.Get(dInfoV1, "layers.#.size").Array() {
|
||||||
|
dSize = dSize + d.Int()
|
||||||
|
}
|
||||||
|
r["size"] = dSize
|
||||||
|
} else {
|
||||||
|
r["size"] = s.Get("size").Int()
|
||||||
|
}
|
||||||
|
delete(r, "mediaType")
|
||||||
|
r["ordered_keys"] = registry.SortedMapKeys(r)
|
||||||
|
digestList = append(digestList, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate template vars
|
||||||
data := jet.VarMap{}
|
data := jet.VarMap{}
|
||||||
data.Set("namespace", namespace)
|
data.Set("namespace", namespace)
|
||||||
data.Set("repo", repo)
|
data.Set("repo", repo)
|
||||||
|
data.Set("tag", tag)
|
||||||
|
data.Set("repoPath", repoPath)
|
||||||
data.Set("sha256", sha256)
|
data.Set("sha256", sha256)
|
||||||
data.Set("imageSize", imageSize)
|
data.Set("imageSize", imageSize)
|
||||||
data.Set("tag", gjson.Get(infoV1, "tag").String())
|
data.Set("created", created)
|
||||||
data.Set("repoPath", gjson.Get(infoV1, "name").String())
|
|
||||||
data.Set("created", gjson.Get(gjson.Get(infoV1, "history.0.v1Compatibility").String(), "created").String())
|
|
||||||
data.Set("layersCount", layersCount)
|
data.Set("layersCount", layersCount)
|
||||||
data.Set("layersV2", layersV2)
|
data.Set("layersV2", layersV2)
|
||||||
data.Set("layersV1", layersV1)
|
data.Set("layersV1", layersV1)
|
||||||
|
data.Set("isDigest", isDigest)
|
||||||
|
data.Set("digestList", digestList)
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "tag_info.html", data)
|
return c.Render(http.StatusOK, "tag_info.html", data)
|
||||||
}
|
}
|
||||||
|
@@ -10,11 +10,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hhkbp2/go-logging"
|
|
||||||
"github.com/parnurzeal/gorequest"
|
"github.com/parnurzeal/gorequest"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const userAgent = "docker-registry-ui"
|
||||||
|
|
||||||
// Client main class.
|
// Client main class.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
url string
|
url string
|
||||||
@@ -22,7 +24,7 @@ type Client struct {
|
|||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
request *gorequest.SuperAgent
|
request *gorequest.SuperAgent
|
||||||
logger logging.Logger
|
logger *logrus.Entry
|
||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
tokens map[string]string
|
tokens map[string]string
|
||||||
repos map[string][]string
|
repos map[string][]string
|
||||||
@@ -44,7 +46,8 @@ func NewClient(url string, verifyTLS bool, username, password string) *Client {
|
|||||||
repos: map[string][]string{},
|
repos: map[string][]string{},
|
||||||
tagCounts: map[string]int{},
|
tagCounts: map[string]int{},
|
||||||
}
|
}
|
||||||
resp, _, errs := c.request.Get(c.url+"/v2/").Set("User-Agent", "docker-registry-ui").End()
|
resp, _, errs := c.request.Get(c.url+"/v2/").
|
||||||
|
Set("User-Agent", userAgent).End()
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
c.logger.Error(errs[0])
|
c.logger.Error(errs[0])
|
||||||
return nil
|
return nil
|
||||||
@@ -82,14 +85,18 @@ func NewClient(url string, verifyTLS bool, username, password string) *Client {
|
|||||||
func (c *Client) getToken(scope string) string {
|
func (c *Client) getToken(scope string) string {
|
||||||
// Check if we have already a token and it's not expired.
|
// Check if we have already a token and it's not expired.
|
||||||
if token, ok := c.tokens[scope]; ok {
|
if token, ok := c.tokens[scope]; ok {
|
||||||
resp, _, _ := c.request.Get(c.url+"/v2/").Set("Authorization", fmt.Sprintf("Bearer %s", token)).Set("User-Agent", "docker-registry-ui").End()
|
resp, _, _ := c.request.Get(c.url+"/v2/").
|
||||||
|
Set("Authorization", fmt.Sprintf("Bearer %s", token)).
|
||||||
|
Set("User-Agent", userAgent).End()
|
||||||
if resp != nil && resp.StatusCode == 200 {
|
if resp != nil && resp.StatusCode == 200 {
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
request := gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: !c.verifyTLS})
|
request := gorequest.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: !c.verifyTLS})
|
||||||
resp, data, errs := request.Get(fmt.Sprintf("%s&scope=%s", c.authURL, scope)).SetBasicAuth(c.username, c.password).Set("User-Agent", "docker-registry-ui").End()
|
resp, data, errs := request.Get(fmt.Sprintf("%s&scope=%s", c.authURL, scope)).
|
||||||
|
SetBasicAuth(c.username, c.password).
|
||||||
|
Set("User-Agent", userAgent).End()
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
c.logger.Error(errs[0])
|
c.logger.Error(errs[0])
|
||||||
return ""
|
return ""
|
||||||
@@ -99,55 +106,50 @@ func (c *Client) getToken(scope string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
c.tokens[scope] = gjson.Get(data, "token").String()
|
token := gjson.Get(data, "token").String()
|
||||||
c.logger.Info("Received new token for scope ", scope)
|
// Fix for docker_auth v1.5.0 only
|
||||||
|
if token == "" {
|
||||||
|
token = gjson.Get(data, "access_token").String()
|
||||||
|
}
|
||||||
|
|
||||||
|
c.tokens[scope] = token
|
||||||
|
c.logger.Debugf("Received new token for scope %s", scope)
|
||||||
|
|
||||||
return c.tokens[scope]
|
return c.tokens[scope]
|
||||||
}
|
}
|
||||||
|
|
||||||
// callRegistry make an HTTP request to Docker registry.
|
// callRegistry make an HTTP request to retrieve data from Docker registry.
|
||||||
func (c *Client) callRegistry(uri, scope string, manifest uint, delete bool) (string, gorequest.Response) {
|
func (c *Client) callRegistry(uri, scope, manifestFormat string) (string, gorequest.Response) {
|
||||||
acceptHeader := fmt.Sprintf("application/vnd.docker.distribution.manifest.v%d+json", manifest)
|
acceptHeader := fmt.Sprintf("application/vnd.docker.distribution.%s+json", manifestFormat)
|
||||||
authHeader := ""
|
authHeader := ""
|
||||||
if c.authURL != "" {
|
if c.authURL != "" {
|
||||||
authHeader = fmt.Sprintf("Bearer %s", c.getToken(scope))
|
authHeader = fmt.Sprintf("Bearer %s", c.getToken(scope))
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, data, errs := c.request.Get(c.url+uri).Set("Accept", acceptHeader).Set("Authorization", authHeader).Set("User-Agent", "docker-registry-ui").End()
|
resp, data, errs := c.request.Get(c.url+uri).
|
||||||
|
Set("Accept", acceptHeader).
|
||||||
|
Set("Authorization", authHeader).
|
||||||
|
Set("User-Agent", userAgent).End()
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
c.logger.Error(errs[0])
|
c.logger.Error(errs[0])
|
||||||
return "", resp
|
return "", resp
|
||||||
}
|
}
|
||||||
|
|
||||||
c.logger.Info("GET ", uri, " ", resp.Status)
|
c.logger.Debugf("GET %s %s", uri, resp.Status)
|
||||||
// Returns 404 when no tags in the repo.
|
// Returns 404 when no tags in the repo.
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return "", resp
|
return "", resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure Docker-Content-Digest header is present as we use it in various places.
|
||||||
|
// The header is probably in AWS ECR case.
|
||||||
digest := resp.Header.Get("Docker-Content-Digest")
|
digest := resp.Header.Get("Docker-Content-Digest")
|
||||||
if digest == "" {
|
if digest == "" {
|
||||||
// Try to get digest from body instead, should be equal to what would be presented
|
// Try to get digest from body instead, should be equal to what would be presented in Docker-Content-Digest.
|
||||||
// in Docker-Content-Digest
|
|
||||||
h := crypto.SHA256.New()
|
h := crypto.SHA256.New()
|
||||||
h.Write([]byte(data))
|
h.Write([]byte(data))
|
||||||
resp.Header.Set("Docker-Content-Digest", fmt.Sprintf("sha256:%x", h.Sum(nil)))
|
resp.Header.Set("Docker-Content-Digest", fmt.Sprintf("sha256:%x", h.Sum(nil)))
|
||||||
}
|
}
|
||||||
|
|
||||||
if delete {
|
|
||||||
// Delete by manifest digest reference.
|
|
||||||
parts := strings.Split(uri, "/manifests/")
|
|
||||||
uri = parts[0] + "/manifests/" + digest
|
|
||||||
resp, _, errs := c.request.Delete(c.url+uri).Set("Accept", acceptHeader).Set("Authorization", authHeader).Set("User-Agent", "docker-registry-ui").End()
|
|
||||||
if len(errs) > 0 {
|
|
||||||
c.logger.Error(errs[0])
|
|
||||||
} else {
|
|
||||||
// Returns 202 on success.
|
|
||||||
c.logger.Info("DELETE ", uri, " (", parts[1], ") ", resp.Status)
|
|
||||||
}
|
|
||||||
return "", resp
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, resp
|
return data, resp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +181,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string {
|
|||||||
uri := "/v2/_catalog"
|
uri := "/v2/_catalog"
|
||||||
c.repos = map[string][]string{}
|
c.repos = map[string][]string{}
|
||||||
for {
|
for {
|
||||||
data, resp := c.callRegistry(uri, scope, 2, false)
|
data, resp := c.callRegistry(uri, scope, "manifest.v2")
|
||||||
if data == "" {
|
if data == "" {
|
||||||
return c.repos
|
return c.repos
|
||||||
}
|
}
|
||||||
@@ -212,7 +214,7 @@ func (c *Client) Repositories(useCache bool) map[string][]string {
|
|||||||
// Tags get tags for the repo.
|
// Tags get tags for the repo.
|
||||||
func (c *Client) Tags(repo string) []string {
|
func (c *Client) Tags(repo string) []string {
|
||||||
scope := fmt.Sprintf("repository:%s:*", repo)
|
scope := fmt.Sprintf("repository:%s:*", repo)
|
||||||
data, _ := c.callRegistry(fmt.Sprintf("/v2/%s/tags/list", repo), scope, 2, false)
|
data, _ := c.callRegistry(fmt.Sprintf("/v2/%s/tags/list", repo), scope, "manifest.v2")
|
||||||
var tags []string
|
var tags []string
|
||||||
for _, t := range gjson.Get(data, "tags").Array() {
|
for _, t := range gjson.Get(data, "tags").Array() {
|
||||||
tags = append(tags, t.String())
|
tags = append(tags, t.String())
|
||||||
@@ -220,25 +222,45 @@ func (c *Client) Tags(repo string) []string {
|
|||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
// TagInfo get image info for the repo tag.
|
// ManifestList gets manifest list entries for a tag for the repo.
|
||||||
func (c *Client) TagInfo(repo, tag string, v1only bool) (rsha256, rinfoV1, rinfoV2 string) {
|
func (c *Client) ManifestList(repo, tag string) (string, []gjson.Result) {
|
||||||
scope := fmt.Sprintf("repository:%s:*", repo)
|
scope := fmt.Sprintf("repository:%s:*", repo)
|
||||||
infoV1, _ := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 1, false)
|
uri := fmt.Sprintf("/v2/%s/manifests/%s", repo, tag)
|
||||||
if infoV1 == "" {
|
// If manifest.list.v2 does not exist because it's a normal image,
|
||||||
return "", "", ""
|
// the registry returns manifest.v1 or manifest.v2 if requested by sha256.
|
||||||
|
info, resp := c.callRegistry(uri, scope, "manifest.list.v2")
|
||||||
|
digest := resp.Header.Get("Docker-Content-Digest")
|
||||||
|
sha256 := ""
|
||||||
|
if digest != "" {
|
||||||
|
sha256 = digest[7:]
|
||||||
}
|
}
|
||||||
|
c.logger.Debugf(`Received manifest.list.v2 with sha256 "%s" from %s: %s`, sha256, uri, info)
|
||||||
|
return sha256, gjson.Get(info, "manifests").Array()
|
||||||
|
}
|
||||||
|
|
||||||
if v1only {
|
// TagInfo get image info for the repo tag or digest sha256.
|
||||||
|
func (c *Client) TagInfo(repo, tag string, v1only bool) (string, string, string) {
|
||||||
|
scope := fmt.Sprintf("repository:%s:*", repo)
|
||||||
|
uri := fmt.Sprintf("/v2/%s/manifests/%s", repo, tag)
|
||||||
|
// Note, if manifest.v1 does not exist because the image is requested by sha256,
|
||||||
|
// the registry returns manifest.v2 instead or manifest.list.v2 if it's the manifest list!
|
||||||
|
infoV1, _ := c.callRegistry(uri, scope, "manifest.v1")
|
||||||
|
c.logger.Debugf("Received manifest.v1 from %s: %s", uri, infoV1)
|
||||||
|
if infoV1 == "" || v1only {
|
||||||
return "", infoV1, ""
|
return "", infoV1, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
infoV2, resp := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, false)
|
// Note, if manifest.v2 does not exist because the image is in the older format (Docker 1.9),
|
||||||
|
// the registry returns manifest.v1 instead or manifest.list.v2 if it's the manifest list requested by sha256!
|
||||||
|
infoV2, resp := c.callRegistry(uri, scope, "manifest.v2")
|
||||||
|
c.logger.Debugf("Received manifest.v2 from %s: %s", uri, infoV2)
|
||||||
digest := resp.Header.Get("Docker-Content-Digest")
|
digest := resp.Header.Get("Docker-Content-Digest")
|
||||||
if infoV2 == "" || digest == "" {
|
if infoV2 == "" || digest == "" {
|
||||||
return "", "", ""
|
return "", "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
sha256 := digest[7:]
|
sha256 := digest[7:]
|
||||||
|
c.logger.Debugf("sha256 for %s/%s is %s", repo, tag, sha256)
|
||||||
return sha256, infoV1, infoV2
|
return sha256, infoV1, infoV2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +272,8 @@ func (c *Client) TagCounts() map[string]int {
|
|||||||
// CountTags count repository tags in background regularly.
|
// CountTags count repository tags in background regularly.
|
||||||
func (c *Client) CountTags(interval uint8) {
|
func (c *Client) CountTags(interval uint8) {
|
||||||
for {
|
for {
|
||||||
c.logger.Info("Calculating tags in background...")
|
start := time.Now()
|
||||||
|
c.logger.Info("[CountTags] Calculating image tags...")
|
||||||
catalog := c.Repositories(false)
|
catalog := c.Repositories(false)
|
||||||
for n, repos := range catalog {
|
for n, repos := range catalog {
|
||||||
for _, r := range repos {
|
for _, r := range repos {
|
||||||
@@ -261,7 +284,7 @@ func (c *Client) CountTags(interval uint8) {
|
|||||||
c.tagCounts[fmt.Sprintf("%s/%s", n, r)] = len(c.Tags(repoPath))
|
c.tagCounts[fmt.Sprintf("%s/%s", n, r)] = len(c.Tags(repoPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.logger.Info("Tags calculation complete.")
|
c.logger.Infof("[CountTags] Job complete (%v).", time.Now().Sub(start))
|
||||||
time.Sleep(time.Duration(interval) * time.Minute)
|
time.Sleep(time.Duration(interval) * time.Minute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,5 +292,27 @@ func (c *Client) CountTags(interval uint8) {
|
|||||||
// DeleteTag delete image tag.
|
// DeleteTag delete image tag.
|
||||||
func (c *Client) DeleteTag(repo, tag string) {
|
func (c *Client) DeleteTag(repo, tag string) {
|
||||||
scope := fmt.Sprintf("repository:%s:*", repo)
|
scope := fmt.Sprintf("repository:%s:*", repo)
|
||||||
c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, 2, true)
|
// Get sha256 digest for tag.
|
||||||
|
_, resp := c.callRegistry(fmt.Sprintf("/v2/%s/manifests/%s", repo, tag), scope, "manifest.v2")
|
||||||
|
|
||||||
|
// Delete by manifest digest reference.
|
||||||
|
authHeader := ""
|
||||||
|
if c.authURL != "" {
|
||||||
|
authHeader = fmt.Sprintf("Bearer %s", c.getToken(scope))
|
||||||
|
}
|
||||||
|
uri := fmt.Sprintf("/v2/%s/manifests/%s", repo, resp.Header.Get("Docker-Content-Digest"))
|
||||||
|
resp, _, errs := c.request.Delete(c.url+uri).
|
||||||
|
Set("Authorization", authHeader).
|
||||||
|
Set("User-Agent", userAgent).End()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
c.logger.Error(errs[0])
|
||||||
|
} else {
|
||||||
|
// Returns 202 on success.
|
||||||
|
if !strings.Contains(repo, "/") {
|
||||||
|
c.tagCounts["library/"+repo]--
|
||||||
|
} else {
|
||||||
|
c.tagCounts[repo]--
|
||||||
|
}
|
||||||
|
c.logger.Infof("DELETE %s (tag:%s) %s", uri, tag, resp.Status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,23 +2,24 @@ package registry
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hhkbp2/go-logging"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetupLogging configure logging.
|
// SetupLogging setup logger
|
||||||
func SetupLogging(name string) logging.Logger {
|
func SetupLogging(name string) *logrus.Entry {
|
||||||
logger := logging.GetLogger(name)
|
logrus.SetFormatter(&logrus.TextFormatter{
|
||||||
handler := logging.NewStdoutHandler()
|
TimestampFormat: time.RFC3339,
|
||||||
format := "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
FullTimestamp: true,
|
||||||
dateFormat := "%Y-%m-%d %H:%M:%S"
|
})
|
||||||
formatter := logging.NewStandardFormatter(format, dateFormat)
|
// Output to stdout instead of the default stderr.
|
||||||
handler.SetFormatter(formatter)
|
logrus.SetOutput(os.Stdout)
|
||||||
logger.SetLevel(logging.LevelInfo)
|
|
||||||
logger.AddHandler(handler)
|
return logrus.WithFields(logrus.Fields{"logger": name})
|
||||||
return logger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SortedMapKeys sort keys of the map where values can be of any type.
|
// SortedMapKeys sort keys of the map where values can be of any type.
|
||||||
@@ -40,7 +41,12 @@ func PrettySize(size float64) string {
|
|||||||
size = size / 1024
|
size = size / 1024
|
||||||
i = i + 1
|
i = i + 1
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%.*f %s", 0, size, units[i])
|
// Format decimals as follow: 0 B, 0 KB, 0.0 MB, 0.00 GB
|
||||||
|
decimals := i - 1
|
||||||
|
if decimals < 0 {
|
||||||
|
decimals = 0
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.*f %s", decimals, size, units[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ItemInSlice check if item is an element of slice
|
// ItemInSlice check if item is an element of slice
|
||||||
|
@@ -37,8 +37,8 @@ func TestPrettySize(t *testing.T) {
|
|||||||
123: "123 B",
|
123: "123 B",
|
||||||
23123: "23 KB",
|
23123: "23 KB",
|
||||||
23923: "23 KB",
|
23923: "23 KB",
|
||||||
723425120: "690 MB",
|
723425120: "689.9 MB",
|
||||||
8534241213: "8 GB",
|
8534241213: "7.95 GB",
|
||||||
}
|
}
|
||||||
for key, val := range input {
|
for key, val := range input {
|
||||||
convey.So(PrettySize(key), convey.ShouldEqual, val)
|
convey.So(PrettySize(key), convey.ShouldEqual, val)
|
||||||
|
@@ -5,7 +5,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hhkbp2/go-logging"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ func (p timeSlice) Swap(i, j int) {
|
|||||||
func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTagsKeepCount int) {
|
func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTagsKeepCount int) {
|
||||||
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
||||||
// Reduce client logging.
|
// Reduce client logging.
|
||||||
client.logger.SetLevel(logging.LevelError)
|
client.logger.Logger.SetLevel(logrus.ErrorLevel)
|
||||||
|
|
||||||
dryRunText := ""
|
dryRunText := ""
|
||||||
if purgeDryRun {
|
if purgeDryRun {
|
||||||
|
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/CloudyKit/jet"
|
"github.com/CloudyKit/jet"
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/quiq/docker-registry-ui/registry"
|
"github.com/quiq/docker-registry-ui/registry"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<div style="padding: 10px 0; margin-bottom: 20px">
|
<div style="padding: 10px 0; margin-bottom: 20px">
|
||||||
<div style="text-align: center; color:darkgrey">
|
<div style="text-align: center; color:darkgrey">
|
||||||
Docker Registry UI v{{version}} © 2017-2018 <a href="https://goquiq.com">Quiq Inc.</a>
|
Docker Registry UI v{{version}} © 2017-2020 <a href="https://quiq.com">Quiq Inc.</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -56,10 +56,12 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range repo := repos}}
|
{{range repo := repos}}
|
||||||
|
{{if !isset(tagCounts[namespace+"/"+repo]) || (isset(tagCounts[namespace+"/"+repo]) && tagCounts[namespace+"/"+repo] > 0)}}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ basePath }}/{{ namespace }}/{{ repo|url }}">{{ repo }}</a></td>
|
<td><a href="{{ basePath }}/{{ namespace }}/{{ repo|url }}">{{ repo }}</a></td>
|
||||||
<td>{{ tagCounts[namespace+"/"+repo] }}</td>
|
<td>{{ tagCounts[namespace+"/"+repo] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -11,31 +11,74 @@
|
|||||||
<li><a href="{{ basePath }}/{{ namespace }}/{{ repo }}">{{ repo|url_decode }}</a></li>
|
<li><a href="{{ basePath }}/{{ namespace }}/{{ repo }}">{{ repo|url_decode }}</a></li>
|
||||||
<li class="active">{{ tag }}</li>
|
<li class="active">{{ tag }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
<h4>Image Details</h4>
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<thead bgcolor="#ddd">
|
<thead bgcolor="#ddd">
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="2">Image Details</th>
|
<th colspan="2">Summary</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tr>
|
<tr>
|
||||||
<td width="20%">Image</td><td>{{ registryHost }}/{{ repoPath }}:{{ tag }}</td>
|
<td width="20%"><b>Image URL</b></td><td>{{ registryHost }}/{{ repoPath }}{{if isDigest}}@{{else}}:{{end}}{{ tag }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>sha256</td><td>{{ sha256 }}</td>
|
<td><b>Digest</b></td><td>sha256:{{ sha256 }}</td>
|
||||||
|
</tr>
|
||||||
|
{{if not isDigest}}
|
||||||
|
<tr>
|
||||||
|
<td><b>Created On</b></td><td>{{ created|pretty_time }}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
{{if not digestList}}
|
||||||
|
<tr>
|
||||||
|
<td><b>Image Size</b></td><td>{{ imageSize|pretty_size }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Created On</td><td>{{ created|pretty_time }}</td>
|
<td><b>Layer Count</b></td><td>{{ layersCount }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{{end}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Image Size</td><td>{{ imageSize|pretty_size }}</td>
|
<td><b>Manifest Formats</b></td>
|
||||||
</tr>
|
<td>{{if not isDigest}}Manifest v2 schema 1{{else}}<font color="#c2c2c2">Manifest v2 schema 1</font>{{end}} |
|
||||||
<tr>
|
{{if not digestList && layersV2}}Manifest v2 schema 2{{else}}<font color="#c2c2c2">Manifest v2 schema 2</font>{{end}} |
|
||||||
<td>Layer Count</td><td>{{ layersCount }}</td>
|
{{if digestList}}Manifest List v2 schema 2{{else}}<font color="#c2c2c2">Manifest List v2 schema 2</font>{{end}}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
{{if layersV2}}
|
{{if digestList}}
|
||||||
<h4>Manifest v2</h4>
|
<h4>Multi-arch Sub-images <!-- Manifest List v2 schema 2 --></h4>
|
||||||
|
{{range index, manifest := digestList}}
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead bgcolor="#ddd">
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">Manifest #{{ index+1 }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
{{range key := manifest["ordered_keys"]}}
|
||||||
|
<tr>
|
||||||
|
<td width="20%">{{ key }}</td>
|
||||||
|
{{if key == "platform" || key == "annotations"}}
|
||||||
|
<td style="padding: 0">
|
||||||
|
<table class="table table-bordered" style="padding: 0; width: 100%; margin-bottom: 0; min-height: 37px">
|
||||||
|
<!-- Nested range does not work. Iterating via filter over the map. -->
|
||||||
|
{{ manifest[key]|parse_map|raw }}
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
{{else if key == "size"}}
|
||||||
|
<td>{{ manifest[key]|pretty_size }}</td>
|
||||||
|
{{else if key == "digest"}}
|
||||||
|
<td><a href="{{ basePath }}/{{ namespace }}/{{ repo }}/{{ manifest["digest"] }}">{{ manifest["digest"] }}</a></td>
|
||||||
|
{{else}}
|
||||||
|
<td>{{ manifest[key] }}</td>
|
||||||
|
{{end}}
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
{{end}}
|
||||||
|
{{else if layersV2}}
|
||||||
|
<h4>Blobs <!-- Manifest v2 schema 2--></h4>
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<thead bgcolor="#ddd">
|
<thead bgcolor="#ddd">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -54,7 +97,8 @@
|
|||||||
</table>
|
</table>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<h4>Manifest v1</h4>
|
{{if not isDigest}}
|
||||||
|
<h4>Image History <!-- Manifest v2 schema 1--></h4>
|
||||||
{{range index, layer := layersV1}}
|
{{range index, layer := layersV1}}
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<thead bgcolor="#ddd">
|
<thead bgcolor="#ddd">
|
||||||
@@ -81,5 +125,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</table>
|
</table>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
const version = "0.8.2"
|
const version = "0.9.0"
|
||||||
|
Reference in New Issue
Block a user