mirror of
https://github.com/Quiq/docker-registry-ui.git
synced 2025-09-29 06:16:08 +00:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
901beb5c9b | ||
|
102e9ae154 | ||
|
e538d4c3f1 | ||
|
b5e11aae10 | ||
|
2aa58fc9ba | ||
|
ad41aca4e5 | ||
|
76f380d3c2 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
config-dev.yml
|
|
||||||
data/registry_events.db
|
data/registry_events.db
|
||||||
vendor/
|
config-dev.yml
|
||||||
|
dev.Makefile
|
||||||
|
keep_tags.json
|
||||||
|
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 0.9.5 (2022-09-05)
|
||||||
|
|
||||||
|
* Upgrade Go version to 1.19.0, alpine to 3.16 and other dependencies.
|
||||||
|
* Add an option `anyone_can_view_events` to restrict access to the event log. Set it to `true` to make event log accessible to anyone (to restore the previous behaviour), otherwise the default `false` will hide it and only admins can view it (thanks to @ribbybibby).
|
||||||
|
* Add an option `purge_tags_keep_regexp` to preserve tags based on regexp (thanks to @dmaes).
|
||||||
|
* Add an option `purge_tags_keep_from_file` to preserve tags for repos listed in the file provided.
|
||||||
|
* When purging tags sort them by name reversibly when no date available, e.g. for OCI image format (thanks to @dmaes).
|
||||||
|
* Fix a bug when there was a bit more tags preserved than defined by `purge_tags_keep_count`.
|
||||||
|
|
||||||
|
Also see `config.yml` in this repo for the description of new options.
|
||||||
|
|
||||||
### 0.9.4 (2022-04-06)
|
### 0.9.4 (2022-04-06)
|
||||||
|
|
||||||
* Upgrade Go version to 1.18.0, alpine to 3.15 and other dependencies.
|
* Upgrade Go version to 1.18.0, alpine to 3.15 and other dependencies.
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.18.0-alpine3.15 as builder
|
FROM golang:1.19.0-alpine3.16 as builder
|
||||||
|
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add ca-certificates git bash gcc musl-dev
|
apk add ca-certificates git bash gcc musl-dev
|
||||||
@@ -12,7 +12,7 @@ RUN go test -v ./registry && \
|
|||||||
go build -o /opt/docker-registry-ui *.go
|
go build -o /opt/docker-registry-ui *.go
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:3.15
|
FROM alpine:3.16
|
||||||
|
|
||||||
WORKDIR /opt
|
WORKDIR /opt
|
||||||
RUN apk add --no-cache ca-certificates tzdata && \
|
RUN apk add --no-cache ca-certificates tzdata && \
|
||||||
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
13
LICENSE.md
13
LICENSE.md
@@ -1,13 +0,0 @@
|
|||||||
Copyright 2017-2020 Quiq Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
@@ -102,8 +102,6 @@ You can try to run in dry-run mode first to see what is going to be purged:
|
|||||||
|
|
||||||
Alternatively, you can schedule the purging task with built-in cron feature:
|
Alternatively, you can schedule the purging task with built-in cron feature:
|
||||||
|
|
||||||
purge_tags_keep_days: 90
|
|
||||||
purge_tags_keep_count: 2
|
|
||||||
purge_tags_schedule: '0 10 3 * * *'
|
purge_tags_schedule: '0 10 3 * * *'
|
||||||
|
|
||||||
Note, the cron schedule format includes seconds! See https://godoc.org/github.com/robfig/cron
|
Note, the cron schedule format includes seconds! See https://godoc.org/github.com/robfig/cron
|
||||||
|
84
config.go
Normal file
84
config.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/quiq/docker-registry-ui/registry"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configData struct {
|
||||||
|
ListenAddr string `yaml:"listen_addr"`
|
||||||
|
BasePath string `yaml:"base_path"`
|
||||||
|
RegistryURL string `yaml:"registry_url"`
|
||||||
|
VerifyTLS bool `yaml:"verify_tls"`
|
||||||
|
Username string `yaml:"registry_username"`
|
||||||
|
Password string `yaml:"registry_password"`
|
||||||
|
PasswordFile string `yaml:"registry_password_file"`
|
||||||
|
EventListenerToken string `yaml:"event_listener_token"`
|
||||||
|
EventRetentionDays int `yaml:"event_retention_days"`
|
||||||
|
EventDatabaseDriver string `yaml:"event_database_driver"`
|
||||||
|
EventDatabaseLocation string `yaml:"event_database_location"`
|
||||||
|
EventDeletionEnabled bool `yaml:"event_deletion_enabled"`
|
||||||
|
CacheRefreshInterval uint8 `yaml:"cache_refresh_interval"`
|
||||||
|
AnyoneCanDelete bool `yaml:"anyone_can_delete"`
|
||||||
|
AnyoneCanViewEvents bool `yaml:"anyone_can_view_events"`
|
||||||
|
Admins []string `yaml:"admins"`
|
||||||
|
Debug bool `yaml:"debug"`
|
||||||
|
PurgeTagsKeepDays int `yaml:"purge_tags_keep_days"`
|
||||||
|
PurgeTagsKeepCount int `yaml:"purge_tags_keep_count"`
|
||||||
|
PurgeTagsKeepRegexp string `yaml:"purge_tags_keep_regexp"`
|
||||||
|
PurgeTagsKeepFromFile string `yaml:"purge_tags_keep_from_file"`
|
||||||
|
PurgeTagsSchedule string `yaml:"purge_tags_schedule"`
|
||||||
|
|
||||||
|
PurgeConfig *registry.PurgeTagsConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfig(configFile string) *configData {
|
||||||
|
var config configData
|
||||||
|
// Read config file.
|
||||||
|
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate registry URL.
|
||||||
|
if _, err := url.Parse(config.RegistryURL); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize base path.
|
||||||
|
config.BasePath = strings.Trim(config.BasePath, "/")
|
||||||
|
if config.BasePath != "" {
|
||||||
|
config.BasePath = "/" + config.BasePath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read password from file.
|
||||||
|
if config.PasswordFile != "" {
|
||||||
|
if _, err := os.Stat(config.PasswordFile); os.IsNotExist(err) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile(config.PasswordFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
config.Password = strings.TrimSuffix(string(data[:]), "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
config.PurgeConfig = ®istry.PurgeTagsConfig{
|
||||||
|
KeepDays: config.PurgeTagsKeepDays,
|
||||||
|
KeepMinCount: config.PurgeTagsKeepCount,
|
||||||
|
KeepTagRegexp: config.PurgeTagsKeepRegexp,
|
||||||
|
KeepFromFile: config.PurgeTagsKeepFromFile,
|
||||||
|
}
|
||||||
|
return &config
|
||||||
|
}
|
16
config.yml
16
config.yml
@@ -32,13 +32,15 @@ event_database_location: data/registry_events.db
|
|||||||
|
|
||||||
# You can disable event deletion on some hosts when you are running docker-registry on master-master or
|
# You can disable event deletion on some hosts when you are running docker-registry on master-master or
|
||||||
# cluster setup to avoid deadlocks or replication break.
|
# cluster setup to avoid deadlocks or replication break.
|
||||||
event_deletion_enabled: True
|
event_deletion_enabled: true
|
||||||
|
|
||||||
# Cache refresh interval in minutes.
|
# Cache refresh interval in minutes.
|
||||||
# How long to cache repository list and tag counts.
|
# How long to cache repository list and tag counts.
|
||||||
cache_refresh_interval: 10
|
cache_refresh_interval: 10
|
||||||
|
|
||||||
# If users can delete tags. If set to False, then only admins listed below.
|
# If all users can view the event log. If set to false, then only admins listed below.
|
||||||
|
anyone_can_view_events: true
|
||||||
|
# If all users can delete tags. If set to false, then only admins listed below.
|
||||||
anyone_can_delete: false
|
anyone_can_delete: false
|
||||||
# Users allowed to delete tags.
|
# Users allowed to delete tags.
|
||||||
# This should be sent via X-WEBAUTH-USER header from your proxy.
|
# This should be sent via X-WEBAUTH-USER header from your proxy.
|
||||||
@@ -50,6 +52,16 @@ debug: true
|
|||||||
# How many days to keep tags but also keep the minimal count provided no matter how old.
|
# How many days to keep tags but also keep the minimal count provided no matter how old.
|
||||||
purge_tags_keep_days: 90
|
purge_tags_keep_days: 90
|
||||||
purge_tags_keep_count: 2
|
purge_tags_keep_count: 2
|
||||||
|
|
||||||
|
# Keep tags matching regexp no matter how old, e.g. '^latest$'
|
||||||
|
# Empty string disables this feature.
|
||||||
|
purge_tags_keep_regexp: ''
|
||||||
|
|
||||||
|
# Keep tags listed in the file no matter how old.
|
||||||
|
# File format is JSON: {"repo1": ["tag1", "tag2"], "repoX": ["tagX"]}
|
||||||
|
# Empty string disables this feature.
|
||||||
|
purge_tags_keep_from_file: ''
|
||||||
|
|
||||||
# Enable built-in cron to schedule purging tags in server mode.
|
# Enable built-in cron to schedule purging tags in server mode.
|
||||||
# Empty string disables this feature.
|
# Empty string disables this feature.
|
||||||
# Example: '25 54 17 * * *' will run it at 17:54:25 daily.
|
# Example: '25 54 17 * * *' will run it at 17:54:25 daily.
|
||||||
|
10
go.mod
10
go.mod
@@ -3,16 +3,16 @@ module github.com/quiq/docker-registry-ui
|
|||||||
require (
|
require (
|
||||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||||
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible
|
github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible
|
||||||
github.com/elazarl/goproxy v0.0.0-20220403042543-a53172b9392e // indirect
|
github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.6.0
|
github.com/go-sql-driver/mysql v1.6.0
|
||||||
github.com/labstack/echo/v4 v4.7.2
|
github.com/labstack/echo/v4 v4.8.0
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
github.com/mattn/go-sqlite3 v1.14.15
|
||||||
github.com/parnurzeal/gorequest v0.2.16
|
github.com/parnurzeal/gorequest v0.2.16
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/robfig/cron v1.2.0
|
github.com/robfig/cron v1.2.0
|
||||||
github.com/sirupsen/logrus v1.8.1
|
github.com/sirupsen/logrus v1.9.0
|
||||||
github.com/smartystreets/goconvey v1.7.2
|
github.com/smartystreets/goconvey v1.7.2
|
||||||
github.com/tidwall/gjson v1.14.0
|
github.com/tidwall/gjson v1.14.3
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
moul.io/http2curl v1.0.0 // indirect
|
moul.io/http2curl v1.0.0 // indirect
|
||||||
)
|
)
|
||||||
|
25
go.sum
25
go.sum
@@ -5,8 +5,8 @@ github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mo
|
|||||||
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/goproxy v0.0.0-20220403042543-a53172b9392e h1:8dhROE/dIrz8nOJQjah6LG37QfL8fZhQTp1RDAjuNpQ=
|
github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021 h1:EbF0UihnxWRcIMOwoVtqnAylsqcjzqpSvMdjF2Ud4rA=
|
||||||
github.com/elazarl/goproxy v0.0.0-20220403042543-a53172b9392e/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
github.com/elazarl/goproxy v0.0.0-20220529153421-8ea89ba92021/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
|
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
|
||||||
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
|
||||||
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
@@ -17,16 +17,16 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
|
|||||||
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/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/labstack/echo/v4 v4.7.2 h1:Kv2/p8OaQ+M6Ex4eGimg9b9e6icoxA42JSlOR3msKtI=
|
github.com/labstack/echo/v4 v4.8.0 h1:wdc6yKVaHxkNOEdz4cRZs1pQkwSXPiRjq69yWP4QQS8=
|
||||||
github.com/labstack/echo/v4 v4.7.2/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
|
github.com/labstack/echo/v4 v4.8.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
|
||||||
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
||||||
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ=
|
github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ=
|
||||||
github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
|
github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
@@ -36,18 +36,17 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
|
||||||
github.com/robfig/cron v1.2.0/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/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
|
||||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
|
||||||
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||||
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
|
||||||
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
|
||||||
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
|
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||||
github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
@@ -64,14 +63,14 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
|||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
|
||||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
276
main.go
276
main.go
@@ -3,53 +3,20 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/CloudyKit/jet"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/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/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type configData struct {
|
|
||||||
ListenAddr string `yaml:"listen_addr"`
|
|
||||||
BasePath string `yaml:"base_path"`
|
|
||||||
RegistryURL string `yaml:"registry_url"`
|
|
||||||
VerifyTLS bool `yaml:"verify_tls"`
|
|
||||||
Username string `yaml:"registry_username"`
|
|
||||||
Password string `yaml:"registry_password"`
|
|
||||||
PasswordFile string `yaml:"registry_password_file"`
|
|
||||||
EventListenerToken string `yaml:"event_listener_token"`
|
|
||||||
EventRetentionDays int `yaml:"event_retention_days"`
|
|
||||||
EventDatabaseDriver string `yaml:"event_database_driver"`
|
|
||||||
EventDatabaseLocation string `yaml:"event_database_location"`
|
|
||||||
EventDeletionEnabled bool `yaml:"event_deletion_enabled"`
|
|
||||||
CacheRefreshInterval uint8 `yaml:"cache_refresh_interval"`
|
|
||||||
AnyoneCanDelete bool `yaml:"anyone_can_delete"`
|
|
||||||
Admins []string `yaml:"admins"`
|
|
||||||
Debug bool `yaml:"debug"`
|
|
||||||
PurgeTagsKeepDays int `yaml:"purge_tags_keep_days"`
|
|
||||||
PurgeTagsKeepCount int `yaml:"purge_tags_keep_count"`
|
|
||||||
PurgeTagsSchedule string `yaml:"purge_tags_schedule"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type template struct {
|
|
||||||
View *jet.Set
|
|
||||||
}
|
|
||||||
|
|
||||||
type apiClient struct {
|
type apiClient struct {
|
||||||
client *registry.Client
|
client *registry.Client
|
||||||
eventListener *events.EventListener
|
eventListener *events.EventListener
|
||||||
config configData
|
config *configData
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -65,48 +32,16 @@ func main() {
|
|||||||
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()
|
||||||
|
|
||||||
|
// Setup logging
|
||||||
if loggingLevel != "info" {
|
if loggingLevel != "info" {
|
||||||
if level, err := logrus.ParseLevel(loggingLevel); err == nil {
|
if level, err := logrus.ParseLevel(loggingLevel); err == nil {
|
||||||
logrus.SetLevel(level)
|
logrus.SetLevel(level)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read config file.
|
// Read config file
|
||||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
a.config = readConfig(configFile)
|
||||||
panic(err)
|
a.config.PurgeConfig.DryRun = purgeDryRun
|
||||||
}
|
|
||||||
bytes, err := ioutil.ReadFile(configFile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := yaml.Unmarshal(bytes, &a.config); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
// Validate registry URL.
|
|
||||||
u, err := url.Parse(a.config.RegistryURL)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
// Normalize base path.
|
|
||||||
if a.config.BasePath != "" {
|
|
||||||
if !strings.HasPrefix(a.config.BasePath, "/") {
|
|
||||||
a.config.BasePath = "/" + a.config.BasePath
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(a.config.BasePath, "/") {
|
|
||||||
a.config.BasePath = a.config.BasePath[0 : len(a.config.BasePath)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Read password from file.
|
|
||||||
if a.config.PasswordFile != "" {
|
|
||||||
if _, err := os.Stat(a.config.PasswordFile); os.IsNotExist(err) {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
passwordBytes, err := ioutil.ReadFile(a.config.PasswordFile)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
a.config.Password = strings.TrimSuffix(string(passwordBytes[:]), "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init registry API client.
|
// Init registry API client.
|
||||||
a.client = registry.NewClient(a.config.RegistryURL, a.config.VerifyTLS, a.config.Username, a.config.Password)
|
a.client = registry.NewClient(a.config.RegistryURL, a.config.VerifyTLS, a.config.Username, a.config.Password)
|
||||||
@@ -114,19 +49,21 @@ func main() {
|
|||||||
panic(fmt.Errorf("cannot initialize api client or unsupported auth method"))
|
panic(fmt.Errorf("cannot initialize api client or unsupported auth method"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
purgeFunc := func() {
|
||||||
|
registry.PurgeOldTags(a.client, a.config.PurgeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
// Execute CLI task and exit.
|
// Execute CLI task and exit.
|
||||||
if purgeTags {
|
if purgeTags {
|
||||||
a.purgeOldTags(purgeDryRun)
|
purgeFunc()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedules to purge tags.
|
// Schedules to purge tags.
|
||||||
if a.config.PurgeTagsSchedule != "" {
|
if a.config.PurgeTagsSchedule != "" {
|
||||||
c := cron.New()
|
c := cron.New()
|
||||||
task := func() {
|
if err := c.AddFunc(a.config.PurgeTagsSchedule, purgeFunc); err != nil {
|
||||||
a.purgeOldTags(purgeDryRun)
|
panic(fmt.Errorf("invalid schedule format: %s", a.config.PurgeTagsSchedule))
|
||||||
}
|
|
||||||
if err := c.AddFunc(a.config.PurgeTagsSchedule, task); err != nil {
|
|
||||||
panic(fmt.Errorf("Invalid schedule format: %s", a.config.PurgeTagsSchedule))
|
|
||||||
}
|
}
|
||||||
c.Start()
|
c.Start()
|
||||||
}
|
}
|
||||||
@@ -143,7 +80,8 @@ func main() {
|
|||||||
|
|
||||||
// Template engine init.
|
// Template engine init.
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.Renderer = setupRenderer(a.config.Debug, u.Host, a.config.BasePath)
|
registryHost, _ := url.Parse(a.config.RegistryURL) // validated already in config.go
|
||||||
|
e.Renderer = setupRenderer(a.config.Debug, registryHost.Host, a.config.BasePath)
|
||||||
|
|
||||||
// Web routes.
|
// Web routes.
|
||||||
e.File("/favicon.ico", "static/favicon.ico")
|
e.File("/favicon.ico", "static/favicon.ico")
|
||||||
@@ -169,187 +107,3 @@ func main() {
|
|||||||
|
|
||||||
e.Logger.Fatal(e.Start(a.config.ListenAddr))
|
e.Logger.Fatal(e.Start(a.config.ListenAddr))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *apiClient) viewRepositories(c echo.Context) error {
|
|
||||||
namespace := c.Param("namespace")
|
|
||||||
if namespace == "" {
|
|
||||||
namespace = "library"
|
|
||||||
}
|
|
||||||
|
|
||||||
repos, _ := a.client.Repositories(true)[namespace]
|
|
||||||
data := jet.VarMap{}
|
|
||||||
data.Set("namespace", namespace)
|
|
||||||
data.Set("namespaces", a.client.Namespaces())
|
|
||||||
data.Set("repos", repos)
|
|
||||||
data.Set("tagCounts", a.client.TagCounts())
|
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "repositories.html", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *apiClient) viewTags(c echo.Context) error {
|
|
||||||
namespace := c.Param("namespace")
|
|
||||||
repo := c.Param("repo")
|
|
||||||
repoPath := repo
|
|
||||||
if namespace != "library" {
|
|
||||||
repoPath = fmt.Sprintf("%s/%s", namespace, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := a.client.Tags(repoPath)
|
|
||||||
deleteAllowed := a.checkDeletePermission(c.Request().Header.Get("X-WEBAUTH-USER"))
|
|
||||||
|
|
||||||
data := jet.VarMap{}
|
|
||||||
data.Set("namespace", namespace)
|
|
||||||
data.Set("repo", repo)
|
|
||||||
data.Set("tags", tags)
|
|
||||||
data.Set("deleteAllowed", deleteAllowed)
|
|
||||||
repoPath, _ = url.PathUnescape(repoPath)
|
|
||||||
data.Set("events", a.eventListener.GetEvents(repoPath))
|
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "tags.html", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *apiClient) viewTagInfo(c echo.Context) error {
|
|
||||||
namespace := c.Param("namespace")
|
|
||||||
repo := c.Param("repo")
|
|
||||||
tag := c.Param("tag")
|
|
||||||
repoPath := repo
|
|
||||||
if namespace != "library" {
|
|
||||||
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)
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
if gjson.Get(infoV2, "layers").Exists() {
|
|
||||||
for _, s := range gjson.Get(infoV2, "layers.#.size").Array() {
|
|
||||||
imageSize = imageSize + s.Int()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, s := range gjson.Get(infoV2, "history.#.v1Compatibility").Array() {
|
|
||||||
imageSize = imageSize + gjson.Get(s.String(), "Size").Int()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count layers
|
|
||||||
layersCount := len(layersV2)
|
|
||||||
if layersCount == 0 {
|
|
||||||
layersCount = len(gjson.Get(infoV1, "fsLayers").Array())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gather sub-image info of multi-arch or cache 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" {
|
|
||||||
// Sub-image of the specific arch.
|
|
||||||
_, 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
|
|
||||||
// Create link here because there is a bug with jet template when referencing a value by map key in the "if" condition under "range".
|
|
||||||
if r["mediaType"] == "application/vnd.docker.distribution.manifest.v2+json" {
|
|
||||||
r["digest"] = fmt.Sprintf(`<a href="%s/%s/%s/%s">%s</a>`, a.config.BasePath, namespace, repo, r["digest"], r["digest"])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Sub-image of the cache type.
|
|
||||||
r["size"] = s.Get("size").Int()
|
|
||||||
}
|
|
||||||
r["ordered_keys"] = registry.SortedMapKeys(r)
|
|
||||||
digestList = append(digestList, r)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate template vars
|
|
||||||
data := jet.VarMap{}
|
|
||||||
data.Set("namespace", namespace)
|
|
||||||
data.Set("repo", repo)
|
|
||||||
data.Set("tag", tag)
|
|
||||||
data.Set("repoPath", repoPath)
|
|
||||||
data.Set("sha256", sha256)
|
|
||||||
data.Set("imageSize", imageSize)
|
|
||||||
data.Set("created", created)
|
|
||||||
data.Set("layersCount", layersCount)
|
|
||||||
data.Set("layersV2", layersV2)
|
|
||||||
data.Set("layersV1", layersV1)
|
|
||||||
data.Set("isDigest", isDigest)
|
|
||||||
data.Set("digestList", digestList)
|
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "tag_info.html", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *apiClient) deleteTag(c echo.Context) error {
|
|
||||||
namespace := c.Param("namespace")
|
|
||||||
repo := c.Param("repo")
|
|
||||||
tag := c.Param("tag")
|
|
||||||
repoPath := repo
|
|
||||||
if namespace != "library" {
|
|
||||||
repoPath = fmt.Sprintf("%s/%s", namespace, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.checkDeletePermission(c.Request().Header.Get("X-WEBAUTH-USER")) {
|
|
||||||
a.client.DeleteTag(repoPath, tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo))
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkDeletePermission check if tag deletion is allowed whether by anyone or permitted users.
|
|
||||||
func (a *apiClient) checkDeletePermission(user string) bool {
|
|
||||||
deleteAllowed := a.config.AnyoneCanDelete
|
|
||||||
if !deleteAllowed {
|
|
||||||
for _, u := range a.config.Admins {
|
|
||||||
if u == user {
|
|
||||||
deleteAllowed = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return deleteAllowed
|
|
||||||
}
|
|
||||||
|
|
||||||
// viewLog view events from sqlite.
|
|
||||||
func (a *apiClient) viewLog(c echo.Context) error {
|
|
||||||
data := jet.VarMap{}
|
|
||||||
data.Set("events", a.eventListener.GetEvents(""))
|
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "event_log.html", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// receiveEvents receive events.
|
|
||||||
func (a *apiClient) receiveEvents(c echo.Context) error {
|
|
||||||
a.eventListener.ProcessEvents(c.Request())
|
|
||||||
return c.String(http.StatusOK, "OK")
|
|
||||||
}
|
|
||||||
|
|
||||||
// purgeOldTags purges old tags.
|
|
||||||
func (a *apiClient) purgeOldTags(dryRun bool) {
|
|
||||||
registry.PurgeOldTags(a.client, dryRun, a.config.PurgeTagsKeepDays, a.config.PurgeTagsKeepCount)
|
|
||||||
}
|
|
||||||
|
@@ -1,12 +1,32 @@
|
|||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/smartystreets/goconvey/convey"
|
"github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestKeepMinCount(t *testing.T) {
|
||||||
|
keepTags := []string{"1.8.15"}
|
||||||
|
purgeTags := []string{"1.8.14", "1.8.13", "1.8.12", "1.8.10", "1.8.9", "1.8.8", "1.8.7", "1.8.6", "1.8.5", "1.8.4", "1.8.3"}
|
||||||
|
purgeTagsKeepCount := 10
|
||||||
|
|
||||||
|
// Keep minimal count of tags no matter how old they are.
|
||||||
|
if len(keepTags) < purgeTagsKeepCount {
|
||||||
|
// Min of threshold-keep but not more than purge.
|
||||||
|
takeFromPurge := int(math.Min(float64(purgeTagsKeepCount-len(keepTags)), float64(len(purgeTags))))
|
||||||
|
keepTags = append(keepTags, purgeTags[:takeFromPurge]...)
|
||||||
|
purgeTags = purgeTags[takeFromPurge:]
|
||||||
|
}
|
||||||
|
|
||||||
|
convey.Convey("Test keep min count logic", t, func() {
|
||||||
|
convey.So(keepTags, convey.ShouldResemble, []string{"1.8.15", "1.8.14", "1.8.13", "1.8.12", "1.8.10", "1.8.9", "1.8.8", "1.8.7", "1.8.6", "1.8.5"})
|
||||||
|
convey.So(purgeTags, convey.ShouldResemble, []string{"1.8.4", "1.8.3"})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestSortedMapKeys(t *testing.T) {
|
func TestSortedMapKeys(t *testing.T) {
|
||||||
a := map[string]string{
|
a := map[string]string{
|
||||||
"foo": "bar",
|
"foo": "bar",
|
||||||
|
@@ -2,12 +2,24 @@ package registry
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PurgeTagsConfig struct {
|
||||||
|
DryRun bool
|
||||||
|
KeepDays int
|
||||||
|
KeepMinCount int
|
||||||
|
KeepTagRegexp string
|
||||||
|
KeepFromFile string
|
||||||
|
}
|
||||||
|
|
||||||
type tagData struct {
|
type tagData struct {
|
||||||
name string
|
name string
|
||||||
created time.Time
|
created time.Time
|
||||||
@@ -24,6 +36,11 @@ func (p timeSlice) Len() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p timeSlice) Less(i, j int) bool {
|
func (p timeSlice) Less(i, j int) bool {
|
||||||
|
// reverse sort tags on name if equal dates (OCI image case)
|
||||||
|
// see https://github.com/Quiq/docker-registry-ui/pull/62
|
||||||
|
if p[i].created.Equal(p[j].created) {
|
||||||
|
return p[i].name > p[j].name
|
||||||
|
}
|
||||||
return p[i].created.After(p[j].created)
|
return p[i].created.After(p[j].created)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,10 +49,27 @@ func (p timeSlice) Swap(i, j int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PurgeOldTags purge old tags.
|
// PurgeOldTags purge old tags.
|
||||||
func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTagsKeepCount int) {
|
func PurgeOldTags(client *Client, config *PurgeTagsConfig) {
|
||||||
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
||||||
|
|
||||||
|
var keepTagsFromFile gjson.Result
|
||||||
|
if config.KeepFromFile != "" {
|
||||||
|
if _, err := os.Stat(config.KeepFromFile); os.IsNotExist(err) {
|
||||||
|
logger.Warnf("Cannot open %s: %s", config.KeepFromFile, err)
|
||||||
|
logger.Error("Not purging anything!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile(config.KeepFromFile)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warnf("Cannot read %s: %s", config.KeepFromFile, err)
|
||||||
|
logger.Error("Not purging anything!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keepTagsFromFile = gjson.ParseBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
dryRunText := ""
|
dryRunText := ""
|
||||||
if purgeDryRun {
|
if config.DryRun {
|
||||||
logger.Warn("Dry-run mode enabled.")
|
logger.Warn("Dry-run mode enabled.")
|
||||||
dryRunText = "skipped"
|
dryRunText = "skipped"
|
||||||
}
|
}
|
||||||
@@ -53,10 +87,10 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTags
|
|||||||
}
|
}
|
||||||
|
|
||||||
tags := client.Tags(repo)
|
tags := client.Tags(repo)
|
||||||
logger.Infof("[%s] scanning %d tags...", repo, len(tags))
|
|
||||||
if len(tags) == 0 {
|
if len(tags) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
logger.Infof("[%s] scanning %d tags...", repo, len(tags))
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
_, infoV1, _ := client.TagInfo(repo, tag, true)
|
_, infoV1, _ := client.TagInfo(repo, tag, true)
|
||||||
if infoV1 == "" {
|
if infoV1 == "" {
|
||||||
@@ -70,23 +104,35 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTags
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("Scanned %d repositories.", count)
|
logger.Infof("Scanned %d repositories.", count)
|
||||||
logger.Info("Filtering out tags for purging...")
|
logger.Infof("Filtering out tags for purging: keep %d days, keep count %d", config.KeepDays, config.KeepMinCount)
|
||||||
|
if config.KeepTagRegexp != "" {
|
||||||
|
logger.Infof("Keeping tags matching regexp: %s", config.KeepTagRegexp)
|
||||||
|
}
|
||||||
|
if config.KeepFromFile != "" {
|
||||||
|
logger.Infof("Keeping tags for repos from the file: %+v", keepTagsFromFile)
|
||||||
|
}
|
||||||
purgeTags := map[string][]string{}
|
purgeTags := map[string][]string{}
|
||||||
keepTags := map[string][]string{}
|
keepTags := map[string][]string{}
|
||||||
count = 0
|
count = 0
|
||||||
for _, repo := range SortedMapKeys(repos) {
|
for _, repo := range SortedMapKeys(repos) {
|
||||||
// Sort tags by "created" from newest to oldest.
|
// Sort tags by "created" from newest to oldest.
|
||||||
sortedTags := make(timeSlice, 0, len(repos[repo]))
|
sort.Sort(repos[repo])
|
||||||
for _, d := range repos[repo] {
|
|
||||||
sortedTags = append(sortedTags, d)
|
|
||||||
}
|
|
||||||
sort.Sort(sortedTags)
|
|
||||||
repos[repo] = sortedTags
|
|
||||||
|
|
||||||
// Filter out tags by retention days.
|
// Prep the list of tags to preserve if defined in the file
|
||||||
|
tagsFromFile := []string{}
|
||||||
|
for _, i := range keepTagsFromFile.Get(repo).Array() {
|
||||||
|
tagsFromFile = append(tagsFromFile, i.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out tags
|
||||||
for _, tag := range repos[repo] {
|
for _, tag := range repos[repo] {
|
||||||
delta := int(now.Sub(tag.created).Hours() / 24)
|
daysOld := int(now.Sub(tag.created).Hours() / 24)
|
||||||
if delta > purgeTagsKeepDays {
|
keepByRegexp := false
|
||||||
|
if config.KeepTagRegexp != "" {
|
||||||
|
keepByRegexp, _ = regexp.MatchString(config.KeepTagRegexp, tag.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if daysOld > config.KeepDays && !keepByRegexp && !ItemInSlice(tag.name, tagsFromFile) {
|
||||||
purgeTags[repo] = append(purgeTags[repo], tag.name)
|
purgeTags[repo] = append(purgeTags[repo], tag.name)
|
||||||
} else {
|
} else {
|
||||||
keepTags[repo] = append(keepTags[repo], tag.name)
|
keepTags[repo] = append(keepTags[repo], tag.name)
|
||||||
@@ -94,14 +140,11 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTags
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Keep minimal count of tags no matter how old they are.
|
// Keep minimal count of tags no matter how old they are.
|
||||||
if len(repos[repo])-len(purgeTags[repo]) < purgeTagsKeepCount {
|
if len(keepTags[repo]) < config.KeepMinCount {
|
||||||
if len(purgeTags[repo]) > purgeTagsKeepCount {
|
// At least "threshold"-"keep" but not more than available for "purge".
|
||||||
keepTags[repo] = append(keepTags[repo], purgeTags[repo][:purgeTagsKeepCount]...)
|
takeFromPurge := int(math.Min(float64(config.KeepMinCount-len(keepTags[repo])), float64(len(purgeTags[repo]))))
|
||||||
purgeTags[repo] = purgeTags[repo][purgeTagsKeepCount:]
|
keepTags[repo] = append(keepTags[repo], purgeTags[repo][:takeFromPurge]...)
|
||||||
} else {
|
purgeTags[repo] = purgeTags[repo][takeFromPurge:]
|
||||||
keepTags[repo] = append(keepTags[repo], purgeTags[repo]...)
|
|
||||||
delete(purgeTags, repo)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
count = count + len(purgeTags[repo])
|
count = count + len(purgeTags[repo])
|
||||||
@@ -116,8 +159,11 @@ func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTags
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, repo := range SortedMapKeys(purgeTags) {
|
for _, repo := range SortedMapKeys(purgeTags) {
|
||||||
|
if len(purgeTags[repo]) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
logger.Infof("[%s] Purging %d tags... %s", repo, len(purgeTags[repo]), dryRunText)
|
logger.Infof("[%s] Purging %d tags... %s", repo, len(purgeTags[repo]), dryRunText)
|
||||||
if purgeDryRun {
|
if config.DryRun {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, tag := range purgeTags[repo] {
|
for _, tag := range purgeTags[repo] {
|
||||||
|
@@ -21,7 +21,7 @@ type Template struct {
|
|||||||
func (r *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
func (r *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
||||||
t, err := r.View.GetTemplate(name)
|
t, err := r.View.GetTemplate(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("Fatal error template file: %s", err))
|
panic(fmt.Errorf("fatal error template file: %s", err))
|
||||||
}
|
}
|
||||||
vars, ok := data.(jet.VarMap)
|
vars, ok := data.(jet.VarMap)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -29,7 +29,7 @@ func (r *Template) Render(w io.Writer, name string, data interface{}, c echo.Con
|
|||||||
}
|
}
|
||||||
err = t.Execute(w, vars, nil)
|
err = t.Execute(w, vars, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("Error rendering template %s: %s", name, err))
|
panic(fmt.Errorf("error rendering template %s: %s", name, err))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -14,16 +14,18 @@
|
|||||||
<div style="float: left">
|
<div style="float: left">
|
||||||
<h2><a href="{{ basePath }}/" style="text-decoration: none">Docker Registry UI</a></h2>
|
<h2><a href="{{ basePath }}/" style="text-decoration: none">Docker Registry UI</a></h2>
|
||||||
</div>
|
</div>
|
||||||
|
{{if eventsAllowed}}
|
||||||
<div style="float: right">
|
<div style="float: right">
|
||||||
<h4><a href="{{ basePath }}/events">Event Log</a></h4>
|
<h4><a href="{{ basePath }}/events">Event Log</a></h4>
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}
|
||||||
<div style="clear: both"></div>
|
<div style="clear: both"></div>
|
||||||
|
|
||||||
{{yield body()}}
|
{{yield body()}}
|
||||||
|
|
||||||
<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-2022 <a href="https://quiq.com">Quiq Inc.</a>
|
Docker Registry UI v{{version}} © by <a href="https://quiq.com">Quiq Inc.</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -20,6 +20,7 @@
|
|||||||
<li class="active">Event Log</li>
|
<li class="active">Event Log</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
{{if eventsAllowed}}
|
||||||
<table id="datatable" class="table table-striped table-bordered">
|
<table id="datatable" class="table table-striped table-bordered">
|
||||||
<thead bgcolor="#ddd">
|
<thead bgcolor="#ddd">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -46,4 +47,9 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{{else}}
|
||||||
|
<div class="text-center">
|
||||||
|
<h4>User "{{user}}" is not permitted to view the Event Log.</h4>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -59,6 +59,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
{{if eventsAllowed}}
|
||||||
<h4>Latest events on this repo</h4>
|
<h4>Latest events on this repo</h4>
|
||||||
<table id="datatable_log" class="table table-striped table-bordered">
|
<table id="datatable_log" class="table table-striped table-bordered">
|
||||||
<thead bgcolor="#ddd">
|
<thead bgcolor="#ddd">
|
||||||
@@ -86,5 +87,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
const version = "0.9.4"
|
const version = "0.9.5"
|
||||||
|
189
web.go
Normal file
189
web.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/CloudyKit/jet"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/quiq/docker-registry-ui/registry"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
const usernameHTTPHeader = "X-WEBAUTH-USER"
|
||||||
|
|
||||||
|
func (a *apiClient) setUserPermissions(c echo.Context) jet.VarMap {
|
||||||
|
user := c.Request().Header.Get(usernameHTTPHeader)
|
||||||
|
|
||||||
|
data := jet.VarMap{}
|
||||||
|
data.Set("user", user)
|
||||||
|
data.Set("eventsAllowed", a.config.AnyoneCanViewEvents || registry.ItemInSlice(user, a.config.Admins))
|
||||||
|
data.Set("deleteAllowed", a.config.AnyoneCanDelete || registry.ItemInSlice(user, a.config.Admins))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiClient) viewRepositories(c echo.Context) error {
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
if namespace == "" {
|
||||||
|
namespace = "library"
|
||||||
|
}
|
||||||
|
|
||||||
|
repos := a.client.Repositories(true)[namespace]
|
||||||
|
data := a.setUserPermissions(c)
|
||||||
|
data.Set("namespace", namespace)
|
||||||
|
data.Set("namespaces", a.client.Namespaces())
|
||||||
|
data.Set("repos", repos)
|
||||||
|
data.Set("tagCounts", a.client.TagCounts())
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "repositories.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiClient) viewTags(c echo.Context) error {
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
repo := c.Param("repo")
|
||||||
|
repoPath := repo
|
||||||
|
if namespace != "library" {
|
||||||
|
repoPath = fmt.Sprintf("%s/%s", namespace, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := a.client.Tags(repoPath)
|
||||||
|
|
||||||
|
data := a.setUserPermissions(c)
|
||||||
|
data.Set("namespace", namespace)
|
||||||
|
data.Set("repo", repo)
|
||||||
|
data.Set("tags", tags)
|
||||||
|
repoPath, _ = url.PathUnescape(repoPath)
|
||||||
|
data.Set("events", a.eventListener.GetEvents(repoPath))
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "tags.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiClient) viewTagInfo(c echo.Context) error {
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
repo := c.Param("repo")
|
||||||
|
tag := c.Param("tag")
|
||||||
|
repoPath := repo
|
||||||
|
if namespace != "library" {
|
||||||
|
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)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if gjson.Get(infoV2, "layers").Exists() {
|
||||||
|
for _, s := range gjson.Get(infoV2, "layers.#.size").Array() {
|
||||||
|
imageSize = imageSize + s.Int()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, s := range gjson.Get(infoV2, "history.#.v1Compatibility").Array() {
|
||||||
|
imageSize = imageSize + gjson.Get(s.String(), "Size").Int()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count layers
|
||||||
|
layersCount := len(layersV2)
|
||||||
|
if layersCount == 0 {
|
||||||
|
layersCount = len(gjson.Get(infoV1, "fsLayers").Array())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather sub-image info of multi-arch or cache 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" {
|
||||||
|
// Sub-image of the specific arch.
|
||||||
|
_, 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
|
||||||
|
// Create link here because there is a bug with jet template when referencing a value by map key in the "if" condition under "range".
|
||||||
|
if r["mediaType"] == "application/vnd.docker.distribution.manifest.v2+json" {
|
||||||
|
r["digest"] = fmt.Sprintf(`<a href="%s/%s/%s/%s">%s</a>`, a.config.BasePath, namespace, repo, r["digest"], r["digest"])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sub-image of the cache type.
|
||||||
|
r["size"] = s.Get("size").Int()
|
||||||
|
}
|
||||||
|
r["ordered_keys"] = registry.SortedMapKeys(r)
|
||||||
|
digestList = append(digestList, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate template vars
|
||||||
|
data := a.setUserPermissions(c)
|
||||||
|
data.Set("namespace", namespace)
|
||||||
|
data.Set("repo", repo)
|
||||||
|
data.Set("tag", tag)
|
||||||
|
data.Set("repoPath", repoPath)
|
||||||
|
data.Set("sha256", sha256)
|
||||||
|
data.Set("imageSize", imageSize)
|
||||||
|
data.Set("created", created)
|
||||||
|
data.Set("layersCount", layersCount)
|
||||||
|
data.Set("layersV2", layersV2)
|
||||||
|
data.Set("layersV1", layersV1)
|
||||||
|
data.Set("isDigest", isDigest)
|
||||||
|
data.Set("digestList", digestList)
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "tag_info.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *apiClient) deleteTag(c echo.Context) error {
|
||||||
|
namespace := c.Param("namespace")
|
||||||
|
repo := c.Param("repo")
|
||||||
|
tag := c.Param("tag")
|
||||||
|
repoPath := repo
|
||||||
|
if namespace != "library" {
|
||||||
|
repoPath = fmt.Sprintf("%s/%s", namespace, repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := a.setUserPermissions(c)
|
||||||
|
if data["deleteAllowed"].Bool() {
|
||||||
|
a.client.DeleteTag(repoPath, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%s/%s/%s", a.config.BasePath, namespace, repo))
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewLog view events from sqlite.
|
||||||
|
func (a *apiClient) viewLog(c echo.Context) error {
|
||||||
|
data := a.setUserPermissions(c)
|
||||||
|
data.Set("events", a.eventListener.GetEvents(""))
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "event_log.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiveEvents receive events.
|
||||||
|
func (a *apiClient) receiveEvents(c echo.Context) error {
|
||||||
|
a.eventListener.ProcessEvents(c.Request())
|
||||||
|
return c.String(http.StatusOK, "OK")
|
||||||
|
}
|
Reference in New Issue
Block a user