Compare commits

..

24 Commits
1.2.1 ... 1.3.0

Author SHA1 Message Date
Joxit
a0c88d06f0 Release v1.3.0: pagination in taglist and indent aggregated entries 2019-07-18 01:13:57 +02:00
Joxit
eec736d4e8 feat: Indent aggregated entries after expansion
closes: #90
2019-07-17 23:59:59 +02:00
Joxit
9e99b08b82 fix: delete toggle button when delete is disabled 2019-07-06 11:59:59 +02:00
Jones Magloire
0f805daafa Merge pull request #86 from Joxit/feat/36-pagination
[Feat #36] Pagination in taglist

## UI update

![image](https://user-images.githubusercontent.com/5153882/59188702-8da13700-8b78-11e9-87ff-64d1b6c8d380.png)

resolves #36 

cc @madhukar93 @gionn
2019-07-05 00:11:23 +02:00
Joxit
8bd1f31c9c Release v1.2.3: Custom header supports 2019-07-04 23:09:11 +02:00
Jones Magloire
7777ff28df Merge pull request #89 from Joxit/custom-headers
Supports custom headers when the ui is used as proxy

## Background

Headers can be useful in some cases such as avoid sending credentials when you are on the UI (like #87). Or give to the registry server other properties such as `X-Forward-For` or `Server` headers for monitoring.

## How to use ?

This is compatible only with static version of the UI and used with `REGISTRY_URL` variable.
When you want to add a custom header, add to the registry ui a environment variable or entry in `/etc/nginx/.env` which looks like `NGINX_PROXY_HEADER_Custom_Header`. All underscores (`_`) will be replaced by hyphens (`-`). 

Some example of custom headers as variable:
- `NGINX_PROXY_HEADER_Authorization` for Basic auth credentials
- `NGINX_PROXY_HEADER_X_Forwarded_For` for identifying the originating IP address of a client

An example is bundled with this PR

closes: #87
2019-07-03 00:09:18 +02:00
Joxit
4fee7b44d3 feat: Supports custom headers via file /etc/nginx/.env
Remove the print of headers for security
2019-07-02 23:04:32 +02:00
Joxit
79960ea52d fix(pagination): page switch and regression on multi delete 2019-06-27 23:54:20 +02:00
Joxit
7716f8b44a feat: Supports custom headers when the ui is used as proxy 2019-06-24 23:54:21 +02:00
Joxit
0ac7a151d9 fix(pagination): Wrong calcul for num pages 2019-06-23 22:55:32 +02:00
Joxit
1321d9b573 fix: Unable to push image on non 80 port
resolves: #88
2019-06-22 10:47:37 +02:00
Joxit
ef149bf1cc chore: Memory optimization when delete is not activated 2019-06-17 00:46:46 +02:00
Joxit
e5a406a6ba fix(pagination): Reset the number of selected tags to delete when the page is updated 2019-06-16 23:07:54 +02:00
Joxit
dbb746981a fix(pagination): Improve spacing for page next and first page buttons 2019-06-15 22:37:24 +02:00
Sébastien Huss
d7a19734ce Kubernetes support (#85)
* Added a kubernetes example
* Added an helm chart for kubernetes usage
* Added README for the added examples
2019-06-07 00:17:30 +02:00
Joxit
92fc37adb4 feat(pagination): Add handler to pagination buttons 2019-06-04 22:06:14 +02:00
Joxit
02210e0943 fix(pagination): getPage doesn't work for page 2 and now support query param 2019-06-03 01:39:23 +02:00
Joxit
0199f87087 feat(pagination): Identify the current page 2019-05-31 00:44:29 +02:00
Joxit
3399030e4e feat(pagination): Add pagination component with its style 2019-05-30 00:11:05 +02:00
Joxit
660a938d6e feat(pagination): Small UI improvements 2019-05-27 22:23:24 +02:00
Joxit
7356591292 feat(pagination): Show only 100 first elements 2019-05-26 01:20:02 +02:00
Joxit
32d0df1af9 New script utils for global static functions 2019-05-21 00:18:24 +02:00
Joxit
5c1cb93a1c Add badges to README 2019-05-11 12:09:34 +02:00
Jakob Ackermann
9d97c30914 [fonts] fix bad references on the Material Icons font
Signed-off-by: Jakob Ackermann <das7pad@outlook.com>
2019-05-03 22:57:37 +02:00
54 changed files with 1412 additions and 164 deletions

View File

@@ -4,6 +4,9 @@ title: Project Page
# Docker Registry UI
![Stars](https://img.shields.io/github/stars/joxit/docker-registry-ui.svg?logo=github&maxAge=86400)
![Pulls](https://img.shields.io/docker/pulls/joxit/docker-registry-ui.svg?maxAge=86400)
## Overview
This project aims to provide a simple and complete user interface for your private docker registry.
@@ -17,7 +20,7 @@ In the **static interface**, it will connect to a single registry and will not c
This web user interface uses [Riot](https://github.com/Riot/riot) the react-like user interface micro-library and [riot-mui](https://github.com/kysonic/riot-mui) components.
## [Project Page](https://joxit.dev/docker-registry-ui) and [Live Demo](https://joxit.dev/docker-registry-ui/demo/)
## [Project Page](https://joxit.dev/docker-registry-ui), [Live Demo](https://joxit.dev/docker-registry-ui/demo/) [Examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples)
![preview](https://raw.github.com/Joxit/docker-registry-ui/master/docker-registry-ui.gif "Preview of Docker Registry UI")
@@ -42,6 +45,7 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
- Use `joxit/docker-registry-ui:static` as reverse proxy (with `REGISTRY_URL` environment variable) to your docker registry (This will avoid CORS) **static interface**.
- Add Title when using `REGISTRY_URL` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)) **static interface**.
- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)) **static interface**.
- Add custom header via environment variable and file (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)) **static interface**
## Getting Started
@@ -214,6 +218,8 @@ auth:
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone)
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/master/examples/traefik)
- [Use docker-registry-ui with docker registry and Amazon s3 (#75)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75)
- [FIX revproxy to registry does not work when published under non-root url (#73)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73)
- [Use docker-registry-ui with HTTPS (#20)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20)
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/88))
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))

View File

@@ -1,5 +1,5 @@
#!/bin/sh
$@
sed -i "s,\${URL},${URL}," scripts/docker-registry-ui.js
sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," scripts/docker-registry-ui.js
sed -i "s,\${PULL_URL},${PULL_URL}," scripts/docker-registry-ui.js
@@ -8,13 +8,31 @@ if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
sed -i -r "s/(isImageRemoveActivated[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
fi
get_nginx_proxy_headers() {
(
env &&
if [ -f "/etc/nginx/.env" ]; then
cat /etc/nginx/.env
# Force new line
echo ""
fi
) | while read e; do
if [ -n "$(echo $e | grep -o '^NGINX_PROXY_HEADER_')" ]; then
key=$(echo ${e%%=*} | sed 's/^NGINX_PROXY_HEADER_//' | sed 's/_/-/g')
value=${e#*=}
echo -n "proxy_set_header ${key} \"${value}\"; "
fi
done
}
if [ -n "${REGISTRY_URL}" ] ; then
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," /etc/nginx/conf.d/default.conf
sed -i "s^\${NGINX_PROXY_HEADERS}^$(get_nginx_proxy_headers)^" /etc/nginx/conf.d/default.conf
sed -i "s,#!,," /etc/nginx/conf.d/default.conf
fi
if [ -z "$@" ]; then
nginx -g "daemon off;"
exec nginx -g "daemon off;"
else
$@
exec $@
fi

13
bin/fill-registry Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
for i in alpine chronograf:alpine consul debian jawg/mapnik3 nginx:alpine postgres:alpine redis:alpine telegraf:alpine joxit/docker-registry-ui joxit/kosmtik joxit/node joxit/rust-openssl joxit/tile-server-ui; do
docker pull $i
docker tag $i 127.0.0.1:5000/$i
docker push 127.0.0.1:5000/$i
done
for i in arm32v7-static 1.2-debian-static master-static 1.2 arm64v8 arm32v7 arm64v8-static master 1.2-debian latest static debian-static debian 1.2-static 1.1 1.1-static 1.1-debian-static 1.1-debian ; do
docker pull joxit/docker-registry-ui:$i
docker tag joxit/docker-registry-ui:$i 127.0.0.1:5000/joxit/docker-registry-ui:$i
docker push 127.0.0.1:5000/joxit/docker-registry-ui:$i
done

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
dist/style.css vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 764 KiB

After

Width:  |  Height:  |  Size: 686 KiB

10
examples/README.md Normal file
View File

@@ -0,0 +1,10 @@
## Examples
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy)
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone)
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/master/examples/traefik)
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/88))
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-73) ([#73](https://github.com/Joxit/docker-registry-ui/issues/73))
- [Use docker-registry-ui with HTTPS](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-20) ([#20](https://github.com/Joxit/docker-registry-ui/issues/20))
- [Unable to push image when docker-registry-ui is used as a proxy on non 80 port](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-88) ([#88](https://github.com/Joxit/docker-registry-ui/issues/88))
- [Add custom headers bases on environment variable and/or file when the ui is used as proxy](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))

View File

@@ -0,0 +1,22 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -0,0 +1,11 @@
apiVersion: v1
appVersion: "1.2.1"
description: The simplest and most complete UI for your private registry
name: docker-registry-ui
home: https://github.com/Joxit/docker-registry-ui
keywords:
- docker
- registry
sources:
- https://github.com/Joxit/docker-registry-ui
version: 0.1.0

View File

@@ -0,0 +1,97 @@
# docker-registry-ui
[docker-registry-ui](https://joxit.dev/docker-registry-ui/) is the simplest and most complete UI for your private registry!
## TL;DR;
```bash
$ helm install .
```
## Introduction
This chart bootstraps a [docker-registry-ui](https://joxit.dev/docker-registry-ui/) deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
It also may deploy the [docker registry](https://docs.docker.com/registry/) if you havent have one already.
## Prerequisites
- Kubernetes 1.9+ with Beta APIs enabled
- PV provisioner support in the underlying infrastructure
## Installing the Chart
To install the chart with the release name `my-release`:
```bash
$ helm update --install my-release .
```
The command deploys docker-registry-ui on the Kubernetes cluster in the default configuration. The [configuration](#configuration) section lists the parameters that can be configured during installation.
> **Tip**: List all releases using `helm list`
## Uninstalling the Chart
To uninstall/delete the `my-release` deployment:
```bash
$ helm delete my-release
```
The command removes all the Kubernetes components associated with the chart and deletes the release.
## Configuration
The following table lists the configurable parameters of the Redmine chart and their default values.
| Parameter | Description | Default |
| --------------------------------- | ---------------------------------------- | ------------------------------------------------------- |
| `ui.title` | Title of the managed repository | `Docker registry UI` |
| `ui.delete_images` | Allow to delete image from the front-end | `false` |
| `ui.proxy` | The UI service act as a proxy of the registry | `true` |
| `ui.replicaCount` | Number of replicas to start | `1` |
| `ui.image.registry` | registry to pull the docker-registry-ui image from | `docker.io` |
| `ui.image.repository` | docker-registry-ui image name | `joxit/docker-registry-ui` |
| `ui.image.tag` | docker-registry-ui image tag (change to latest to have multi registry support) | `static` |
| `ui.image.pullPolicy` | docker-registry-ui image pull policy | `Always` |
| `ui.probe.liveness` | Ask kubernetes to check the service port for liveness | `true` |
| `ui.probe.readyness ` | Ask kubernetes to check the service port for readyness | `true` |
| `ui.service.type` | Desired service type | `ClusterIP` |
| `ui.service.port` | Service exposed port | `80` |
| `ui.ingress.enabled` | Create an ingress for docker-regstry-ui | `false` |
| `registry.external` | Use an already available registry | `false` |
| `registry.url` | URL of the existing registry | `http://localhost:5000` |
| `registry.replicaCount` | Number of replicas to start | `1` |
| `registry.image.registry` | registry to pull the docker-registry image from | `docker.io` |
| `registry.image.repository` | docker-registry-ui image name | `registry` |
| `registry.image.tag` | docker-registry-ui image tag | `2.6.2` |
| `registry.image.pullPolicy` | docker-registry-ui image pull policy | `Always` |
| `registry.probe.liveness` | Ask kubernetes to check the service port for liveness | `true` |
| `registry.probe.readyness ` | Ask kubernetes to check the service port for readyness | `true` |
| `registry.persistence.enabled` | Enable persistence using PVC for the registry | `false` |
| `registry.persistence.storageClass` | PVC Storage Class | `-` |
| `registry.persistence.size` | PVC Storage Request size | `1Gi` |
| `registry.service.type` | Desired service type | `ClusterIP` |
| `registry.service.port` | Service exposed port | `5000` |
| `registry.ingress.enabled` | Create an ingress for the regstry | `false` |
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example,
```bash
$ helm upgrade --install my-release \
--set registry.external=true \
--set registry.url=http://registry.example.com:5000 \
.
```
Alternatively, a YAML file that specifies the values for the above parameters can be provided while installing the chart. For example,
```bash
$ helm upgrade --install my-release -f values.yaml .
```
> **Tip**: You can use the default [values.yaml](values.yaml)

View File

@@ -0,0 +1,147 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "docker-registry-ui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "docker-registry-ui.fullname" -}}
{{- if .Values.ui.fullnameOverride -}}
{{- .Values.ui.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- printf "%s-ui" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-ui-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- define "docker-registry.fullname" -}}
{{- if .Values.registry.fullnameOverride -}}
{{- .Values.registry.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- printf "%s-registry" .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-registry-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "docker-registry-ui.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Common labels
*/}}
{{- define "docker-registry-ui.labels" -}}
app: registry-ui
chart: {{ include "docker-registry-ui.chart" . }}
release: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- end -}}
{{- define "docker-registry-ui.matchLabels" -}}
app: registry-ui
release: {{ .Release.Name }}
{{- end -}}
{{- define "docker-registry.labels" -}}
app: registry
chart: {{ include "docker-registry-ui.chart" . }}
release: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app/version: {{ .Chart.AppVersion | quote }}
{{- end }}
{{- end -}}
{{- define "docker-registry.matchLabels" -}}
app: registry
release: {{ .Release.Name }}
{{- end -}}
{{- define "docker-registry-ui.probes" -}}
{{- if and .Values.ui.probe.liveness (eq .Values.ui.probe.liveness true) -}}
livenessProbe:
httpGet:
path: /
port: http
{{- end -}}
{{- if and .Values.ui.probe.readiness (eq .Values.ui.probe.readiness true) }}
readinessProbe:
httpGet:
path: /
port: http
{{- end -}}
{{- end -}}
{{- define "docker-registry.probes" -}}
{{- if and .Values.registry.probe.liveness (eq .Values.registry.probe.liveness true) -}}
livenessProbe:
httpGet:
path: /v2/
port: registry
{{- end -}}
{{- if and .Values.registry.probe.readiness (eq .Values.registry.probe.readiness true) }}
readinessProbe:
httpGet:
path: /v2/
port: registry
{{- end -}}
{{- end -}}
{{- define "docker-registry-ui.url-name" -}}
{{- if eq .Values.ui.proxy true -}}
REGISTRY_URL
{{- else -}}
URL
{{- end -}}
{{- end -}}
{{- define "docker-registry-ui.url-value" -}}
{{- if eq .Values.registry.external true -}}
{{ .Values.registry.url }}
{{- else -}}
{{- $fullName := include "docker-registry.fullname" . -}}
{{ printf "http://%s.%s:%.0f" $fullName .Release.Namespace .Values.registry.service.port }}
{{- end -}}
{{- end -}}
{{- define "docker-registry-ui.pull" -}}
{{- if eq .Values.registry.external true -}}
{{ .Values.registry.url }}
{{- else -}}
{{- if eq .Values.ui.proxy true -}}
{{- if eq .Values.ui.ingress.enabled true -}}
{{- $host := index .Values.ui.ingress.hosts 0 -}}
{{ $host.host }}
{{- else -}}
{{- $fullName := include "docker-registry-ui.fullname" . -}}
{{ printf "%s.%s:%.0f" $fullName .Release.Namespace .Values.ui.service.port }}
{{- end -}}
{{- else -}}
{{- if eq .Values.registry.ingress.enabled true -}}
{{- $host := index .Values.registry.ingress.hosts 0 -}}
{{ $host.host }}
{{- else -}}
{{- $fullName := include "docker-registry.fullname" . -}}
{{ printf "%s.%s:%.0f" $fullName .Release.Namespace .Values.registry.service.port }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,31 @@
{{- if eq .Values.registry.external false -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "docker-registry.fullname" . }}
labels:
{{ include "docker-registry.labels" . | indent 4 }}
data:
config.yml: |-
version: 0.1
log:
fields:
service: registry
storage:
delete:
enabled: true
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ['*']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization']
Access-Control-Max-Age: [1728000]
Access-Control-Allow-Credentials: [true]
Access-Control-Expose-Headers: ['Docker-Content-Digest']
{{- end -}}

View File

@@ -0,0 +1,62 @@
{{- if eq .Values.registry.external false -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "docker-registry.fullname" . }}
labels:
{{ include "docker-registry.labels" . | indent 4 }}
spec:
replicas: {{ .Values.registry.replicaCount }}
selector:
matchLabels:
{{ include "docker-registry.matchLabels" . | indent 6 }}
template:
metadata:
labels:
{{ include "docker-registry.matchLabels" . | indent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: config
configMap:
defaultMode: 420
name: {{ include "docker-registry.fullname" . }}
- name: data
{{- if .Values.registry.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "docker-registry.fullname" . }}
{{- else }}
emptyDir: {}
{{- end }}
containers:
- name: registry
image: "{{ .Values.registry.image.registry }}/{{ .Values.registry.image.repository }}:{{ .Values.registry.image.tag }}"
imagePullPolicy: {{ .Values.registry.image.pullPolicy }}
ports:
- name: registry
containerPort: 5000
protocol: TCP
volumeMounts:
- mountPath: "/var/lib/registry"
name: "data"
- mountPath: "/etc/docker/registry"
name: "config"
{{ include "docker-registry.probes" . | indent 10 }}
resources:
{{- toYaml .Values.registry.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end -}}

View File

@@ -0,0 +1,34 @@
{{- if and (eq .Values.registry.external false) (and (eq .Values.ui.proxy false) .Values.registry.ingress.enabled) -}}
{{- $fullName := include "docker-registry.fullname" . -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{ include "docker-registry.labels" . | indent 4 }}
{{- with .Values.registry.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.registry.ingress.tls }}
tls:
{{- range .Values.registry.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.registry.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
- path: /
backend:
serviceName: {{ $fullName }}
servicePort: registry
{{- end }}
{{- end -}}

View File

@@ -0,0 +1,23 @@
{{- if and (eq .Values.registry.external false) .Values.registry.persistence.enabled -}}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
{{ include "docker-registry.labels" . | indent 4 }}
name: {{ include "docker-registry.fullname" . }}
spec:
accessModes:
{{- range .Values.registry.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.registry.persistence.size }}
{{- if .Values.registry.persistence.storageClass }}
{{- if (eq "-" .Values.registry.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.registry.persistence.storageClass | quote }}
{{- end }}
{{- end }}
{{- end -}}

View File

@@ -0,0 +1,17 @@
{{- if eq .Values.registry.external false -}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "docker-registry.fullname" . }}
labels:
{{ include "docker-registry.labels" . | indent 4 }}
spec:
type: {{ .Values.registry.service.type }}
ports:
- port: {{ .Values.registry.service.port }}
targetPort: registry
protocol: TCP
name: registry
selector:
{{ include "docker-registry.matchLabels" . | indent 6 }}
{{- end -}}

View File

@@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "docker-registry-ui.fullname" . }}
labels:
{{ include "docker-registry-ui.labels" . | indent 4 }}
spec:
replicas: {{ .Values.ui.replicaCount }}
selector:
matchLabels:
{{ include "docker-registry-ui.matchLabels" . | indent 6 }}
template:
metadata:
labels:
{{ include "docker-registry-ui.matchLabels" . | indent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: registry-ui
image: "{{ .Values.ui.image.registry }}/{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag }}"
imagePullPolicy: {{ .Values.ui.image.pullPolicy }}
env:
- name: REGISTRY_TITLE
value: {{ .Values.ui.title| quote }}
- name: DELETE_IMAGES
value: {{ .Values.ui.delete_images| quote }}
- name: {{ include "docker-registry-ui.url-name" . }}
value: {{ include "docker-registry-ui.url-value" . | quote }}
- name: PULL_URL
value: {{ include "docker-registry-ui.pull" . | quote }}
ports:
- name: http
containerPort: 80
protocol: TCP
{{ include "docker-registry-ui.probes" . | indent 10 }}
resources:
{{- toYaml .Values.ui.resources | nindent 12 }}
{{- with .Values.ui.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.ui.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.ui.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,34 @@
{{- if .Values.ui.ingress.enabled -}}
{{- $fullName := include "docker-registry-ui.fullname" . -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{ include "docker-registry-ui.labels" . | indent 4 }}
{{- with .Values.ui.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ui.ingress.tls }}
tls:
{{- range .Values.ui.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ui.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
- path: /
backend:
serviceName: {{ $fullName }}
servicePort: http
{{- end }}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "docker-registry-ui.fullname" . }}
labels:
{{ include "docker-registry-ui.labels" . | indent 4 }}
spec:
type: {{ .Values.ui.service.type }}
ports:
- port: {{ .Values.ui.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{ include "docker-registry-ui.matchLabels" . | indent 6 }}

View File

@@ -0,0 +1,129 @@
# Default values for docker-registry-ui.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
ui:
# title of the registry
title: "Docker registry UI"
# allow delete of images
delete_images: false
# UI behave as a proxy of the registry
proxy: true
replicaCount: 1
image:
registry: docker.io
repository: joxit/docker-registry-ui
tag: static
pullPolicy: Always
probe:
liveness: true
readiness: true
resources: {}
# If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
fullnameOverride: ""
service:
type: ClusterIP
port: 80
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: docker-registry-ui.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
registry:
external: false
# URL of the registry (requiered. Note: this wont work as localhost is inside the container. Only used if the registry is external)
url: http://localhost:5000
replicaCount: 1
# Image definition for the registry (Only used if the registry is not external)
image:
registry: docker.io
repository: registry
tag: 2.6.2
pullPolicy: Always
probe:
liveness: true
readiness: true
resources: {}
# If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
fullnameOverride: ""
persistence:
## If true, use a Persistent Volume Claim, If false, use emptyDir
##
enabled: false
## Persistent Volume Storage Class
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
# storageClass: "-"
## Persistent Volume Claim annotations
##
annotations:
## Persistent Volume Access Mode
##
accessModes:
# This have to be ReadWriteMany if replicaCount>1
- ReadWriteOnce
## Persistent Volume size
##
size: 1Gi
##
service:
type: ClusterIP
port: 5000
ingress:
enabled: false
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
- host: docker-registry.local
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
imagePullSecrets: []
nameOverride: ""

View File

@@ -0,0 +1,4 @@
# Example for issue #88
When the docker registry ui is used as a proxy and its port is not 80, we can't push images.
To fix that, I added the correct Host header in the proxy_set_header.

View File

@@ -0,0 +1,23 @@
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ['*']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Expose-Headers: ['Docker-Content-Digest']
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3

View File

@@ -0,0 +1,34 @@
version: '2'
services:
registry-srv:
image: registry:latest
restart: always
volumes:
- storage:/var/lib/registry
- ./config.yml:/etc/docker/registry/config.yml:ro
networks:
- registry-ui-net
container_name: registry-srv
registry-ui:
image: joxit/docker-registry-ui:static
restart: always
ports:
- 8080:80
environment:
- REGISTRY_TITLE=Private Docker Registry
- REGISTRY_URL=http://registry-srv:5000
- DELETE_IMAGES=true
depends_on:
- debugproxy
networks:
- registry-ui-net
container_name: registry-ui
networks:
registry-ui-net:
volumes:
storage:
driver: local

View File

@@ -0,0 +1,19 @@
# Kubernetes installation of Docker Registry UI
## Full installation
Install a registry and docker-registry-ui as frontend of this registry to kubernetes.
```sh
kubectl apply -f *.yaml
```
Please note that you'll need a PV provisionner to be able to store the uploaded images.
## Dynamic installation
Edit the image tag in the ui-deployement.yaml file and set it to `latest`, then :
```sh
kubectl apply -f ui*.yaml
```
You'll get a docker-registry-ui pod installed inside kubernetes and you'll be able to configure it to act as a frontend to your existing registry(ies).

View File

@@ -0,0 +1,31 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: docker-registry
labels:
app: registry
release: docker-registry-ui
app/version: "1.2.1"
data:
config.yml: |-
version: 0.1
log:
fields:
service: registry
storage:
delete:
enabled: true
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ['*']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization']
Access-Control-Max-Age: [1728000]
Access-Control-Allow-Credentials: [true]
Access-Control-Expose-Headers: ['Docker-Content-Digest']

View File

@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-registry
labels:
app: registry
release: docker-registry-ui
app/version: "1.2.1"
spec:
replicas: 1
selector:
matchLabels:
app: registry
release: docker-registry-ui
template:
metadata:
labels:
app: registry
release: docker-registry-ui
spec:
volumes:
- name: config
configMap:
defaultMode: 420
name: docker-registry
- name: data
persistentVolumeClaim:
claimName: docker-registry
containers:
- name: registry
image: "docker.io/registry:2.6.2"
imagePullPolicy: Always
ports:
- name: registry
containerPort: 5000
protocol: TCP
volumeMounts:
- mountPath: "/var/lib/registry"
name: "data"
- mountPath: "/etc/docker/registry"
name: "config"
livenessProbe:
httpGet:
path: /v2/
port: registry
readinessProbe:
httpGet:
path: /v2/
port: registry
resources:
{}

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
labels:
app: registry
release: docker-registry-ui
app/version: "1.2.1"
name: docker-registry
spec:
accessModes:
- "ReadWriteOnce"
resources:
requests:
storage: 1Gi

View File

@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: docker-registry
labels:
app: registry
release: docker-registry-ui
app/version: "1.2.1"
spec:
type: ClusterIP
ports:
- port: 5000
targetPort: registry
protocol: TCP
name: registry
selector:
app: registry
release: docker-registry-ui

View File

@@ -0,0 +1,48 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-registry-ui
labels:
app: registry-ui
release: docker-registry-ui
app/version: "1.2.1"
spec:
replicas: 1
selector:
matchLabels:
app: registry-ui
release: docker-registry-ui
template:
metadata:
labels:
app: registry-ui
release: docker-registry-ui
spec:
containers:
- name: registry-ui
image: "docker.io/joxit/docker-registry-ui:static"
imagePullPolicy: Always
env:
- name: REGISTRY_TITLE
value: "Docker registry UI"
- name: DELETE_IMAGES
value: "false"
- name: REGISTRY_URL
value: "http://docker-registry.default:5000"
- name: PULL_URL
value: "docker-registry-ui.default:80"
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
resources:
{}

View File

@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: docker-registry-ui
labels:
app: registry-ui
release: docker-registry-ui
app/version: "1.2.1"
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app: registry-ui
release: docker-registry-ui

View File

@@ -0,0 +1,19 @@
# Set custom headers to the registry example
The interface and the docker registry will be accessible with <http://localhost>.
This example highlight the usage of custom headers when the UI is used as a proxy. When you wants to use a header name with hyphens, replace them by underscores in the variable. You can put headers in environment variable or in config file `/etc/nginx/.env`. They have the same writing style.
Headers can be useful in some cases such as avoid sending credentials when you are on the UI. Or give to the registry server other properties such as X-Forward-For header.
I will set these two headers in this example. X-Forward-For by environment variable and Authorization by file.
In order to set your credentials in the header, you need to know how [Authorization](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) header works. Here we use the `Basic` authentication scheme, the credentials are constructed like this:
- The username and the password are combined with a colon (`registry:ui`).
- The resulting string is base64 encoded (`cmVnaXN0cnk6dWk=`). You can simply run `echo -n "registry:ui" | base64`.
- In your header, put this value `Basic cmVnaXN0cnk6dWk=`
- In your `/etc/nginx/.env`, the file will contains `NGINX_PROXY_HEADER_Authorization=Basic cmVnaXN0cnk6dWk=`
For X-Forward-For, replace all hyphens by underscores, and the value will be a nginx variable which is `$proxy_add_x_forwarded_for`. In your docker compose you will need to duplicate the `$` character. In your docker-compose, your environment will look like `NGINX_PROXY_HEADER_X_Forwarded_For=$$proxy_add_x_forwarded_for`
As usual, run the project with `docker-compose up -d` (for background mode)

View File

@@ -0,0 +1,28 @@
version: '2.0'
services:
registry:
image: registry:2.7
volumes:
- ./registry-data:/var/lib/registry
- ./registry-config/credentials.yml:/etc/docker/registry/config.yml
- ./registry-config/htpasswd:/etc/docker/registry/htpasswd
networks:
- registry-ui-net
ui:
image: joxit/docker-registry-ui:static
ports:
- 80:80
environment:
- REGISTRY_TITLE=My Private Docker Registry
- REGISTRY_URL=http://registry:5000
- NGINX_PROXY_HEADER_X_Forwarded_For=$$proxy_add_x_forwarded_for
volumes:
- ./nginx.env:/etc/nginx/.env
depends_on:
- registry
networks:
- registry-ui-net
networks:
registry-ui-net:

View File

@@ -0,0 +1 @@
NGINX_PROXY_HEADER_Authorization=Basic cmVnaXN0cnk6dWk=

View File

@@ -0,0 +1,25 @@
version: 0.1
log:
fields:
service: registry
storage:
delete:
enabled: true
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ['http://localhost']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization']
Access-Control-Max-Age: [1728000]
Access-Control-Allow-Credentials: [true]
Access-Control-Expose-Headers: ['Docker-Content-Digest']
auth:
htpasswd:
realm: basic-realm
path: /etc/docker/registry/htpasswd

View File

@@ -0,0 +1 @@
registry:$2y$11$1bmuJLK8HrQl5ACS/WeqRuJLUArUZfUcP2R23asmozEpfN76.pCHy

View File

@@ -20,14 +20,16 @@ const allTags = ['src/tags/*.tag', 'src/tags/dialogs/*.tag'];
const allScripts = [
'src/scripts/http.js',
'src/scripts/script.js'
'src/scripts/script.js',
'src/scripts/utils.js'
];
const staticTags = ['src/tags/*.tag'];
const staticScripts = [
'src/scripts/http.js',
'src/scripts/static.js'
'src/scripts/static.js',
'src/scripts/utils.js'
];
function html() {

View File

@@ -24,6 +24,8 @@ server {
#! if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
#! return 404;
#! }
#! proxy_set_header Host $http_host;
#! ${NGINX_PROXY_HEADERS}
#! proxy_pass ${REGISTRY_URL};
#! }

View File

@@ -1,6 +1,6 @@
{
"name": "docker-registry-ui",
"version": "1.2.1",
"version": "1.3.0",
"scripts": {
"build": "./node_modules/gulp/bin/gulp.js build"
},
@@ -14,7 +14,7 @@
"dependencies": {},
"devDependencies": {
"del": "^3.0.0",
"gulp": "^4.0.1",
"gulp": "^4.0.2",
"gulp-clean-css": "^4.2.0",
"gulp-concat": "^2.6.0",
"gulp-filter": "^5.1.0",

View File

@@ -58,9 +58,11 @@
<script src="tags/dialogs/menu.tag" type="riot/tag"></script>
<script src="tags/image-size.tag" type="riot/tag"></script>
<script src="tags/image-date.tag" type="riot/tag"></script>
<script src="tags/pagination.tag" type="riot/tag"></script>
<script src="tags/app.tag" type="riot/tag"></script>
<script src="scripts/http.js"></script>
<script src="scripts/script.js"></script>
<script src="scripts/utils.js"></script>
<!-- endbuild -->
</body>

View File

@@ -3,9 +3,9 @@
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('fonts/Material Icons'),
local('fonts/MaterialIcons-Regular'),
src: url(fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url(fonts/MaterialIcons-Regular.woff2) format('woff2'),
url(fonts/MaterialIcons-Regular.woff) format('woff'),
url(fonts/MaterialIcons-Regular.ttf) format('truetype');

View File

@@ -85,7 +85,7 @@ registryUI.removeServer = function(url) {
}
registryUI.updateHistory = function(url) {
history.pushState(null, '', (url ? '?url=' + registryUI.encodeURI(url) : '?') + window.location.hash);
registryUI.updateQueryString({ url: registryUI.encodeURI(url) })
registryUI._url = url;
}
@@ -100,10 +100,12 @@ registryUI.getUrlQueryParam = function () {
};
registryUI.encodeURI = function(url) {
if (!url) { return; }
return url.indexOf('&') < 0 ? window.encodeURIComponent(url) : btoa(url);
};
registryUI.decodeURI = function(url) {
if (!url) { return; }
return url.startsWith('http') ? window.decodeURIComponent(url) : atob(url);
};

114
src/scripts/utils.js Normal file
View File

@@ -0,0 +1,114 @@
registryUI.bytesToSize = function (bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == undefined || isNaN(bytes)) {
return '?';
} else if (bytes == 0) {
return '0 Byte';
}
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.ceil(bytes / Math.pow(1024, i)) + ' ' + sizes[i];
};
registryUI.dateFormat = function(date) {
if (date === undefined) {
return '';
}
const labels = ['a second', 'seconds', 'a minute', 'minutes', 'an hour', 'hours', 'a day', 'days', 'a month', 'months', 'a year', 'years'];
const maxSeconds = [1, 60, 3600, 86400, 2592000, 31104000, Infinity];
const diff = (new Date() - date) / 1000;
for (var i = 0; i < maxSeconds.length - 1; i++) {
if (maxSeconds[i] * 2 >= diff) {
return labels[i * 2];
} else if (maxSeconds[i + 1] > diff) {
return Math.floor(diff / maxSeconds[i]) + ' ' + labels[i * 2 + 1];
}
}
};
registryUI.getHistoryIcon = function(attribute) {
switch (attribute) {
case 'architecture':
return 'memory';
case 'created':
return 'event';
case 'docker_version':
return '';
case 'os':
return 'developer_board';
case 'Cmd':
return 'launch';
case 'Entrypoint':
return 'input';
case 'Env':
return 'notes';
case 'Labels':
return 'label';
case 'User':
return 'face';
case 'Volumes':
return 'storage';
case 'WorkingDir':
return 'home';
case 'author':
return 'account_circle';
case 'id':
case 'digest':
return 'settings_ethernet';
case 'created_by':
return 'build';
case 'size':
return 'get_app';
case 'ExposedPorts':
return 'router';
default:
''
}
}
registryUI.getPage = function(elts, page, limit) {
if (!limit) { limit = 100; }
if (!elts) { return []; }
return elts.slice((page - 1) * limit, limit * page);
}
registryUI.getNumPages = function(elts, limit) {
if (!limit) { limit = 100; }
if (!elts) { return 0; }
return Math.trunc(elts.length / limit) + 1;
}
registryUI.getPageLabels = function(page, nPages) {
var pageLabels = [];
var maxItems = 10;
if (nPages === 1) { return pageLabels; }
if (page !== 1 && nPages >= maxItems) {
pageLabels.push({'icon': 'first_page', page: 1});
pageLabels.push({'icon': 'chevron_left', page: page - 1});
}
var start = Math.round(Math.max(1, Math.min(page - maxItems / 2, nPages - maxItems + 1)));
for (var i = start; i < Math.min(nPages + 1, start + maxItems); i++) {
pageLabels.push({
page: i,
current: i === page,
'space-left': page === 1 && nPages > maxItems,
'space-right': page === nPages && nPages > maxItems
});
}
if (page !== nPages && nPages >= maxItems) {
pageLabels.push({'icon': 'chevron_right', page: page + 1});
pageLabels.push({'icon': 'last_page', page: nPages});
}
return pageLabels;
}
registryUI.updateQueryString = function(qs) {
var search = '';
for (var key in qs) {
if (qs[key] !== undefined) {
search += (search.length > 0 ? '&' : '?') +key + '=' + qs[key];
}
}
history.pushState(null, '', search + window.location.hash);
}

View File

@@ -48,14 +48,24 @@ main {
font-weight: inherit;
}
material-card {
min-height: 200px;
material-card, pagination .conatianer {
max-width: 75%;
margin: auto;
margin-top: 20px;
margin-bottom: 20px;
}
pagination .conatianer {
display: flex;
display: -moz-flex;
display: -webkit-flex;
display: -ms-flexbox;
}
pagination .conatianer .pagination-centered {
margin: auto;
}
@media screen and (max-width: 950px){
material-card {
width: 95%;
@@ -212,12 +222,14 @@ material-card table th {
}
material-card material-button:hover,
material-card table tbody tr:hover {
material-card table tbody tr:hover,
pagination material-button:hover {
background-color: #eee;
}
material-card material-button,
material-card table tbody tr {
material-card table tbody tr,
pagination material-button {
transition-duration: .28s;
transition-timing-function: cubic-bezier(.4, 0, .2, 1);
transition-property: background-color;
@@ -326,7 +338,8 @@ dropdown-item, #menu-control-dropdown p {
background-color: #e0e0e0;
}
material-popup material-button {
material-popup material-button,
pagination material-button {
background-color: #fff;
color: #000;
}
@@ -445,7 +458,8 @@ tag-history-button button {
border: none;
}
material-card material-button {
material-card material-button,
pagination material-button {
max-height: 30px;
max-width: 30px;
}
@@ -454,7 +468,8 @@ material-button:hover material-waves {
background: none;
}
material-card material-button {
material-card material-button,
pagination material-button {
background-color: inherit;
}
@@ -475,6 +490,10 @@ catalog-element catalog-element.hide material-card {
opacity: 0;
}
catalog-element catalog-element .list > span i.material-icons {
margin-right: 48px;
}
remove-image {
width: 30px;
}
@@ -506,4 +525,30 @@ material-checkbox .checkbox {
material-checkbox .checkbox.checked {
background-color: #777;
}
pagination material-button {
padding: 0.2em 0.75em;
}
pagination material-button .content {
display: flex;
align-content: center;
line-height: 1.9em;
}
pagination material-button.current {
border: 1px solid rgba(0, 0, 0, .12);
}
pagination material-button.current.space-left {
margin-left: 85px;
}
pagination material-button.current.space-right {
margin-right: 85px;
}
pagination material-button .content i.material-icons {
color: #000;
}

View File

@@ -130,8 +130,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return this.fillInfo();
});
this.on('get-date', function() {
if (this.date !== undefined) {
return this.trigger('date', this.date);
if (this.creationDate !== undefined) {
return this.trigger('creation-date', this.creationDate);
}
return this.fillInfo();
});
@@ -221,21 +221,34 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
oReq.send();
};
registryUI.bytesToSize = function (bytes) {
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
if (bytes == undefined || isNaN(bytes)) {
return '?';
} else if (bytes == 0) {
return '0 Byte';
}
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
return Math.ceil(bytes / Math.pow(1024, i)) + ' ' + sizes[i];
};
registryUI.taglist.go = function(image) {
route('taglist/' + image);
};
registryUI.getPageQueryParam = function() {
var qs = route.query();
try {
return qs.page !== undefined ? parseInt(qs.page.replace(/#.*/, '')) : 1;
} catch(e) { return 1; }
}
registryUI.getQueryParams = function(update) {
var qs = route.query();
update = update || {};
for (var key in qs) {
if (qs[key] !== undefined) {
qs[key] = qs[key].replace(/#!.*/, '');
} else {
delete qs[key];
}
}
for (var key in update) {
if (update[key] !== undefined) {
qs[key] = update[key];
}
}
return qs;
}
route.start(true);
</script>
</app>

View File

@@ -21,13 +21,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<span>
<i class="material-icons">send</i>
{ typeof opts.item === "string" ? opts.item : opts.item.repo }
<div hide="{typeof opts.item === "string"}" class="item-count right">
<div if="{typeof opts.item !== "string"}" class="item-count right">
{ opts.item.images && opts.item.images.length } images
<i class="material-icons animated {expanded: opts.expanded}">expand_more</i>
</div>
</span>
</material-card>
<catalog-element hide="{typeof opts.item === "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
<catalog-element if="{typeof opts.item !== "string"}" class="animated {hide: !expanded, expanding: expanding}" each="{item in item.images}" />
<script>
this.on('mount', function() {
const self = this;

View File

@@ -16,17 +16,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<catalog>
<!-- Begin of tag -->
<material-card ref="catalog-tag" class="catalog">
<material-card ref="catalog-tag" class="catalog header">
<div class="material-card-title-action">
<h2>
Repositories of { registryUI.name() }
<div class="item-count">{ registryUI.catalog.length } images</div>
</h2>
</div>
<div hide="{ registryUI.catalog.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
</material-card>
<div hide="{ registryUI.catalog.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
<catalog-element each="{ item in registryUI.catalog.repositories }" />
<script>
registryUI.catalog.instance = this;

View File

@@ -15,29 +15,16 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<image-date>
<div title="Creation date { this.localDate }">{ this.dateFormat(this.date) } ago</div>
<div title="Creation date { this.localDate }">{ registryUI.dateFormat(this.date) } ago</div>
<script type="text/javascript">
const self = this;
this.dateFormat = function(date) {
if (date === undefined) {
return '';
}
const labels = ['a second', 'seconds', 'a minute', 'minutes', 'an hour', 'hours', 'a day', 'days', 'a month', 'months', 'a year', 'years'];
const maxSeconds = [1, 60, 3600, 86400, 2592000, 31104000, Infinity];
const diff = (new Date() - date) / 1000;
for (var i = 0; i < maxSeconds.length - 1; i++) {
if (maxSeconds[i] * 2 >= diff) {
return labels[i * 2];
} else if (maxSeconds[i + 1] > diff) {
return Math.floor(diff / maxSeconds[i]) + ' ' + labels[i * 2 + 1];
}
}
};
opts.image.on('creation-date', function(date) {
self.date = date;
self.localDate = date.toLocaleString()
self.update();
});
opts.image.trigger('get-date');
</script>
</image-date>

39
src/tags/pagination.tag Normal file
View File

@@ -0,0 +1,39 @@
<!--
Copyright (C) 2016-2019 Jones Magloire @Joxit
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<pagination>
<!-- Begin of tag -->
<div class="conatianer">
<div class="pagination-centered">
<material-button waves-color="rgba(158,158,158,.4)" each="{p in this.opts.pages}" class="{ current: p.current, space-left: p['space-left'], space-right: p['space-right']}">
<i show="{ p.icon }" class="material-icons">{ p.icon }</i>
<div hide="{ p.icon }">{ p.page }</div>
</material-button>
</div>
<div>
<script>
this.on('updated', function() {
if (!this.tags['material-button']) { return; }
var buttons = Array.isArray(this.tags['material-button']) ? this.tags['material-button'] : [this.tags['material-button']];
buttons.forEach(function(button) {
button.root.onclick = function() {
registryUI.taglist.instance.trigger('page-update', button.p.page)
}
});
});
</script>
<!-- End of tag -->
</pagination>

View File

@@ -15,63 +15,71 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<remove-image>
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." hide="{ opts.multiDelete }">
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." if="{ !opts.multiDelete }">
<i class="material-icons">delete</i>
</material-button>
<material-checkbox show="{ opts.multiDelete }" title="Select this tag to delete it."></material-checkbox>
<material-checkbox if="{ opts.multiDelete }" title="Select this tag to delete it."></material-checkbox>
<script type="text/javascript">
const self = this;
this.on('update', function() {
if (!this.opts.multiDelete && this.tags['material-checkbox'].checked) {
this.tags['material-checkbox'].toggle();
}
this.on('updated', function() {
});
this.on('mount', function() {
this.delete = this.tags['material-button'].root.onclick = function(ignoreError) {
const name = self.opts.image.name;
const tag = self.opts.image.tag;
const oReq = new Http();
oReq.addEventListener('loadend', function() {
registryUI.taglist.go(name);
if (this.status == 200) {
if (!this.hasHeader('Docker-Content-Digest')) {
registryUI.errorSnackbar('You need to add Access-Control-Expose-Headers: [\'Docker-Content-Digest\'] in your server configuration.');
return;
}
const digest = this.getResponseHeader('Docker-Content-Digest');
const oReq = new Http();
oReq.addEventListener('loadend', function() {
if (this.status == 200 || this.status == 202) {
registryUI.taglist.display()
registryUI.snackbar('Deleting ' + name + ':' + tag + ' image. Run `registry garbage-collect config.yml` on your registry');
} else if (this.status == 404) {
ignoreError || registryUI.errorSnackbar('Digest not found');
} else {
registryUI.snackbar(this.responseText);
this.on('updated', function() {
if (self.multiDelete == self.opts.multiDelete) {
return;
}
if (this.tags['material-button']) {
this.delete = this.tags['material-button'].root.onclick = function(ignoreError) {
const name = self.opts.image.name;
const tag = self.opts.image.tag;
const oReq = new Http();
oReq.addEventListener('loadend', function() {
registryUI.taglist.go(name);
if (this.status == 200) {
if (!this.hasHeader('Docker-Content-Digest')) {
registryUI.errorSnackbar('You need to add Access-Control-Expose-Headers: [\'Docker-Content-Digest\'] in your server configuration.');
return;
}
});
oReq.open('DELETE', registryUI.url() + '/v2/' + name + '/manifests/' + digest);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.addEventListener('error', function() {
registryUI.errorSnackbar('An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].');
});
oReq.send();
} else if (this.status == 404) {
registryUI.errorSnackbar('Manifest for ' + name + ':' + tag + ' not found');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('HEAD', registryUI.url() + '/v2/' + name + '/manifests/' + tag);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.send();
};
const digest = this.getResponseHeader('Docker-Content-Digest');
const oReq = new Http();
oReq.addEventListener('loadend', function() {
if (this.status == 200 || this.status == 202) {
registryUI.taglist.display()
registryUI.snackbar('Deleting ' + name + ':' + tag + ' image. Run `registry garbage-collect config.yml` on your registry');
} else if (this.status == 404) {
ignoreError || registryUI.errorSnackbar('Digest not found');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('DELETE', registryUI.url() + '/v2/' + name + '/manifests/' + digest);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.addEventListener('error', function() {
registryUI.errorSnackbar('An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].');
});
oReq.send();
} else if (this.status == 404) {
registryUI.errorSnackbar('Manifest for ' + name + ':' + tag + ' not found');
} else {
registryUI.snackbar(this.responseText);
}
});
oReq.open('HEAD', registryUI.url() + '/v2/' + name + '/manifests/' + tag);
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json');
oReq.send();
};
}
this.tags['material-checkbox'].on('toggle', function() {
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
});
if (this.tags['material-checkbox']) {
if (!this.opts.multiDelete && this.tags['material-checkbox'].checked) {
this.tags['material-checkbox'].toggle();
}
this.tags['material-checkbox'].on('toggle', function() {
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
});
}
self.multiDelete = self.opts.multiDelete;
});
</script>
</remove-image>

View File

@@ -15,50 +15,9 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<tag-history-element class="{entry.key}">
<div class="headline"><i class="material-icons">{ this.getIcon(entry.key) }</i>
<div class="headline"><i class="material-icons">{ registryUI.getHistoryIcon(entry.key) }</i>
<p>{ entry.key.replace('_', ' ') }</p>
</div>
<div class="value" if={!(entry.value instanceof Array)}> { entry.value }</div>
<div class="value" each={ e in entry.value } if={entry.value instanceof Array}> { e }</div>
<script type="text/javascript">
this.getIcon = function(attribute) {
switch (attribute) {
case 'architecture':
return 'memory';
case 'created':
return 'event';
case 'docker_version':
return '';
case 'os':
return 'developer_board';
case 'Cmd':
return 'launch';
case 'Entrypoint':
return 'input';
case 'Env':
return 'notes';
case 'Labels':
return 'label';
case 'User':
return 'face';
case 'Volumes':
return 'storage';
case 'WorkingDir':
return 'home';
case 'author':
return 'account_circle';
case 'id':
case 'digest':
return 'settings_ethernet';
case 'created_by':
return 'build';
case 'size':
return 'get_app';
case 'ExposedPorts':
return 'router';
default:
''
}
}
</script>
</tag-history-element>

View File

@@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<tag-history>
<material-card ref="tag-history-tag" class="tag-history">
<material-card ref="tag-history-tag" class="tag-history header">
<div class="material-card-title-action">
<material-button waves-center="true" rounded="true" waves-color="#ddd">
<i class="material-icons">arrow_back</i>

View File

@@ -16,8 +16,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<taglist>
<!-- Begin of tag -->
<material-card ref="taglist-tag" class="taglist" multi-delete={ this.multiDelete }>
<div class="material-card-title-action">
<material-card class="header">
<div class="material-card-title-action ">
<material-button waves-center="true" rounded="true" waves-color="#ddd" onclick="registryUI.home();">
<i class="material-icons">arrow_back</i>
</material-button>
@@ -26,9 +26,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<div class="item-count">{ registryUI.taglist.tags.length } tags</div>
</h2>
</div>
<div hide="{ registryUI.taglist.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
</material-card>
<div hide="{ registryUI.taglist.loadend }" class="spinner-wrapper">
<material-spinner></material-spinner>
</div>
<pagination pages="{ registryUI.getPageLabels(this.page, registryUI.getNumPages(registryUI.taglist.tags)) }"></pagination>
<material-card ref="taglist-tag" class="taglist"
multi-delete={ this.multiDelete }
tags={ registryUI.getPage(registryUI.taglist.tags, this.page) }
show="{ registryUI.taglist.loadend }">
<table show="{ registryUI.taglist.loadend }" style="border: none;">
<thead>
<tr>
@@ -42,7 +48,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onclick="registryUI.taglist.reverse();">Tag
</th>
<th class="show-tag-history">History</th>
<th class={ 'remove-tag': true, delete: this.parent.toDelete > 0 } show="{ registryUI.isImageRemoveActivated }">
<th class={ 'remove-tag': true, delete: this.parent.toDelete > 0 } if="{ registryUI.isImageRemoveActivated }">
<material-checkbox ref="remove-tag-checkbox" class="indeterminate" show={ this.toDelete === 0} title="Toggle multi-delete. Alt+Click to select all tags."></material-checkbox>
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete selected images." onclick={ registryUI.taglist.bulkDelete } show={ this.toDelete > 0 }>
<i class="material-icons">delete</i>
@@ -50,7 +56,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</tr>
</thead>
<tbody>
<tr each="{ image in registryUI.taglist.tags }">
<tr each="{ image in this.opts.tags }">
<td class="material-card-th-left">{ image.name }</td>
<td class="copy-to-clipboard">
<copy-to-clipboard image={ image }/>
@@ -67,15 +73,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<td class="show-tag-history">
<tag-history-button image={ image }/>
</td>
<td show="{ registryUI.isImageRemoveActivated }">
<td if="{ registryUI.isImageRemoveActivated }">
<remove-image multi-delete={ this.opts.multiDelete } image={ image }/>
</td>
</tr>
</tbody>
</table>
</material-card>
<pagination pages="{ registryUI.getPageLabels(this.page, registryUI.getNumPages(registryUI.taglist.tags)) }"></pagination>
<script>
var self = registryUI.taglist.instance = this;
self.page = registryUI.getPageQueryParam();
this.multiDelete = false;
this.toDelete = 0;
@@ -105,6 +113,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
});
this.on('page-update', function(page) {
self.page = page < 1 ? 1 : page;
registryUI.updateQueryString(registryUI.getQueryParams({ page: self.page }) );
this.toDelete = 0;
this.update();
});
this._getRemoveImageTags = function() {
var images = self.refs['taglist-tag'].tags['remove-image'];
if (!(images instanceof Array)) {
@@ -123,19 +138,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
};
this.on('mount', function() {
var toggle = this.tags['material-card'].refs['remove-tag-checkbox'].toggle;
this.tags['material-card'].refs['remove-tag-checkbox'].toggle = function(e) {
this.on('update', function() {
var checkbox = this.refs['taglist-tag'].refs['remove-tag-checkbox'];
if (!checkbox || checkbox._toggle) { return; }
checkbox._toggle = checkbox.toggle;
checkbox.toggle = function(e) {
if (e.altKey) {
self._getRemoveImageTags()
.filter(function(img) { return !img.tags['material-checkbox'].checked; })
.forEach(function(img) { img.tags['material-checkbox'].toggle() });
} else {
toggle();
this._toggle();
}
};
this.tags['material-card'].refs['remove-tag-checkbox'].on('toggle', function() {
checkbox.on('toggle', function() {
registryUI.taglist.instance.multiDelete = this.checked;
registryUI.taglist.instance.update();
});
@@ -153,6 +171,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
registryUI.taglist.tags = registryUI.taglist.tags.map(function(tag) {
return new registryUI.DockerImage(registryUI.taglist.name, tag);
}).sort(registryUI.DockerImage.compare);
self.trigger('page-update', Math.min(self.page, registryUI.getNumPages(registryUI.taglist.tags)))
} else if (this.status == 404) {
registryUI.snackbar('Server not found', true);
} else {