mirror of
https://github.com/Quiq/docker-registry-ui.git
synced 2025-07-18 00:01:20 +00:00
Implement purge_tags_keep_from_file option. Refactoring.
This commit is contained in:
parent
2aa58fc9ba
commit
6d0fc3d913
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
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 0.9.5 (unreleased)
|
||||||
|
|
||||||
|
* Upgrade Go version to 1.19.0, alpine to 3.16 and other dependencies.
|
||||||
|
* Added an option `purge_tags_keep_regexp` to preserve tags based on regexp
|
||||||
|
* Added an option `purge_tags_keep_from_file` to preserve tags for repos listed in the file provided
|
||||||
|
* Fix for a bug when there was a bit more tags preserved than defined by `purge_tags_keep_count`
|
||||||
|
|
||||||
### 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.
|
||||||
|
@ -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
|
||||||
|
93
config.go
Normal file
93
config.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/quiq/docker-registry-ui/registry"
|
||||||
|
"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"`
|
||||||
|
PurgeTagsKeepRegexp string `yaml:"purge_tags_keep_regexp"`
|
||||||
|
PurgeTagsKeepFile 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.
|
||||||
|
if config.BasePath != "" {
|
||||||
|
config.BasePath = "/" + strings.Trim(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,
|
||||||
|
}
|
||||||
|
if config.PurgeTagsKeepFile != "" {
|
||||||
|
if _, err := os.Stat(config.PurgeTagsKeepFile); os.IsNotExist(err) {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
data, err := ioutil.ReadFile(config.PurgeTagsKeepFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
config.PurgeConfig.KeepTagsFromFile = gjson.ParseBytes(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config
|
||||||
|
}
|
14
config.yml
14
config.yml
@ -32,7 +32,7 @@ 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.
|
||||||
@ -50,8 +50,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
|
|
||||||
# purge_tags_keep_regexp: '^latest$'
|
# 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.
|
||||||
|
277
main.go
277
main.go
@ -3,54 +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"`
|
|
||||||
PurgeTagsKeepRegexp string `yaml:"purge_tags_keep_regexp"`
|
|
||||||
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() {
|
||||||
@ -66,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)
|
||||||
@ -115,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()
|
||||||
}
|
}
|
||||||
@ -144,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")
|
||||||
@ -170,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, a.config.PurgeTagsKeepRegexp)
|
|
||||||
}
|
|
||||||
|
@ -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,6 +2,7 @@ package registry
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
@ -9,6 +10,14 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type PurgeTagsConfig struct {
|
||||||
|
DryRun bool
|
||||||
|
KeepDays int
|
||||||
|
KeepMinCount int
|
||||||
|
KeepTagRegexp string
|
||||||
|
KeepTagsFromFile gjson.Result
|
||||||
|
}
|
||||||
|
|
||||||
type tagData struct {
|
type tagData struct {
|
||||||
name string
|
name string
|
||||||
created time.Time
|
created time.Time
|
||||||
@ -37,10 +46,10 @@ func (p timeSlice) Swap(i, j int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PurgeOldTags purge old tags.
|
// PurgeOldTags purge old tags.
|
||||||
func PurgeOldTags(client *Client, purgeDryRun bool, purgeTagsKeepDays, purgeTagsKeepCount int, purgeTagsKeepRegexp string) {
|
func PurgeOldTags(client *Client, config *PurgeTagsConfig) {
|
||||||
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
logger := SetupLogging("registry.tasks.PurgeOldTags")
|
||||||
dryRunText := ""
|
dryRunText := ""
|
||||||
if purgeDryRun {
|
if config.DryRun {
|
||||||
logger.Warn("Dry-run mode enabled.")
|
logger.Warn("Dry-run mode enabled.")
|
||||||
dryRunText = "skipped"
|
dryRunText = "skipped"
|
||||||
}
|
}
|
||||||
@ -75,27 +84,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.KeepTagsFromFile.IsObject() {
|
||||||
|
logger.Infof("Keeping tags for repos from the file: %+v", config.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 and regexp
|
// Prep the list of tags to preserve if defined in the file
|
||||||
|
tagsFromFile := []string{}
|
||||||
|
for _, i := range config.KeepTagsFromFile.Get(repo).Array() {
|
||||||
|
tagsFromFile = append(tagsFromFile, i.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out tags
|
||||||
for _, tag := range repos[repo] {
|
for _, tag := range repos[repo] {
|
||||||
regexpKeep := false
|
daysOld := int(now.Sub(tag.created).Hours() / 24)
|
||||||
if purgeTagsKeepRegexp != "" {
|
keepByRegexp := false
|
||||||
regexpKeep, _ = regexp.MatchString(purgeTagsKeepRegexp, tag.name)
|
if config.KeepTagRegexp != "" {
|
||||||
|
keepByRegexp, _ = regexp.MatchString(config.KeepTagRegexp, tag.name)
|
||||||
}
|
}
|
||||||
delta := int(now.Sub(tag.created).Hours() / 24)
|
|
||||||
if !regexpKeep && delta > purgeTagsKeepDays {
|
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)
|
||||||
@ -103,14 +120,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])
|
||||||
@ -125,8 +139,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
|
||||||
}
|
}
|
||||||
|
@ -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-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>
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
const version = "0.9.4"
|
const version = "0.9.5"
|
||||||
|
192
web.go
Normal file
192
web.go
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user