Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c8b929e4f | ||
|
|
4fcbea698b | ||
|
|
999e5ae1c6 | ||
|
|
8b9efa0397 | ||
|
|
1c88ad57f8 | ||
|
|
c66887fac7 | ||
|
|
3dc035dac8 | ||
|
|
29cd2b7a8f | ||
|
|
e065298eed | ||
|
|
f02c99f12d | ||
|
|
7eed05ae50 | ||
|
|
7a35d61359 | ||
|
|
e990c39a18 | ||
|
|
3bfe107e3c | ||
|
|
0f54555b52 | ||
|
|
1ca1a1a207 | ||
|
|
bc5082dcf9 | ||
|
|
58b1486c81 | ||
|
|
2c9f006b8c | ||
|
|
026cf8ccdc | ||
|
|
69066dd867 | ||
|
|
6d4d507db5 | ||
|
|
83f15aa267 | ||
|
|
b643a44113 | ||
|
|
a2b3c592df | ||
|
|
263584fc43 | ||
|
|
1173453f72 | ||
|
|
7d095916db | ||
|
|
8ef411059c | ||
|
|
603b5861fa | ||
|
|
4ff1b2fabe | ||
|
|
e6af9321a8 | ||
|
|
bb3182d56e | ||
|
|
761a680703 | ||
|
|
926f67e1b5 | ||
|
|
781b026471 | ||
|
|
92fd78165f | ||
|
|
a3276dcb79 | ||
|
|
962592c54a | ||
|
|
669c3399d0 | ||
|
|
9c303d32c7 | ||
|
|
3d4267f5ab | ||
|
|
b3427213d0 | ||
|
|
7beac85f25 | ||
|
|
ea21483346 | ||
|
|
8fe3adf125 | ||
|
|
142727e8ac | ||
|
|
fb80283dd9 | ||
|
|
11692c136e | ||
|
|
cde932ec17 | ||
|
|
307e171ede | ||
|
|
6b012cc8fd | ||
|
|
887e77f2f1 | ||
|
|
5890b723d2 | ||
|
|
e947d2251d |
@@ -4,4 +4,5 @@
|
||||
!nginx
|
||||
!src
|
||||
!package.json
|
||||
!gulpfile.js
|
||||
!gulpfile.js
|
||||
!favicon.ico
|
||||
14
.github/workflows/main.yml
vendored
@@ -9,6 +9,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Build the interface
|
||||
run: npm run build
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
@@ -18,7 +22,7 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push Standard Version
|
||||
- name: Build and push Beta Alpine Version
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
@@ -28,13 +32,13 @@ jobs:
|
||||
tags: |
|
||||
joxit/docker-registry-ui:master
|
||||
joxit/docker-registry-ui:main
|
||||
- name: Build and push Static version
|
||||
- name: Build and push Beta Debian Version
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./static.dockerfile
|
||||
file: ./debian.dockerfile
|
||||
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||
push: true
|
||||
tags: |
|
||||
joxit/docker-registry-ui:master-static
|
||||
joxit/docker-registry-ui:main-static
|
||||
joxit/docker-registry-ui:master-debian
|
||||
joxit/docker-registry-ui:main-debian
|
||||
16
.github/workflows/release.yml
vendored
@@ -11,9 +11,9 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
- name: Build
|
||||
- name: Build the interface
|
||||
run: npm run build
|
||||
- name: Current tag
|
||||
id: current-tag
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push Standard Version
|
||||
- name: Build and push Latest Version
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
@@ -50,15 +50,15 @@ jobs:
|
||||
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||
push: true
|
||||
tags: |
|
||||
joxit/docker-registry-ui:master
|
||||
joxit/docker-registry-ui:latest
|
||||
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}
|
||||
- name: Build and push Static version
|
||||
- name: Build and push Latest Debian Version
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./static.dockerfile
|
||||
file: ./debian.dockerfile
|
||||
platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||
push: true
|
||||
tags: |
|
||||
joxit/docker-registry-ui:master-static
|
||||
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}-static
|
||||
joxit/docker-registry-ui:debian
|
||||
joxit/docker-registry-ui:${{steps.current-tag.outputs.tag}}-debian
|
||||
1
.gitignore
vendored
@@ -5,3 +5,4 @@ registry-data
|
||||
.idea
|
||||
_site
|
||||
*.orig
|
||||
.serve/
|
||||
|
||||
19
Dockerfile
@@ -12,22 +12,15 @@
|
||||
#
|
||||
# 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/>.
|
||||
FROM node:10-alpine AS builder
|
||||
|
||||
WORKDIR /usr/app
|
||||
|
||||
COPY package.json .
|
||||
|
||||
RUN yarn install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
COPY --from=builder /usr/app/dist/ /usr/share/nginx/html/
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
@@ -1 +0,0 @@
|
||||
static.dockerfile
|
||||
114
README.md
@@ -9,20 +9,15 @@ title: Docker Registry User Interface
|
||||
|
||||
## Overview
|
||||
|
||||
This project aims to provide a simple and complete user interface for your private docker registry.
|
||||
You have the choice between two versions, the **standard interface** (`joxit/docker-registry-ui:latest`) and the **static interface** (`joxit/docker-registry-ui:static`).
|
||||
This project aims to provide a simple and complete user interface for your private docker registry. You can customize the interface with various options. The major option is `SINGLE_REGISTRY` which allows you to disable the dynamic selection of docker registeries (same behavior as the old **static** tag).
|
||||
|
||||
In the **standard interface**, there is no default registry, you need to add your own within the UI.
|
||||
With this version, you can manage **more than one** registry server but all the environment variables will be **unavailable**.
|
||||
All registries will be stored in the [local storage](https://en.wikipedia.org/wiki/Web_storage#Local_and_session_storage) of your browser. No configuration is needed when you launch the UI.
|
||||
|
||||
In the **static interface**, it will connect to a single registry and will not change. The configuration is done at the start of the interface, when you use the docker images whose tags contain the `static` keyword. With this version, you can manage **only one registry** and all environment variables will be **available**.
|
||||
You may need the [migration guide from 1.x to 2.x](https://github.com/Joxit/docker-registry-ui/wiki/Migrating-from-1.x-to-2.x) or [the 1.x readme](https://github.com/Joxit/docker-registry-ui/blob/8fe3adf12540d1316cb57628ebe86a392a703d90/README.md)
|
||||
|
||||
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), [Live Demo](https://joxit.dev/docker-registry-ui/demo/), [Examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples)
|
||||
## [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/main/examples)
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
@@ -40,20 +35,20 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
- Display image/tag count (see [#56 issue comment](https://github.com/Joxit/docker-registry-ui/issues/56#issuecomment-449246524)).
|
||||
- Select multiple tags to delete (see [#29](https://github.com/Joxit/docker-registry-ui/issues/29)).
|
||||
- Select all tags with ALT + Click to delete (see [#80](https://github.com/Joxit/docker-registry-ui/issues/80)).
|
||||
- One interface for many registries **standard interface**.
|
||||
- Share your docker registry with query parameter `url` (e.g. `https://joxit.dev/docker-registry-ui/demo?url=https://registry.example.com`) **standard interface**.
|
||||
- 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 via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89)) **static interface**
|
||||
- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)) **static interface**.
|
||||
- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)) **static interface**.
|
||||
- One interface for many registries (when `SINGLE_REGISTRY=false`).
|
||||
- Share your docker registry with query parameter `url` (e.g. `https://joxit.dev/docker-registry-ui/demo?url=https://registry.example.com`) (when `SINGLE_REGISTRY=false`).
|
||||
- Use the UI as reverse proxy (with `NGINX_PROXY_PASS_URL` environment variable) to your docker registry (This will avoid CORS).
|
||||
- Add Title when using `REGISTRY_TITLE` (see [#28](https://github.com/Joxit/docker-registry-ui/issues/28)).
|
||||
- Customise docker pull command on static registry UI (see [#71](https://github.com/Joxit/docker-registry-ui/issues/71)).
|
||||
- Add custom header via environment variable and file via `NGINX_PROXY_HEADER_*` (see [#89](https://github.com/Joxit/docker-registry-ui/pull/89))
|
||||
- Show/Hide content digest in taglist via `SHOW_CONTENT_DIGEST` (values are: [`true`, `false`], default: `true`) (see [#126](https://github.com/Joxit/docker-registry-ui/issues/126)).
|
||||
- Limit the number of elements in the image list via `CATALOG_ELEMENTS_LIMIT` (see [#127](https://github.com/Joxit/docker-registry-ui/pull/127)).
|
||||
- Multi arch support in history page (see [#130](https://github.com/Joxit/docker-registry-ui/issues/130) and [#134](https://github.com/Joxit/docker-registry-ui/pull/134))
|
||||
|
||||
## FAQ
|
||||
|
||||
- What is the difference between **`joxit/docker-registry-ui:latest`** and **`joxit/docker-registry-ui:static`** tags ?
|
||||
- The `latest` tag was the first version of the project, one UI for many docker registries. The `static` tag allows you to have an interface for a single registry and also allows you select your features.
|
||||
- What is the difference between **`SINGLE_REGISTRY=false`** and **`SINGLE_REGISTRY=true`** options ?
|
||||
- When `SINGLE_REGISTRY` is set to false, a menu appears on the interface allowing you to dynamically change docker registry URLs.
|
||||
- Why, when I delete all tags of an image, the image is still in the UI ?
|
||||
- This is a limitation of docker registry, the garbage collector don't remove empty images. If you want to delete dangling images, you will need to delete the folder in your registry data. (see [#77](https://github.com/Joxit/docker-registry-ui/issues/77))
|
||||
- Why the image size in the UI is not the same as displayed during `docker images` ?
|
||||
@@ -71,57 +66,30 @@ This web user interface uses [Riot](https://github.com/Riot/riot) the react-like
|
||||
- Why DELETE fails with 401 status code (using Basic Auth) ?
|
||||
- This is caused by a bug in docker registry, I suggest to have your UI on the same domain than your registry e.g. registry.example.com/ui/. (see [#104](https://github.com/Joxit/docker-registry-ui/issues/104)).
|
||||
- Can I use the docker registry ui as a standalone application (with Electron) ?
|
||||
- Yes, check out the example [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron). (see [#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- Yes, check out the example [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron). (see [#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- I deleted images through the UI, but they are still present on the server. How can I delete them?
|
||||
- When you delete an image with the UI, only the reference is deleted and not the content. To remove dangling images, you need to run the garbage collector of the registry with the command `registry garbage-collect config.yml` or `docker exec registry registry garbage-collect config.yml`. (see [#77](https://github.com/Joxit/docker-registry-ui/issues/77) [#147](https://github.com/Joxit/docker-registry-ui/issues/147))
|
||||
- Why when I delete one tag, all tags with the same SHA are deleted ?
|
||||
- This a docker registry API limitation, there is only one way to [delete images with tag](https://docs.docker.com/registry/spec/api/#deleting-an-image), it's by its `name` and its `manifest` (it's a sha of the content). So when you delete a tag, this will delete all tags of this image with the same SHA/manifest.
|
||||
|
||||
|
||||
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/master/examples) or open an issue.
|
||||
Need more informations ? Try my [examples](https://github.com/Joxit/docker-registry-ui/tree/main/examples) or open an issue.
|
||||
|
||||
## Getting Started
|
||||
|
||||
The docker image contains the source code and nginx in order to serve the docker-registry-ui. Please remember the difference between the **standard interface** (`latest` tag) and **static interface** (`static` tags).
|
||||
|
||||
### Run the standard interface
|
||||
|
||||
You can run the standard interface see the website on your 80 port. You will be able to use the interface for **many registry servers**, but all the configuration via environment variables from the static interface will be **unavailable**.
|
||||
|
||||
```sh
|
||||
docker run -d -p 80:80 joxit/docker-registry-ui:latest
|
||||
```
|
||||
|
||||
### Run the static interface
|
||||
## Available options
|
||||
|
||||
Some env options are available for use this interface for **only one server**.
|
||||
|
||||
- [`URL`](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone): set the static URL to use (You will need CORS configuration). Example: `http://127.0.0.1:5000`. (`Required`)
|
||||
- [`REGISTRY_URL`](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy): your docker registry URL to contact (CORS configuration is not needed). Example: `http://my-docker-container:5000`. (Can't be used with `URL`, since 0.3.2).
|
||||
- `DELETE_IMAGES`: if this variable is empty or `false`, delete feature is deactivated. It is activated otherwise.
|
||||
- `REGISTRY_TITLE`: Set a custom title for your user interface when using `REGISTRY_URL` (since 0.3.4).
|
||||
- `PULL_URL`: Set a custom url for the docker pull command, this is useful when you use `REGISTRY_URL` and your registry is on a different host (since 1.1.0).
|
||||
- [`NGINX_PROXY_HEADER_*`](https://github.com/Joxit/docker-registry-ui/tree/master/examples/proxy-headers): Set custom headers for your docker registry, usefull when you want to add your credentials. (Can be use only with `REGISTRY_URL`).
|
||||
- [`SHOW_CONTENT_DIGEST`](https://github.com/Joxit/docker-registry-ui/issues/126): Show content digest in docker tag list. Default: `true`.
|
||||
- [`CATALOG_ELEMENTS_LIMIT`](https://github.com/Joxit/docker-registry-ui/pull/132): Limit the number of elements in the catalog page. Default: `100000`.
|
||||
- `REGISTRY_URL`: The default url of your docker registry. You may need CORS configuration on your registry. This is usually the domain name or IP of your registry reachable by your computer (e.g `http://registry.example.com`). (default: derived from the hostname of your UI).
|
||||
- `REGISTRY_TITLE`: Set a custom title for your user interface. (default: value derived from `REGISTRY_URL`).
|
||||
- `PULL_URL`: Set a custom url when you copy the `docker pull` command. (default: value derived from `REGISTRY_URL`).
|
||||
- `DELETE_IMAGES`: Set if we can delete images from the UI. (default: `false`)
|
||||
- `SHOW_CONTENT_DIGEST`: Show content digest in docker tag list. (default: `true`)
|
||||
- `CATALOG_ELEMENTS_LIMIT`: Limit the number of elements in the catalog page. (default: `100000`).
|
||||
- `SINGLE_REGISTRY`: Remove the menu that show the dialogs to add, remove and change the endpoint of your docker registry. (default `false`)
|
||||
- `NGINX_PROXY_PASS_URL`: Update the default Nginx configuration and set the **proxy_pass** to your backend docker registry (this avoid CORS configuration). This is usually the name of your registry container in the form `http://registry:5000`.
|
||||
- `NGINX_PROXY_HEADER_*`: Update the default Nginx configuration and set **custom headers** for your backend docker registry. Only when `NGINX_PROXY_PASS_URL` is used.
|
||||
|
||||
Example with `URL` option.
|
||||
|
||||
```sh
|
||||
docker run -d -p 80:80 -e URL=http://127.0.0.1:5000 -e DELETE_IMAGES=true joxit/docker-registry-ui:static
|
||||
```
|
||||
|
||||
Example with `REGISTRY_URL`, this will add a proxy to your registry.
|
||||
Your registry will be accessible here : `http://127.0.0.1/v2`, this will avoid CORS errors (see [#25](https://github.com/Joxit/docker-registry-ui/issues/25#issuecomment-360522487)).
|
||||
Be careful, `joxit/docker-registry-ui` and `registry:2` will communicate, both containers should be in the same network or use your private IP.
|
||||
|
||||
```sh
|
||||
docker network create registry-ui-net
|
||||
docker run -d --net registry-ui-net --name registry-srv registry:2
|
||||
docker run -d --net registry-ui-net -p 80:80 -e REGISTRY_URL=http://registry-srv:5000 -e DELETE_IMAGES=true -e REGISTRY_TITLE="My registry" joxit/docker-registry-ui:static
|
||||
```
|
||||
|
||||
There are some examples with [docker-compose](https://docs.docker.com/compose/) and docker-registry-ui as proxy [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-proxy/) or docker-registry-ui as standalone [here](https://github.com/Joxit/docker-registry-ui/tree/master/examples/ui-as-standalone/).
|
||||
There are some examples with [docker-compose](https://docs.docker.com/compose/) and docker-registry-ui as proxy [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy/) or docker-registry-ui as standalone [here](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone/).
|
||||
|
||||
## Using CORS
|
||||
|
||||
@@ -131,12 +99,12 @@ If your docker registry does not need credentials, you will need to send this HE
|
||||
|
||||
Access-Control-Allow-Origin: ['*']
|
||||
|
||||
If your docker registry need credentials, you will need to send these HEADERS:
|
||||
If your docker registry need credentials, you will need to send these HEADERS (you must add the protocol `http`/`https` and the port when not default `80`/`443`):
|
||||
|
||||
```yml
|
||||
http:
|
||||
headers:
|
||||
Access-Control-Allow-Origin: ['<your docker-registry-ui url>']
|
||||
Access-Control-Allow-Origin: ['http://registry.example.com']
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS'] # Optional
|
||||
@@ -185,7 +153,7 @@ http:
|
||||
addr: :5000
|
||||
headers:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['http://127.0.0.1:8001']
|
||||
Access-Control-Allow-Origin: ['http://127.0.0.1:8000']
|
||||
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
@@ -203,14 +171,14 @@ check out the [Electron](examples/electron/README.md) standalone application.
|
||||
|
||||
## All 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/75))
|
||||
- [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))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/master/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
|
||||
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy)
|
||||
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone)
|
||||
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/main/examples/traefik)
|
||||
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/75))
|
||||
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/main/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/main/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/main/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/main/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/main/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Copyright (C) 2016-2018 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/>.
|
||||
FROM arm32v7/nginx
|
||||
|
||||
LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
@@ -18,4 +18,9 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Copyright (C) 2016-2018 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/>.
|
||||
FROM arm64v8/nginx
|
||||
|
||||
LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
@@ -18,4 +18,9 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
#!/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
|
||||
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," index.html
|
||||
sed -i "s,\${REGISTRY_TITLE},${REGISTRY_TITLE}," index.html
|
||||
sed -i "s,\${PULL_URL},${PULL_URL}," index.html
|
||||
sed -i "s,\${SINGLE_REGISTRY},${SINGLE_REGISTRY}," index.html
|
||||
sed -i "s/\${CATALOG_ELEMENTS_LIMIT}/${CATALOG_ELEMENTS_LIMIT}/" index.html
|
||||
sed -i "s/\${SHOW_CONTENT_DIGEST}/${SHOW_CONTENT_DIGEST}/" index.html
|
||||
|
||||
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
|
||||
sed -i -r "s/(isImageRemoveActivated[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
|
||||
fi
|
||||
|
||||
if [ "${SHOW_CONTENT_DIGEST}" = false ] ; then
|
||||
sed -i -r "s/(showContentDigest[:=])[^,;]*/\1false/" scripts/docker-registry-ui.js
|
||||
fi
|
||||
|
||||
if [ -n "${CATALOG_ELEMENTS_LIMIT}" ] ; then
|
||||
sed -i -r "s/(catalogElementsLimit[:=])[^,;]*/\1${CATALOG_ELEMENTS_LIMIT}/" scripts/docker-registry-ui.js
|
||||
sed -i "s/\${DELETE_IMAGES}/false/" index.html
|
||||
else
|
||||
sed -i "s/\${DELETE_IMAGES}/true/" index.html
|
||||
fi
|
||||
|
||||
get_nginx_proxy_headers() {
|
||||
@@ -33,8 +30,8 @@ get_nginx_proxy_headers() {
|
||||
done
|
||||
}
|
||||
|
||||
if [ -n "${REGISTRY_URL}" ] ; then
|
||||
sed -i "s,\${REGISTRY_URL},${REGISTRY_URL}," /etc/nginx/conf.d/default.conf
|
||||
if [ -n "${NGINX_PROXY_PASS_URL}" ] ; then
|
||||
sed -i "s,\${NGINX_PROXY_PASS_URL},${NGINX_PROXY_PASS_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
|
||||
|
||||
@@ -6,8 +6,16 @@ for i in alpine chronograf:alpine consul debian jawg/mapnik3 nginx:alpine postgr
|
||||
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
|
||||
for i in arm64v8 arm32v7 master latest debian main; 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
|
||||
|
||||
for v in 1.1 1.2 1.3 1.4 1.5 2.0 ; do
|
||||
for type in "-debian" ""; do
|
||||
docker pull joxit/docker-registry-ui:$v$type
|
||||
docker tag joxit/docker-registry-ui:$v$type 127.0.0.1:5000/joxit/docker-registry-ui:$v$type
|
||||
docker push 127.0.0.1:5000/joxit/docker-registry-ui:$v$type
|
||||
done
|
||||
done
|
||||
@@ -1,26 +0,0 @@
|
||||
# Copyright (C) 2016-2018 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/>.
|
||||
FROM nginx:latest
|
||||
|
||||
LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY dist/scripts/docker-registry-ui-static.js /usr/share/nginx/html/scripts/docker-registry-ui.js
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
@@ -18,4 +18,9 @@ LABEL maintainer="Jones MAGLOIRE @Joxit"
|
||||
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
|
||||
ENV NGINX_PROXY_HEADER_Host '$http_host'
|
||||
|
||||
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
|
||||
COPY bin/entrypoint /docker-entrypoint.d/90-docker-registry-ui.sh
|
||||
COPY dist/ /usr/share/nginx/html/
|
||||
COPY favicon.ico /usr/share/nginx/html/
|
||||
|
||||
@@ -19,12 +19,13 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="../dist/vendor.css">
|
||||
<link rel="stylesheet" href="../dist/style.css">
|
||||
<link rel="stylesheet" href="../dist/docker-registry-ui.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta name="description" content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
|
||||
<meta property="og:description" content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
|
||||
<meta name="description"
|
||||
content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
|
||||
<meta property="og:description"
|
||||
content="This is the live demo for Docker Registry User Interface. Try it now! Sources : https://github.com/Joxit/docker-registry-ui" />
|
||||
<link rel="canonical" href="https://joxit.dev/docker-registry-ui/demo/" />
|
||||
<meta property="og:url" content="https://joxit.dev/docker-registry-ui/demo/" />
|
||||
<meta property="og:site_name" content="Live Demo | Docker Registry User Interface" />
|
||||
@@ -35,26 +36,20 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app></app>
|
||||
<docker-registry-ui registry-url="" name="Demo Docker Registry UI" pull-url="" show-content-digest="true"
|
||||
is-image-remove-activated="true" catalog-elements-limit="1000" single-registry="false">
|
||||
<script>
|
||||
if (!localStorage.getItem('registryServer')) {
|
||||
localStorage.setItem('registryServer', JSON.stringify([
|
||||
'https://raw.githubusercontent.com/Joxit/docker-registry-ui/master/demo'
|
||||
]))
|
||||
}
|
||||
</script>
|
||||
<script src="../dist/docker-registry-ui.js"></script>
|
||||
<script>
|
||||
if ((function() {
|
||||
try {
|
||||
const res = JSON.parse(localStorage.getItem('registryServer'));
|
||||
if (!(res instanceof Array) || res.length == 0) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) { return true; }
|
||||
})()) {
|
||||
localStorage.setItem('registryServer', JSON.stringify(['https://raw.githubusercontent.com/Joxit/docker-registry-ui/master/demo']))
|
||||
}
|
||||
|
||||
</script>
|
||||
<script src="../dist/scripts/vendor.js"></script>
|
||||
<script src="../dist/scripts/docker-registry-ui.js"></script>
|
||||
<script>
|
||||
(function(i, s, o, g, r, a, m) {
|
||||
(function (i, s, o, g, r, a, m) {
|
||||
i['GoogleAnalyticsObject'] = r;
|
||||
i[r] = i[r] || function() {
|
||||
i[r] = i[r] || function () {
|
||||
(i[r].q = i[r].q || []).push(arguments)
|
||||
}, i[r].l = 1 * new Date();
|
||||
a = s.createElement(o),
|
||||
|
||||
1
dist/docker-registry-ui.css
vendored
Normal file
20
dist/docker-registry-ui.js
vendored
Normal file
2
dist/images/docker-logo.svg
vendored
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" x="0px" y="0px" viewBox="-5.724 -43.601 730 600" fill="#777"><path d="m595.942,422.343 c -7.543,0.119 -13.562,6.331 -13.443,13.875 0.119,7.544 6.332,13.562 13.875,13.443 7.495,-0.118 13.494,-6.254 13.445,-13.75 -0.085,-7.578 -6.297,-13.652 -13.875,-13.568 0,0 -10e-4,0 0,0 m 0,24.398 c -5.975,0.272 -11.039,-4.352 -11.311,-10.326 -0.271,-5.976 4.352,-11.04 10.327,-11.312 5.975,-0.271 11.039,4.352 11.311,10.327 0.01,0.19 0.013,0.382 0.011,0.573 0.204,5.723 -4.27,10.527 -9.992,10.731 -0.115,0.005 -0.23,0.007 -0.346,0.007"/><path d="m599.081,436.342 v -0.185 c 1.512,-0.292 2.65,-1.544 2.8,-3.076 0.057,-1.175 -0.432,-2.311 -1.323,-3.077 -1.445,-0.765 -3.076,-1.106 -4.707,-0.984 -1.743,-0.024 -3.484,0.12 -5.2,0.431 v 13.538 h 3.077 v -5.446 h 1.477 c 1.754,0 2.554,0.646 2.83,2.154 0.184,1.143 0.536,2.252 1.047,3.292 h 3.415 c -0.53,-1.062 -0.873,-2.207 -1.016,-3.385 -0.138,-1.473 -1.088,-2.744 -2.462,-3.292 m -3.723,-0.985 h -1.508 v -3.908 c 0.583,-0.069 1.172,-0.069 1.754,0 1.97,0 2.893,0.831 2.893,2.062 0,1.231 -1.415,2 -3.076,2"/><path d="M707.494,193.557c-1.938-1.539-20.029-15.199-58.181-15.199c-10.074,0.044-20.127,0.908-30.061,2.584 c-7.384-50.612-49.228-75.288-51.104-76.395l-10.245-5.908l-6.738,9.723c-8.438,13.061-14.598,27.459-18.214,42.582 c-6.831,28.891-2.677,56.027,11.999,79.226c-17.722,9.876-46.151,12.307-51.904,12.522H22.367 c-12.294,0.017-22.27,9.952-22.337,22.245c-0.549,41.234,6.437,82.222,20.614,120.946c16.214,42.521,40.336,73.842,71.719,93.01 c35.167,21.537,92.302,33.844,157.067,33.844c29.258,0.092,58.461-2.556,87.226-7.907c39.986-7.342,78.463-21.318,113.839-41.352 c29.149-16.88,55.383-38.354,77.688-63.596c37.29-42.213,59.505-89.226,76.026-131.007c2.215,0,4.431,0,6.584,0 c40.828,0,65.935-16.338,79.78-30.029c9.201-8.732,16.384-19.369,21.045-31.167l2.923-8.553L707.494,193.557z"/><path d="M65.995,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.476-5.538 c-0.01,0-0.021,0-0.031,0H65.995c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C60.488,226.443,62.953,228.909,65.995,228.909L65.995,228.909"/><path d="M152.913,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.059,0-5.538,2.479-5.538,5.538v56.181C147.392,226.448,149.866,228.909,152.913,228.909"/><path d="M241.153,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C235.646,226.443,238.112,228.909,241.153,228.909L241.153,228.909"/><path d="M328.348,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C322.841,226.443,325.307,228.909,328.348,228.909L328.348,228.909"/><path d="M152.913,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.046,0-5.521,2.46-5.538,5.507v56.181C147.392,145.597,149.861,148.066,152.913,148.083"/><path d="M241.153,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C235.646,145.591,238.107,148.066,241.153,148.083"/><path d="M328.348,148.083h63.073c3.052-0.017,5.521-2.486,5.538-5.538V86.364c-0.017-3.047-2.491-5.507-5.538-5.507h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C322.841,145.591,325.302,148.066,328.348,148.083"/><path d="M328.348,67.227h63.073c3.047,0,5.521-2.461,5.538-5.507V5.507C396.942,2.46,394.468,0,391.421,0h-63.073 c-3.042,0-5.507,2.465-5.507,5.507l0,0v56.212C322.841,64.761,325.307,67.227,328.348,67.227"/><path d="M416.312,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.041,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C410.805,226.443,413.271,228.909,416.312,228.909"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2" x="0px" y="0px" viewBox="-5.724 -43.601 730 600" fill="#777"> <path d="m595.942,422.343 c -7.543,0.119 -13.562,6.331 -13.443,13.875 0.119,7.544 6.332,13.562 13.875,13.443 7.495,-0.118 13.494,-6.254 13.445,-13.75 -0.085,-7.578 -6.297,-13.652 -13.875,-13.568 0,0 -10e-4,0 0,0 m 0,24.398 c -5.975,0.272 -11.039,-4.352 -11.311,-10.326 -0.271,-5.976 4.352,-11.04 10.327,-11.312 5.975,-0.271 11.039,4.352 11.311,10.327 0.01,0.19 0.013,0.382 0.011,0.573 0.204,5.723 -4.27,10.527 -9.992,10.731 -0.115,0.005 -0.23,0.007 -0.346,0.007"/> <path d="m599.081,436.342 v -0.185 c 1.512,-0.292 2.65,-1.544 2.8,-3.076 0.057,-1.175 -0.432,-2.311 -1.323,-3.077 -1.445,-0.765 -3.076,-1.106 -4.707,-0.984 -1.743,-0.024 -3.484,0.12 -5.2,0.431 v 13.538 h 3.077 v -5.446 h 1.477 c 1.754,0 2.554,0.646 2.83,2.154 0.184,1.143 0.536,2.252 1.047,3.292 h 3.415 c -0.53,-1.062 -0.873,-2.207 -1.016,-3.385 -0.138,-1.473 -1.088,-2.744 -2.462,-3.292 m -3.723,-0.985 h -1.508 v -3.908 c 0.583,-0.069 1.172,-0.069 1.754,0 1.97,0 2.893,0.831 2.893,2.062 0,1.231 -1.415,2 -3.076,2"/> <path d="M707.494,193.557c-1.938-1.539-20.029-15.199-58.181-15.199c-10.074,0.044-20.127,0.908-30.061,2.584 c-7.384-50.612-49.228-75.288-51.104-76.395l-10.245-5.908l-6.738,9.723c-8.438,13.061-14.598,27.459-18.214,42.582 c-6.831,28.891-2.677,56.027,11.999,79.226c-17.722,9.876-46.151,12.307-51.904,12.522H22.367 c-12.294,0.017-22.27,9.952-22.337,22.245c-0.549,41.234,6.437,82.222,20.614,120.946c16.214,42.521,40.336,73.842,71.719,93.01 c35.167,21.537,92.302,33.844,157.067,33.844c29.258,0.092,58.461-2.556,87.226-7.907c39.986-7.342,78.463-21.318,113.839-41.352 c29.149-16.88,55.383-38.354,77.688-63.596c37.29-42.213,59.505-89.226,76.026-131.007c2.215,0,4.431,0,6.584,0 c40.828,0,65.935-16.338,79.78-30.029c9.201-8.732,16.384-19.369,21.045-31.167l2.923-8.553L707.494,193.557z"/> <path d="M65.995,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.476-5.538 c-0.01,0-0.021,0-0.031,0H65.995c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C60.488,226.443,62.953,228.909,65.995,228.909L65.995,228.909"/> <path d="M152.913,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.059,0-5.538,2.479-5.538,5.538v56.181C147.392,226.448,149.866,228.909,152.913,228.909"/> <path d="M241.153,228.909h63.073c3.042,0,5.507-2.466,5.507-5.507l0,0V167.22c0.017-3.042-2.435-5.521-5.477-5.538 c-0.01,0-0.021,0-0.031,0h-63.073c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181 C235.646,226.443,238.112,228.909,241.153,228.909L241.153,228.909"/> <path d="M328.348,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C322.841,226.443,325.307,228.909,328.348,228.909L328.348,228.909"/> <path d="M152.913,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.046,0-5.521,2.46-5.538,5.507v56.181C147.392,145.597,149.861,148.066,152.913,148.083"/> <path d="M241.153,148.083h63.073c3.046-0.017,5.507-2.492,5.507-5.538V86.364c0-3.042-2.466-5.507-5.507-5.507l0,0h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C235.646,145.591,238.107,148.066,241.153,148.083"/> <path d="M328.348,148.083h63.073c3.052-0.017,5.521-2.486,5.538-5.538V86.364c-0.017-3.047-2.491-5.507-5.538-5.507h-63.073 c-3.042,0-5.507,2.466-5.507,5.507l0,0v56.181C322.841,145.591,325.302,148.066,328.348,148.083"/> <path d="M328.348,67.227h63.073c3.047,0,5.521-2.461,5.538-5.507V5.507C396.942,2.46,394.468,0,391.421,0h-63.073 c-3.042,0-5.507,2.465-5.507,5.507l0,0v56.212C322.841,64.761,325.307,67.227,328.348,67.227"/> <path d="M416.312,228.909h63.073c3.047,0,5.521-2.46,5.538-5.507V167.22c0-3.059-2.479-5.538-5.538-5.538l0,0h-63.073 c-3.041,0-5.507,2.466-5.507,5.507c0,0.01,0,0.021,0,0.031v56.181C410.805,226.443,413.271,228.909,416.312,228.909"/> </svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.1 KiB |
6
dist/index.html
vendored
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2019 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2021 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
|
||||
@@ -13,4 +13,6 @@
|
||||
|
||||
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/>.
|
||||
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link rel="stylesheet" href="vendor.css"><link rel="stylesheet" href="style.css"><meta name="viewport" content="width=device-width,initial-scale=1"><meta property="og:site_name" content="Docker Registry UI"><meta name="twitter:card" content="summary"><meta name="twitter:site" content="@Joxit"><meta name="twitter:creator" content="@Jones Magloire"><title>Docker Registry UI</title></head><body><app></app><script src="scripts/vendor.js"></script><script src="scripts/docker-registry-ui.js"></script></body></html>
|
||||
--><!DOCTYPE html><html><head><meta charset="UTF-8"><link href="docker-registry-ui.css" rel="stylesheet" type="text/css"><meta name="viewport" content="width=device-width, initial-scale=1"><meta property="og:site_name" content="Docker Registry UI" /><meta name="twitter:card" content="summary" /><meta name="twitter:site" content="@Joxit" /><meta name="twitter:creator" content="@Jones Magloire" /><title>Docker Registry UI</title></head><body><docker-registry-ui registry-url="${REGISTRY_URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}"
|
||||
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}"
|
||||
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}"></docker-registry-ui><script src="docker-registry-ui.js"></script></body></html>
|
||||
18
dist/scripts/docker-registry-ui-static.js
vendored
18
dist/scripts/docker-registry-ui.js
vendored
5
dist/scripts/vendor.js
vendored
18
dist/style.css
vendored
1
dist/vendor.css
vendored
|
Before Width: | Height: | Size: 686 KiB After Width: | Height: | Size: 1.2 MiB |
@@ -1,13 +1,13 @@
|
||||
## 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/75))
|
||||
- [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))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/master/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/master/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/master/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
|
||||
- [Use docker-registry-ui as a proxy (use REGISTRY_URL)](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-proxy)
|
||||
- [Use docker-registry-ui as standalone (use URL)](https://github.com/Joxit/docker-registry-ui/tree/main/examples/ui-as-standalone)
|
||||
- [Use docker-registry-ui with traefik](https://github.com/Joxit/docker-registry-ui/tree/main/examples/traefik)
|
||||
- [Use docker-registry-ui with docker registry and Amazon s3](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-75) ([#75](https://github.com/Joxit/docker-registry-ui/issues/75))
|
||||
- [FIX revproxy to registry does not work when published under non-root url](https://github.com/Joxit/docker-registry-ui/tree/main/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/main/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/main/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/main/examples/proxy-headers) ([#89](https://github.com/Joxit/docker-registry-ui/pull/89))
|
||||
- [UI showing same sha256 content digest for all tags + Delete is not working](https://github.com/Joxit/docker-registry-ui/tree/main/examples/issue-116) ([#116](https://github.com/Joxit/docker-registry-ui/issues/116))
|
||||
- [Electron-based Standalone Application](https://github.com/Joxit/docker-registry-ui/tree/main/examples/electron) ([#129](https://github.com/Joxit/docker-registry-ui/pull/129))
|
||||
- [Use docker-registry-ui as proxy with read-only right](https://github.com/Joxit/docker-registry-ui/tree/main/examples/read-only-auth) ([#47](https://github.com/Joxit/docker-registry-ui/issues/47))
|
||||
@@ -18,16 +18,16 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.52",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
"keytar": "^5.6.0",
|
||||
"keytar": "^7.3.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"electron": "^8.0.0",
|
||||
"electron": "^11.2.3",
|
||||
"electron-builder": "^22.6.0",
|
||||
"electron-packager": "^14.2.1",
|
||||
"electron-rebuild": "^1.10.1",
|
||||
"electron-packager": "^15.2.0",
|
||||
"electron-rebuild": "^2.3.5",
|
||||
"parcel-bundler": "^1.12.4",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
|
||||
@@ -24,14 +24,15 @@ services:
|
||||
container_name: registry-srv
|
||||
|
||||
registry-ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
restart: always
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=Private Docker Registry
|
||||
- URL=http://localhost:5000
|
||||
- REGISTRY_URL=http://localhost:5000
|
||||
- DELETE_IMAGES=true
|
||||
- SINGLE_REGISTRY=true
|
||||
container_name: registry-ui
|
||||
|
||||
networks:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Example for issue #20 (HTTPS supports)
|
||||
|
||||
This example will override the original nginx conf with one supporting HTTPS. You will need to rewrite all the project configuration (replaces `proxy_pass` with our value).
|
||||
This example will override the original nginx conf with one supporting HTTPS. You will need to rewrite all the project configuration (replaces `proxy_pass` with your own value, in this example `http://registry:5000` is fine).
|
||||
|
||||
Generating a self signed certificate:
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ services:
|
||||
- registry-ui-net
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
environment:
|
||||
- SINGLE_REGISTRY=true
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -6,6 +6,9 @@ server {
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# disable any limits to avoid HTTP 413 for large image uploads
|
||||
client_max_body_size 0;
|
||||
|
||||
location /v2 {
|
||||
# Do not allow connections from docker 1.5 and earlier
|
||||
# docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
|
||||
@@ -19,6 +22,7 @@ server {
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
# Force HTTPS
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@ services:
|
||||
- registry-ui-net
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
environment:
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- REGISTRY_URL=http://registry:5000
|
||||
- NGINX_PROXY_PASS_URL=http://registry:5000
|
||||
- SINGLE_REGISTRY=true
|
||||
depends_on:
|
||||
- registry
|
||||
networks:
|
||||
|
||||
@@ -10,12 +10,13 @@ services:
|
||||
network_mode: host
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- URL=http://127.0.0.1:5000
|
||||
- REGISTRY_URL=http://127.0.0.1:5000
|
||||
- DELETE_IMAGES=true
|
||||
- SINGLE_REGISTRY=true
|
||||
depends_on:
|
||||
- registry
|
||||
network_mode: host
|
||||
|
||||
@@ -12,14 +12,15 @@ services:
|
||||
container_name: registry-srv
|
||||
|
||||
registry-ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=Private Docker Registry
|
||||
- REGISTRY_URL=http://registry-srv:5000
|
||||
- NGINX_PROXY_PASS_URL=http://registry-srv:5000
|
||||
- DELETE_IMAGES=true
|
||||
- SINGLE_REGISTRY=true
|
||||
networks:
|
||||
- registry-ui-net
|
||||
container_name: registry-ui
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
labels:
|
||||
app: registry
|
||||
release: docker-registry-ui
|
||||
app/version: "1.2.1"
|
||||
app/version: "2.0.0"
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
labels:
|
||||
app: registry
|
||||
release: docker-registry-ui
|
||||
app/version: "1.2.1"
|
||||
app/version: "2.0.0"
|
||||
name: docker-registry
|
||||
spec:
|
||||
accessModes:
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
labels:
|
||||
app: registry
|
||||
release: docker-registry-ui
|
||||
app/version: "1.2.1"
|
||||
app/version: "2.0.0"
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
labels:
|
||||
app: registry-ui
|
||||
release: docker-registry-ui
|
||||
app/version: "1.2.1"
|
||||
app/version: "2.0.0"
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
@@ -27,10 +27,12 @@ spec:
|
||||
value: "Docker registry UI"
|
||||
- name: DELETE_IMAGES
|
||||
value: "false"
|
||||
- name: REGISTRY_URL
|
||||
- name: NGINX_PROXY_PASS_URL
|
||||
value: "http://docker-registry.default:5000"
|
||||
- name: PULL_URL
|
||||
value: "docker-registry-ui.default:80"
|
||||
- name: SINGLE_REGISTRY
|
||||
value: "true"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
|
||||
@@ -5,7 +5,7 @@ metadata:
|
||||
labels:
|
||||
app: registry-ui
|
||||
release: docker-registry-ui
|
||||
app/version: "1.2.1"
|
||||
app/version: "2.0.0"
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
|
||||
@@ -10,13 +10,14 @@ services:
|
||||
- registry-ui-net
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- REGISTRY_URL=http://registry:5000
|
||||
- NGINX_PROXY_PASS_URL=http://registry:5000
|
||||
- NGINX_PROXY_HEADER_X_Forwarded_For=$$proxy_add_x_forwarded_for
|
||||
- SINGLE_REGISTRY=true
|
||||
volumes:
|
||||
- ./nginx.env:/etc/nginx/.env
|
||||
depends_on:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Docker registry with read only access
|
||||
|
||||
This is the configuration for a docker registry UI using `REGISTRY_URL` (as a proxy) with read only access to the registry.
|
||||
This example will override the original nginx conf with read only access to the registry. You will need to rewrite all the project configuration (replaces `proxy_pass` with your own value, in this example `http://registry:5000` is fine).
|
||||
|
||||
There are two htpasswd files. `read-write.htpasswd` a read and write access to the registry and `read-only.htpasswd` for a read only access.
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ services:
|
||||
- registry-ui-net
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- REGISTRY_URL=http://registry:5000
|
||||
- SINGLE_REGISTRY=true
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./read-write.htpasswd:/etc/nginx/auth/read-write.htpasswd:ro
|
||||
|
||||
2
examples/token-auth-keycloak/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
data/registry/
|
||||
data/keycloak/
|
||||
137
examples/token-auth-keycloak/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Token Authentication with Keycloak
|
||||
|
||||
In this example, we'll see how to configure your keycloak server and use token authentication with your registry. This will use the [docker registry v2 token authentication protocol](https://docs.docker.com/registry/spec/auth/token/).
|
||||
|
||||

|
||||
|
||||
In this image, we will replace the docker client/daemon by the Docker Registry UI. Here are the steps:
|
||||
|
||||
1. Attempt to get a resource (catalog, image info, image delete) with the registry.
|
||||
2. If the registry requires authorization it will return a `401 Unauthorized` HTTP response with information on how to authenticate.
|
||||
3. The **docker registry ui** makes a request to **keycloak** for a Bearer token.
|
||||
1. Your browser will use the [Basic Access Authentication Protocol](https://en.wikipedia.org/wiki/Basic_access_authentication#Protocol). But keycloak does not support this protocol... That's why we need a nginx proxy on top of keycloak.
|
||||
2. Your proxy will receive a request on `/auth/realms/{realm name}/protocol/docker-v2/auth` without `Authentication` header. It will return a `401 Unauthorized` HTTP response with `WWW-Authenticate` header.
|
||||
3. Your browser will ask you your credentials.
|
||||
4. The proxy will pass the credentials to keycloak.
|
||||
4. Keycloak returns an opaque Bearer token representing the client’s authorized access.
|
||||
5. The **docker registry ui** retries the original request with the Bearer token embedded in the request’s Authorization header.
|
||||
6. The Registry authorizes the client by validating the Bearer token and the claim set embedded within it and begins the session as usual.
|
||||
|
||||
:warning: If you are configuring from scratch your own keycloak server, remove files in `data` folder first with certificates in `conf/registry/localhost.*`
|
||||
|
||||
## Configure your nginx/proxy server
|
||||
|
||||
I will highlight required configuration for Basic Access Authentication Protocol. Replace the `{realm name}` by the name of your realm. In my example the realm is master, but you should create your own realm for your users.
|
||||
|
||||
```nginx
|
||||
resolver 127.0.0.11 valid=30s;
|
||||
set $keycloak "http://keycloak:8080";
|
||||
|
||||
# Location to get keycloak token
|
||||
location /auth/realms/{realm name}/protocol/docker-v2/auth {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
# By default, keycloak returns 400 instead of 401, we need to change that
|
||||
if ($http_authorization = "") {
|
||||
add_header WWW-Authenticate 'Basic realm="Keycloak login"' always;
|
||||
return 401;
|
||||
}
|
||||
proxy_pass $keycloak;
|
||||
}
|
||||
```
|
||||
|
||||
Start your nginx server. It will be available on http://localhost/ in my example.
|
||||
|
||||
```sh
|
||||
docker-compose up -d proxy
|
||||
```
|
||||
|
||||
## Configure your keycloak server
|
||||
|
||||
I will highlight required configuration for docker protocol. You will need to add this option to your keycloak command line:
|
||||
|
||||
```
|
||||
-Dkeycloak.profile.feature.docker=enabled
|
||||
```
|
||||
|
||||
Then the defalt user can be configured via environment variables
|
||||
```yml
|
||||
services:
|
||||
keycloak:
|
||||
image: jboss/keycloak
|
||||
environment:
|
||||
KEYCLOAK_USER: admin
|
||||
KEYCLOAK_PASSWORD: password
|
||||
user: root
|
||||
networks:
|
||||
- registry-ui-net
|
||||
command: -Dkeycloak.profile.feature.docker=enabled -b 0.0.0.0
|
||||
```
|
||||
|
||||
Now you can start your keycloak server, it will be available on http://localhost/auth in my example.
|
||||
|
||||
|
||||
```sh
|
||||
docker-compose up -d keycloak
|
||||
```
|
||||
|
||||
Now you need to configure your docker client with these steps:
|
||||
|
||||
Go to the keycloak home page: http://localhost/auth and click on `Administration Console`.
|
||||
|
||||

|
||||
|
||||
Sign in with your login and password (in my example it's `admin` and `password`).
|
||||
|
||||

|
||||
|
||||
Go to `Clients` in the left side menu.
|
||||
|
||||

|
||||
|
||||
Create a new client.
|
||||
|
||||

|
||||
|
||||
Enter a name for `Client ID`, choose `docker-v2` as the `Client Protocol`, and click `Save`.
|
||||
|
||||

|
||||
|
||||
Navigate to `Installation` tab, choose `Docker Compose YAML` as `Format Option` and click `Download`
|
||||
|
||||

|
||||
|
||||
When you extract the archive, the resulting directory should look like this.
|
||||
|
||||
```
|
||||
keycloak-docker-compose-yaml
|
||||
├── certs
|
||||
│ ├── localhost.crt
|
||||
│ ├── localhost.key
|
||||
│ └── localhost_trust_chain.pem
|
||||
├── data
|
||||
├── docker-compose.yaml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
Copy all the files from `certs` folder to `conf/registry` (this will replace files generated for this example).
|
||||
|
||||
## Configure your registry server
|
||||
|
||||
The last step is the configuration of your registry server. The config file is located in `conf/registry/config.yml`. The import part of the configuration is `auth.token` where you need to set `realm`, `service`, `issuer` and the `rootcertbundle` from the previous archive.
|
||||
|
||||
```yml
|
||||
auth:
|
||||
token:
|
||||
realm: http://localhost/auth/realms/{realm name}/protocol/docker-v2/auth
|
||||
service: docker-registry
|
||||
issuer: http://localhost/auth/realms/{realm name}
|
||||
rootcertbundle: /etc/docker/registry/localhost_trust_chain.pem
|
||||
```
|
||||
|
||||
Now you can start your docker registry with your docker registry ui.
|
||||
|
||||
```sh
|
||||
docker-compose up -d registry ui
|
||||
```
|
||||
78
examples/token-auth-keycloak/conf/proxy/nginx.conf
Normal file
@@ -0,0 +1,78 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
resolver 127.0.0.11 valid=30s;
|
||||
|
||||
set $keycloak "http://keycloak:8080";
|
||||
set $registry "http://registry:5000";
|
||||
set $ui "http://ui";
|
||||
|
||||
|
||||
#charset koi8-r;
|
||||
#access_log /var/log/nginx/host.access.log main;
|
||||
|
||||
# disable any limits to avoid HTTP 413 for large image uploads
|
||||
client_max_body_size 0;
|
||||
|
||||
# required to avoid HTTP 411: see Issue #1486 (https://github.com/moby/moby/issues/1486)
|
||||
chunked_transfer_encoding on;
|
||||
# required for strict SNI checking: see Issue #70 (https://github.com/Joxit/docker-registry-ui/issues/70)
|
||||
proxy_ssl_server_name on;
|
||||
proxy_buffering off;
|
||||
proxy_ignore_headers "X-Accel-Buffering";
|
||||
|
||||
location /v2 {
|
||||
# Do not allow connections from docker 1.5 and earlier
|
||||
# docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
|
||||
if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
|
||||
return 404;
|
||||
}
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_pass $registry;
|
||||
}
|
||||
|
||||
location /auth {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_pass $keycloak;
|
||||
}
|
||||
|
||||
location /auth/realms/master/protocol/docker-v2/auth {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
if ($http_authorization = "") {
|
||||
add_header WWW-Authenticate 'Basic realm="Keycloak login"' always;
|
||||
return 401;
|
||||
}
|
||||
proxy_pass $keycloak;
|
||||
}
|
||||
|
||||
location /ui {
|
||||
proxy_pass $ui;
|
||||
}
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
#error_page 404 /404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# deny access to .htaccess files, if Apache's document root
|
||||
# concurs with nginx's one
|
||||
#
|
||||
#location ~ /\.ht {
|
||||
# deny all;
|
||||
#}
|
||||
}
|
||||
27
examples/token-auth-keycloak/conf/registry/config.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
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', 'Accept']
|
||||
Access-Control-Max-Age: [1728000]
|
||||
Access-Control-Allow-Credentials: [true]
|
||||
Access-Control-Expose-Headers: ['Docker-Content-Digest']
|
||||
auth:
|
||||
token:
|
||||
realm: http://localhost/auth/realms/master/protocol/docker-v2/auth
|
||||
service: docker-registry
|
||||
issuer: http://localhost/auth/realms/master
|
||||
rootcertbundle: /etc/docker/registry/localhost_trust_chain.pem
|
||||
16
examples/token-auth-keycloak/conf/registry/localhost.crt
Normal file
@@ -0,0 +1,16 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICmzCCAYMCBgF3SzuJuTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0
|
||||
ZXIwHhcNMjEwMTI4MjMwMDI5WhcNMzEwMTI4MjMwMjA5WjARMQ8wDQYDVQQDDAZt
|
||||
YXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCliGzyYtDTTmaj
|
||||
QYso8nxIY2tO5ITnRbgDQIXMuAY5HMv2XglT2zSHJNfC/HYSilZPSd8Ee/0oxm/q
|
||||
On1Al3JENx21txUWOBe48CVzLlIYlUnIXqaFh0YyL6feUZaDKg1YSVGhSzHDI57X
|
||||
DcfnR+g0V5QxzIKzK624Lw7vqGvZz5e9sS9mTn9EZUmqQRQBerB5qrPnuDLxEbj4
|
||||
LPxqjuFyKQ4g8wooYlBNSFruRas3TpG/90Xy15pa9a3ofiVPZCt3IoaQGPw4Ah3O
|
||||
ygnelgEhWNRwROx4dErmW2l7dUQP8dbSz+qI4g04Wx3GjnZAlY7mt7LG8OncavsA
|
||||
Gp52m8QrAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGvqPm9nmEMfGYjWN6JlnM2x
|
||||
/YkKzvH6B+IeXz/j2bXKZqQoQ1up62HNM4e3GKSw/51Lge6QXqgOZmFSHABev8EV
|
||||
+vzDMfRLjBfV1RmhZXYCh6nje0d61jAa0Sn6CfsUllIQRt3Hn67qzPk1d6SnKSHA
|
||||
tsbh5+pCDivfJBRm7sJCv1y9dPP1rlaxAOZrVU8LEsJlTP3D0OScrDQv09CVonwi
|
||||
4W2bnLcB6aPW5Fw3gyY4TtXfcQzQqbV5Gjs9EZNA6Vczu+80U14T4VD9CDgC8yky
|
||||
2KY2pGClWEjM+dJnZ0440wXuGK/lJEN41SzfKxyfCBTJVebwOXsqsyonYYK8Pxo=
|
||||
-----END CERTIFICATE-----
|
||||
28
examples/token-auth-keycloak/conf/registry/localhost.key
Normal file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCliGzyYtDTTmaj
|
||||
QYso8nxIY2tO5ITnRbgDQIXMuAY5HMv2XglT2zSHJNfC/HYSilZPSd8Ee/0oxm/q
|
||||
On1Al3JENx21txUWOBe48CVzLlIYlUnIXqaFh0YyL6feUZaDKg1YSVGhSzHDI57X
|
||||
DcfnR+g0V5QxzIKzK624Lw7vqGvZz5e9sS9mTn9EZUmqQRQBerB5qrPnuDLxEbj4
|
||||
LPxqjuFyKQ4g8wooYlBNSFruRas3TpG/90Xy15pa9a3ofiVPZCt3IoaQGPw4Ah3O
|
||||
ygnelgEhWNRwROx4dErmW2l7dUQP8dbSz+qI4g04Wx3GjnZAlY7mt7LG8OncavsA
|
||||
Gp52m8QrAgMBAAECggEAS7VHztwvElXrT4Ost/+fpCQEckLGHlievq4GBAmunvRy
|
||||
vK1pqra5IY5SOFXrUrN+oijxHUXwFXQcv44ctywNEPH8Xp3rwQvKncUH+9QVrDSr
|
||||
WD8h/jROgKmXJ3E9W6QiEl0GPrT7f3qNLWKaKUeUBkx/9P7KUFQL0g+Dz0zCdw1k
|
||||
wzJNE+tJgspeInlgylmXsuT2A0lPkIDq6uxZY/yhaEG03dytX12Mmg5VcfIzjLWB
|
||||
cF0hKDbb+Pmu+tWbLHEwHg00Qj3k7Y21u9DrHRC0uBYrVsytSxDIuayRfwskAcIm
|
||||
fyMNAYVtOQxzdaDm2OTT0SAZJOOxa+ZYMGToEzzFcQKBgQDTp961U0RAlbfygA4a
|
||||
IDxSvLDaIQ1pZuDzHwm/8b7SNGWiTSCjSWXZFd35rOXian53u69ey61ZTIAyv1sQ
|
||||
c7R1yCfyg8YUTWZLbl1s5Bb+ekh64WEaMmf+eChcegsXt/kW/wQKM9DLRMAM4v5Q
|
||||
9g3VEMH5xuFdGRa2AwDVZrRB5wKBgQDINsNkPgxMq+B1GfnT6PpuyAWlb7GuUENc
|
||||
yAucIBMGKdY8QlwnQmAaKPTl1t/MFsiiwUhRJiyXurG+skd3BmMPdACWnXa7nKBW
|
||||
XoXM6MRhS1QrDds6hid1usO86fB50UhupSr3tkHdeWy8l1erll/rhrParSyer1iK
|
||||
AjLfwx2rHQKBgQCaGTehpv0jVJ43tZoO1Xd1+aF9PuFH4zpWaDut/zEiVDnHAAaK
|
||||
O+8mLbCOjp5UyZpITGKzTvFn+bXAvOdtRACYXGERRXWa5Htc4f6tQCepoZhRtvP+
|
||||
ocJrWEpygfy/iReW8ZacYvtaczSsbTwh7/NENE42L+F26cRKQkeCF6OX8wKBgQDF
|
||||
Y1hXp+SwYnO0f5uSlIryVTlb1TazyGXhP0hS8DxRQ0X3uuTnv8THhcGMJ8AUkhHU
|
||||
hAIsHxqvrFw4ycMzUZSwU4mQ9EVuygg5no8DaijSU1Xz7IFKvaCBrVP1GB8BupdS
|
||||
nnwyI/njxCaz9/FzNZnztqXy3fCzseP0jB5kBRVm8QKBgEDbzlXPdF5Jr4mb3+Pt
|
||||
hqhAP1Swx2vwIyooDqhP0D5nUV1EDJ6Hqwb6L9R0Apt2gLW0YD1ETh9TsqUOWbIm
|
||||
kiE9IDHZFbtBj014clx3if6snKarrjaHQJ9THzB5uwtv8b3/k81T/IyAnPmXobQx
|
||||
Rrw+6hjfOdQsT90raYyhRWWx
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,16 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICmzCCAYMCBgF3SzrlgjANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0
|
||||
ZXIwHhcNMjEwMTI4MjI1OTQ3WhcNMzEwMTI4MjMwMTI3WjARMQ8wDQYDVQQDDAZt
|
||||
YXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHFs8UdWQrDZ/K
|
||||
RMvX/bu4SYa4fq38OHCu/ci8c/Vjuz8A4TI6hn84i82WLdHsiXpi4PO4YnGxTxHF
|
||||
vI8cAarLW2aYv8ZK81YP63kPUMGS3dMtQE8CRTAtHBOVXrxp6ab18kTeQZ/KxiXy
|
||||
nQM4rM7nSy+zoCY1AcPAvzLz0uqD/D8jBvsR90mrnHGrS2cJZCvaKkwfpMddUBOv
|
||||
qnlrDjiv0T5PE0fShvZWlUJn4tVhjwXGv6Nu32o87BnEE+6LoQek8YqaXohQLE3V
|
||||
MVFOHnxmCr7JwMk1Cdr+R2WyHleeC+FTL+O+kcpAzz9DIAhhUs2bHMR2OJAP3l4X
|
||||
PfPPGeWhAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIy0ru1wK6HTqO2UrNxa/Yqa
|
||||
GXdppS5tj2ssivKBqTc88TK2n8ORUyVVDeGVvicgASkru7ZjepR6APhdDG5Gy44x
|
||||
xnj/207txc6YXz+TS/2JO5SMNWrwQCwmjT9Ld4HtjEzzxt3O7tGUmSpRIAoRo4Zy
|
||||
gpaGuiHvJ0twhZWwcFS4sEYuGfF3uQ+hR2MQuSVm9El6GihY6c7dpv7E5GL71dDm
|
||||
VPjuN9/rkxEUvFl0EHZByfUpqXnVhujEDgw8eSyOleIFNJ0vEKKJRnbwcIi2SQ2z
|
||||
zPZjXKdr03R7YukmdMV5X7Swn7ehwF6AJijw6zpCoKcaiOQYexGMXxasK2xXw90=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1 @@
|
||||
cc04bebe-cb07-4384-b886-3670f757fd2e
|
||||
BIN
examples/token-auth-keycloak/data/keycloak/keycloak.mv.db
Normal file
163
examples/token-auth-keycloak/data/keycloak/keycloak.trace.db
Normal file
@@ -0,0 +1,163 @@
|
||||
2021-01-28 22:43:31 jdbc[3]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Table "DATABASECHANGELOGLOCK" not found; SQL statement:
|
||||
select count(*) from PUBLIC.DATABASECHANGELOGLOCK [42102-197]
|
||||
2021-01-28 22:43:32 jdbc[4]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Table "DATABASECHANGELOG" not found; SQL statement:
|
||||
select count(*) from PUBLIC.DATABASECHANGELOG [42102-197]
|
||||
2021-01-28 22:43:32 jdbc[4]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Table "DATABASECHANGELOG" not found; SQL statement:
|
||||
select count(*) from PUBLIC.DATABASECHANGELOG [42102-197]
|
||||
2021-01-28 22:44:05 jdbc[4]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
|
||||
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
|
||||
at java.base/java.lang.Thread.run(Thread.java:834)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
|
||||
2021-01-28 22:44:05 jdbc[3]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
|
||||
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
|
||||
at java.base/java.lang.Thread.run(Thread.java:834)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
|
||||
2021-01-28 23:16:37 jdbc[5]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
|
||||
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
|
||||
at java.base/java.lang.Thread.run(Thread.java:834)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
|
||||
2021-01-28 23:16:37 jdbc[4]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
|
||||
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
|
||||
at java.base/java.lang.Thread.run(Thread.java:834)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
|
||||
2021-01-28 23:16:37 jdbc[3]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
|
||||
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1348)
|
||||
at java.base/java.lang.Thread.run(Thread.java:834)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
|
||||
2021-01-28 23:17:00 jdbc[4]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
|
||||
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
|
||||
at java.base/java.lang.Thread.run(Thread.java:834)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
|
||||
2021-01-28 23:17:00 jdbc[3]: exception
|
||||
org.h2.jdbc.JdbcSQLException: Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-197]
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.getJdbcSQLException(DbException.java:357)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:179)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:155)
|
||||
at com.h2database.h2@1.4.197//org.h2.message.DbException.get(DbException.java:144)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosed(JdbcConnection.java:1526)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.checkClosedForWrite(JdbcConnection.java:1512)
|
||||
at com.h2database.h2@1.4.197//org.h2.jdbc.JdbcConnection.rollback(JdbcConnection.java:516)
|
||||
at org.jboss.ironjacamar.jdbcadapters@1.4.23.Final//org.jboss.jca.adapters.jdbc.BaseWrapperManagedConnection.destroy(BaseWrapperManagedConnection.java:485)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.listener.AbstractConnectionListener.destroy(AbstractConnectionListener.java:637)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.mcp.SemaphoreConcurrentLinkedDequeManagedConnectionPool.shutdown(SemaphoreConcurrentLinkedDequeManagedConnectionPool.java:1069)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.pool.AbstractPool.shutdown(AbstractPool.java:945)
|
||||
at org.jboss.ironjacamar.impl@1.4.23.Final//org.jboss.jca.core.connectionmanager.AbstractConnectionManager.shutdown(AbstractConnectionManager.java:321)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService.stopService(AbstractDataSourceService.java:232)
|
||||
at org.jboss.as.connector@21.0.2.Final//org.jboss.as.connector.subsystems.datasources.AbstractDataSourceService$1.run(AbstractDataSourceService.java:186)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
|
||||
at java.base/java.lang.Thread.run(Thread.java:834)
|
||||
at org.jboss.threads@2.4.0.Final//org.jboss.threads.JBossThread.run(JBossThread.java:513)
|
||||
41
examples/token-auth-keycloak/docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
version: '2.4'
|
||||
services:
|
||||
keycloak:
|
||||
image: jboss/keycloak
|
||||
environment:
|
||||
KEYCLOAK_USER: admin
|
||||
KEYCLOAK_PASSWORD: password
|
||||
user: root
|
||||
volumes:
|
||||
- ./data/keycloak:/opt/jboss/keycloak/standalone/data/
|
||||
networks:
|
||||
- registry-ui-net
|
||||
command: -Dkeycloak.profile.feature.docker=enabled -b 0.0.0.0
|
||||
|
||||
proxy:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- 80:80
|
||||
volumes:
|
||||
- ./conf/proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
networks:
|
||||
- registry-ui-net
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui
|
||||
environment:
|
||||
REGISTRY_TITLE: My Private Docker Registry
|
||||
REGISTRY_URL: http://localhost
|
||||
SINGLE_REGISTRY: 'true'
|
||||
networks:
|
||||
- registry-ui-net
|
||||
|
||||
registry:
|
||||
image: registry:2.7
|
||||
volumes:
|
||||
- ./conf/registry/:/etc/docker/registry/
|
||||
- ./data/registry:/var/lib/registry
|
||||
networks:
|
||||
- registry-ui-net
|
||||
networks:
|
||||
registry-ui-net:
|
||||
BIN
examples/token-auth-keycloak/images/01-keycloak-home.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
examples/token-auth-keycloak/images/02-keycloak-signin.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
examples/token-auth-keycloak/images/03-keycloak-to-clients.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 72 KiB |
BIN
examples/token-auth-keycloak/images/05-keycloak-new-client.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
examples/token-auth-keycloak/images/06-keycloak-download.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
@@ -12,11 +12,12 @@ services:
|
||||
constraints: [node.role == manager]
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
environment:
|
||||
- DELETE_IMAGES=true
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- REGISTRY_URL=http://docker-registry_registry:5000
|
||||
- NGINX_PROXY_PASS_URL=http://docker-registry_registry:5000
|
||||
- SINGLE_REGISTRY=true
|
||||
depends_on: ['registry']
|
||||
networks: ['proxy', 'default']
|
||||
deploy:
|
||||
|
||||
@@ -10,12 +10,13 @@ services:
|
||||
- registry-ui-net
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- REGISTRY_URL=http://registry:5000
|
||||
- NGINX_PROXY_PASS_URL=http://registry:5000
|
||||
- SINGLE_REGISTRY=true
|
||||
depends_on:
|
||||
- registry
|
||||
networks:
|
||||
|
||||
@@ -8,12 +8,13 @@ services:
|
||||
- registry-ui-net
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- REGISTRY_URL=http://registry:5000
|
||||
- NGINX_PROXY_PASS_URL=http://registry:5000
|
||||
- SINGLE_REGISTRY=true
|
||||
depends_on:
|
||||
- registry
|
||||
networks:
|
||||
|
||||
@@ -13,7 +13,7 @@ docker-compose -f simple.yml up -d
|
||||
./populate.sh
|
||||
```
|
||||
|
||||
You can add some credentials to access your registry wit `credentials.yml` docker-compose file.
|
||||
You can add some credentials to access your registry with `credentials.yml` docker-compose file.
|
||||
Credentials for this example are login: `registry` and password: `ui` using bcrypt.
|
||||
|
||||
```sh
|
||||
|
||||
@@ -10,11 +10,12 @@ services:
|
||||
- ./registry-config/htpasswd:/etc/docker/registry/htpasswd
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- URL=http://localhost:5000
|
||||
- REGISTRY_URL=http://localhost:5000
|
||||
- SINGLE_REGISTRY=true
|
||||
depends_on:
|
||||
- registry
|
||||
@@ -9,11 +9,12 @@ services:
|
||||
- ./registry-config/simple.yml:/etc/docker/registry/config.yml
|
||||
|
||||
ui:
|
||||
image: joxit/docker-registry-ui:static
|
||||
image: joxit/docker-registry-ui:latest
|
||||
ports:
|
||||
- 80:80
|
||||
environment:
|
||||
- REGISTRY_TITLE=My Private Docker Registry
|
||||
- URL=http://localhost:5000
|
||||
- REGISTRY_URL=http://localhost:5000
|
||||
- SINGLE_REGISTRY=true
|
||||
depends_on:
|
||||
- registry
|
||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 296 KiB |
123
gulpfile.js
@@ -1,123 +0,0 @@
|
||||
'use strict';
|
||||
const cleanCSS = require('gulp-clean-css');
|
||||
const concat = require('gulp-concat');
|
||||
const del = require('del');
|
||||
const filter = require('gulp-filter');
|
||||
const gIf = require('gulp-if');
|
||||
const gulp = require('gulp');
|
||||
const parallel = gulp.parallel;
|
||||
const series = gulp.series;
|
||||
const htmlmin = require('gulp-htmlmin');
|
||||
const license = require('gulp-license');
|
||||
const riot = require('gulp-riot');
|
||||
const uglify = require('uglify-es');
|
||||
const minifier = require('gulp-uglify/composer')(uglify);
|
||||
const useref = require('gulp-useref');
|
||||
const injectVersion = require('gulp-inject-version');
|
||||
const merge = require('stream-series');
|
||||
|
||||
const allTags = ['src/tags/*.riot', 'src/tags/dialogs/*.riot'];
|
||||
|
||||
const allScripts = [
|
||||
'src/scripts/http.js',
|
||||
'src/scripts/script.js',
|
||||
'src/scripts/utils.js'
|
||||
];
|
||||
|
||||
const staticTags = ['src/tags/*.riot'];
|
||||
|
||||
const staticScripts = [
|
||||
'src/scripts/http.js',
|
||||
'src/scripts/static.js',
|
||||
'src/scripts/utils.js'
|
||||
];
|
||||
|
||||
function html() {
|
||||
var htmlFilter = filter('**/*.html', {restore: true});
|
||||
return gulp.src(['src/index.html'])
|
||||
.pipe(useref())
|
||||
.pipe(gIf(['*.js', '!*.min.js'], minifier())) // FIXME
|
||||
.pipe(htmlFilter)
|
||||
.pipe(htmlmin({
|
||||
removeComments: false,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeEmptyAttributes: true,
|
||||
minifyJS: uglify
|
||||
}))
|
||||
.pipe(htmlFilter.restore)
|
||||
.pipe(gulp.dest('dist'));
|
||||
};
|
||||
|
||||
function clean() {
|
||||
return del(['dist']);
|
||||
};
|
||||
|
||||
function appStatic() {
|
||||
return merge(gulp.src(staticScripts), gulp.src(staticTags).pipe(riot()))
|
||||
.pipe(concat('docker-registry-ui-static.js'))
|
||||
.pipe(minifier())
|
||||
.pipe(license('agpl3', {
|
||||
tiny: false,
|
||||
project: 'docker-registry-ui',
|
||||
year: '2016-2019',
|
||||
organization: 'Jones Magloire @Joxit'
|
||||
}))
|
||||
.pipe(injectVersion())
|
||||
.pipe(gulp.dest('dist/scripts'));
|
||||
};
|
||||
|
||||
function app() {
|
||||
return merge(gulp.src(allScripts), gulp.src(allTags).pipe(riot()))
|
||||
.pipe(concat('docker-registry-ui.js'))
|
||||
.pipe(minifier())
|
||||
.pipe(license('agpl3', {
|
||||
tiny: false,
|
||||
project: 'docker-registry-ui',
|
||||
year: '2016-2019',
|
||||
organization: 'Jones Magloire @Joxit'
|
||||
}))
|
||||
.pipe(injectVersion())
|
||||
.pipe(gulp.dest('dist/scripts'));
|
||||
};
|
||||
|
||||
function vendor() {
|
||||
return gulp.src(['node_modules/riot/riot.min.js', 'node_modules/riot-route/dist/route.min.js', 'node_modules/riot-mui/build/js/riot-mui-min.js'])
|
||||
.pipe(concat('vendor.js'))
|
||||
.pipe(gulp.dest('dist/scripts'));
|
||||
};
|
||||
|
||||
function styles() {
|
||||
return gulp.src(['src/*.css'])
|
||||
.pipe(concat('style.css'))
|
||||
.pipe(cleanCSS({
|
||||
compatibility: 'ie8'
|
||||
}))
|
||||
.pipe(license('agpl3', {
|
||||
tiny: false,
|
||||
project: 'docker-registry-ui',
|
||||
year: '2016-2019',
|
||||
organization: 'Jones Magloire @Joxit'
|
||||
}))
|
||||
.pipe(gulp.dest('dist/'));
|
||||
};
|
||||
|
||||
function fonts() {
|
||||
return gulp.src('src/fonts/*')
|
||||
.pipe(filter('**/*.{otf,eot,svg,ttf,woff,woff2}'))
|
||||
.pipe(gulp.dest('dist/fonts'));
|
||||
};
|
||||
|
||||
function svgs() {
|
||||
return gulp.src(['src/images/*.svg'])
|
||||
.pipe(htmlmin({
|
||||
removeComments: false,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeEmptyAttributes: true,
|
||||
minifyJS: uglify
|
||||
}))
|
||||
.pipe(gulp.dest('dist/images/'));
|
||||
};
|
||||
|
||||
exports.build = series(clean, html, parallel(fonts, styles, vendor, app, appStatic, svgs));
|
||||
@@ -27,7 +27,7 @@ server {
|
||||
#! return 404;
|
||||
#! }
|
||||
#! ${NGINX_PROXY_HEADERS}
|
||||
#! proxy_pass ${REGISTRY_URL};
|
||||
#! proxy_pass ${NGINX_PROXY_PASS_URL};
|
||||
#! }
|
||||
|
||||
#error_page 404 /404.html;
|
||||
|
||||
46
package.json
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "docker-registry-ui",
|
||||
"version": "1.5.2",
|
||||
"version": "2.0.2",
|
||||
"scripts": {
|
||||
"build": "./node_modules/gulp/bin/gulp.js build",
|
||||
"start": "ROLLUP_SERVE=true rollup -c -w",
|
||||
"build": "rollup -c",
|
||||
"build:electron": "npm run build && cd examples/electron && npm install && npm run dist"
|
||||
},
|
||||
"repository": {
|
||||
@@ -12,24 +13,29 @@
|
||||
"author": "Jones Magloire (Joxit)",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A web UI for private docker registry",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"del": "^3.0.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-clean-css": "^4.2.0",
|
||||
"gulp-concat": "^2.6.0",
|
||||
"gulp-filter": "^5.1.0",
|
||||
"gulp-htmlmin": "^5.0.1",
|
||||
"gulp-if": "^2.0.0",
|
||||
"gulp-inject-version": "^1.0.1",
|
||||
"gulp-license": "^1.1.0",
|
||||
"gulp-riot": "^1.1.5",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"gulp-useref": "^3.1.6",
|
||||
"riot": "^3.13.2",
|
||||
"riot-mui": "^0.1.1",
|
||||
"riot-route": "^3.1.4",
|
||||
"stream-series": "^0.1.1",
|
||||
"uglify-es": "^3.3.10"
|
||||
"@babel/core": "^7.12.9",
|
||||
"@babel/preset-env": "^7.12.7",
|
||||
"@riotjs/compiler": "^5.3.1",
|
||||
"@riotjs/observable": "^4.0.4",
|
||||
"@riotjs/route": "^7.0.0",
|
||||
"@rollup/plugin-babel": "^5.2.2",
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
"@rollup/plugin-html": "^0.2.3",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^11.0.0",
|
||||
"core-js": "^3.9.1",
|
||||
"js-beautify": "^1.13.0",
|
||||
"riot": "^5.3.1",
|
||||
"riot-mui": "joxit/riot-5-mui#4d68d7f",
|
||||
"rollup": "^2.34.2",
|
||||
"rollup-plugin-app-utils": "^1.0.6",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-riot": "^5.0.0",
|
||||
"rollup-plugin-scss": "^2.6.1",
|
||||
"rollup-plugin-serve": "^1.1.0",
|
||||
"rollup-plugin-styles": "^3.14.1",
|
||||
"rollup-plugin-terser": "^7.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
53
rollup.config.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import riot from 'rollup-plugin-riot';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
import { emptyDirectories } from 'rollup-plugin-app-utils';
|
||||
import { babel } from '@rollup/plugin-babel';
|
||||
import scss from 'rollup-plugin-scss';
|
||||
import serve from 'rollup-plugin-serve';
|
||||
import html from '@rollup/plugin-html';
|
||||
import htmlUseref from './rollup/html-useref';
|
||||
import json from '@rollup/plugin-json';
|
||||
import copy from 'rollup-plugin-copy';
|
||||
import copyTransform from './rollup/copy-transform';
|
||||
import license from './rollup/license';
|
||||
|
||||
const useServe = process.env.ROLLUP_SERVE === 'true';
|
||||
const output = useServe ? '.serve' : 'dist';
|
||||
|
||||
const plugins = [
|
||||
riot(),
|
||||
json(),
|
||||
nodeResolve(),
|
||||
commonjs(),
|
||||
scss({ output: `./${output}/docker-registry-ui.css`, outputStyle: 'compressed' }),
|
||||
babel({ babelHelpers: 'bundled', presets: [['@babel/env', { useBuiltIns: 'usage', corejs: { version: '2' } }]] }),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: 'src/fonts', dest: `${output}` },
|
||||
{ src: 'src/images/*', dest: `${output}/images`, transform: copyTransform },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
if (useServe) {
|
||||
plugins.push(serve({ host: 'localhost', port: 8000, contentBase: [output, './'] }));
|
||||
} else {
|
||||
plugins.push(terser({ format: { preamble: license } }));
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
input: { 'docker-registry-ui': 'src/index.js' },
|
||||
output: {
|
||||
dir: output,
|
||||
name: 'DockerRegistryUI',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [emptyDirectories(output)].concat(
|
||||
plugins,
|
||||
html({ template: () => htmlUseref('./src/index.html', { developement: useServe, production: !useServe }) })
|
||||
),
|
||||
},
|
||||
];
|
||||
6
rollup/copy-transform.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default function (contents, name) {
|
||||
if (name.endsWith('.svg')) {
|
||||
return contents.toString('utf8').split(/\n */).join(' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
26
rollup/html-useref.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import fs from 'fs';
|
||||
|
||||
const useref = /<!--\s*build:([a-z]+) ([-a-zA-Z./]+)\s*-->(.*?)<!--\s*endbuild\s*-->/ms;
|
||||
|
||||
const generateBalise = (type, output, body, opts = {}) => {
|
||||
switch (type) {
|
||||
case 'css':
|
||||
return `<link href="${output}" rel="stylesheet" type="text/css">`;
|
||||
case 'js':
|
||||
return `<script src="${output}"></script>`;
|
||||
case 'keep':
|
||||
return opts[output] ? body : '';
|
||||
}
|
||||
};
|
||||
|
||||
export default function (src, opts) {
|
||||
let html = fs
|
||||
.readFileSync(src)
|
||||
.toString()
|
||||
.replace(/>\n+\s*/g, '>');
|
||||
while (useref.test(html)) {
|
||||
const [raw, type, output, body] = useref.exec(html);
|
||||
html = html.replace(raw, generateBalise(type, output, body, opts));
|
||||
}
|
||||
return html;
|
||||
}
|
||||
18
rollup/license.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export default `/*
|
||||
* Copyright (C) 2016-2021 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/>.
|
||||
*
|
||||
* @license AGPL
|
||||
*/`
|
||||
85
src/components/catalog/catalog-element.riot
Normal file
@@ -0,0 +1,85 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<catalog-element>
|
||||
<!-- Begin of tag -->
|
||||
<div class="content"
|
||||
if="{!props.filterResults || state.nImages > 0 || matchSearch(props.filterResults, state.image)}">
|
||||
<material-card class="list highlight" expanded="{state.expanded}" onclick="{ onClick }">
|
||||
<material-waves onmousedown="{this.triggerLaunch}" center="true" color="#ddd"
|
||||
setLaunchListener="{ setLaunchListener }" />
|
||||
<span>
|
||||
<i class="material-icons">send</i>
|
||||
{ state.image || state.repo }
|
||||
<div if="{state.images}" class="item-count right">
|
||||
{ state.nImages } images
|
||||
<i class="material-icons animated {state.expanded ? 'expanded' : ''}">expand_more</i>
|
||||
</div>
|
||||
</span>
|
||||
</material-card>
|
||||
<catalog-element if="{ state.images }" filter-results="{ props.filterResults }"
|
||||
class="animated {!state.expanded && !props.filterResults ? 'hide' : ''} {state.expanding ? 'expanding' : ''}"
|
||||
each="{item in state.images}" item="{ item }" />
|
||||
</div>
|
||||
<script>
|
||||
import router from '../../scripts/router';
|
||||
import {
|
||||
matchSearch
|
||||
} from '../search-bar.riot';
|
||||
|
||||
export default {
|
||||
onBeforeMount(props, state) {
|
||||
if (props.item.images && props.item.images.length === 1) {
|
||||
state.image = props.item.images[0];
|
||||
} else if (typeof props.item === 'string') {
|
||||
state.image = props.item;
|
||||
} else if (props.item.images && props.item.repo) {
|
||||
state.images = props.item.images;
|
||||
state.repo = props.item.repo;
|
||||
state.nImages = props.item.images.length;
|
||||
}
|
||||
},
|
||||
onBeforeUpdate(props, state) {
|
||||
if (props.filterResults && state.images) {
|
||||
state.nImages = state.images.filter(image => matchSearch(props.filterResults, image)).length;
|
||||
} else {
|
||||
state.nImages = state.images && state.images.length;
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
const state = this.state;
|
||||
if (!state.repo) {
|
||||
router.taglist(state.image);
|
||||
} else {
|
||||
this.update({
|
||||
expanded: !this.state.expanded,
|
||||
expanding: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.update({
|
||||
expanding: false
|
||||
});
|
||||
}, 50)
|
||||
}
|
||||
},
|
||||
setLaunchListener(cb) {
|
||||
this.triggerLaunch = cb;
|
||||
},
|
||||
matchSearch
|
||||
}
|
||||
</script>
|
||||
<!-- End of tag -->
|
||||
</catalog-element>
|
||||
103
src/components/catalog/catalog.riot
Normal file
@@ -0,0 +1,103 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<catalog>
|
||||
<material-card ref="catalog-tag" class="catalog header">
|
||||
<div class="material-card-title-action">
|
||||
<h2>
|
||||
Repositories of { state.registryName }
|
||||
<div class="item-count">{ state.nImages } images in { state.nRepositories } repositories</div>
|
||||
</h2>
|
||||
</div>
|
||||
</material-card>
|
||||
<div if="{ !state.loadend }" class="spinner-wrapper">
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
<catalog-element each="{ item in state.repositories }" item="{ item }" filter-results="{ props.filterResults }"/>
|
||||
<script>
|
||||
import CatalogElement from './catalog-element.riot'
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CatalogElement
|
||||
},
|
||||
state: {
|
||||
registryName: '',
|
||||
length: 0,
|
||||
loadend: false,
|
||||
repositories: []
|
||||
},
|
||||
|
||||
onBeforeMount(props) {
|
||||
this.state.registryName = props.registryName;
|
||||
this.state.catalogElementsLimit = props.catalogElementsLimit;
|
||||
},
|
||||
onMounted(props) {
|
||||
this.display(props, this.state)
|
||||
},
|
||||
|
||||
display(props, state) {
|
||||
let repositories = [];
|
||||
const self = this;
|
||||
const oReq = new Http({
|
||||
onAuthentication: this.props.onAuthentication
|
||||
});
|
||||
oReq.addEventListener('load', function () {
|
||||
if (this.status == 200) {
|
||||
repositories = JSON.parse(this.responseText).repositories || [];
|
||||
repositories.sort();
|
||||
repositories = repositories.reduce(function (acc, e) {
|
||||
const slash = e.indexOf('/');
|
||||
if (slash > 0) {
|
||||
const repoName = e.substring(0, slash) + '/';
|
||||
if (acc.length == 0 || acc[acc.length - 1].repo != repoName) {
|
||||
acc.push({
|
||||
repo: repoName,
|
||||
images: []
|
||||
});
|
||||
}
|
||||
acc[acc.length - 1].images.push(e);
|
||||
return acc;
|
||||
}
|
||||
acc.push(e);
|
||||
return acc;
|
||||
}, []);
|
||||
} else if (this.status == 404) {
|
||||
self.props.onNotify('Server not found', true);
|
||||
} else {
|
||||
self.props.onNotify(this.responseText);
|
||||
}
|
||||
});
|
||||
oReq.addEventListener('error', function () {
|
||||
self.props.onNotify(this.getErrorMessage(), true);
|
||||
});
|
||||
oReq.addEventListener('loadend', function () {
|
||||
self.update({
|
||||
repositories,
|
||||
nRepositories: repositories.length,
|
||||
nImages: repositories.reduce((acc, e) => acc + (e.images && e.images.length || 1), 0),
|
||||
loadend: true
|
||||
});
|
||||
});
|
||||
oReq.open('GET', `${props.registryUrl}/v2/_catalog?n=${state.catalogElementsLimit}`);
|
||||
oReq.send();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</catalog>
|
||||
64
src/components/dialogs/add-registry-url.riot
Normal file
@@ -0,0 +1,64 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<add-registry-url>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
|
||||
<div slot="title">Add your Server ?</div>
|
||||
<div slot="content">
|
||||
<material-input onkeyup="{ onKeyUp }" placeholder="Server URL"></material-input>
|
||||
<span>Write your URL without /v2</span>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ add }">
|
||||
Add
|
||||
</material-button>
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
|
||||
Cancel
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
getRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
import router from '../../scripts/router';
|
||||
|
||||
export default {
|
||||
onKeyUp(event) {
|
||||
// if keyCode is Enter
|
||||
if (event.keyCode === 13) {
|
||||
this.add();
|
||||
}
|
||||
},
|
||||
add() {
|
||||
const input = this.$('input');
|
||||
if (!input || !input.value || input.value.length === 0) {
|
||||
return this.props.onNotify('The input field is empty. Please enter an url.', true);
|
||||
}
|
||||
if (!input.value.startsWith('http')) {
|
||||
return this.props.onNotify('The input field should start with http:// or https://.', true);
|
||||
}
|
||||
const url = input.value.trim().replace(/\/*$/, '');
|
||||
const registryServer = getRegistryServers().filter(e => e !== url);
|
||||
localStorage.setItem('registryServer', JSON.stringify([url].concat(registryServer)));
|
||||
router.home()
|
||||
this.props.onServerChange(url);
|
||||
this.props.onClose()
|
||||
setTimeout(() => router.updateUrlQueryParam(url), 100);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</add-registry-url>
|
||||
78
src/components/dialogs/change-registry-url.riot
Normal file
@@ -0,0 +1,78 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<change-registry-url>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClick }">
|
||||
<div slot="title">Change your Server ?</div>
|
||||
<div slot="content">
|
||||
<select>
|
||||
<option each="{ url in getRegistryServers() }" value="{ url }">{ url }</option>
|
||||
</select>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ change }">
|
||||
Change
|
||||
</material-button>
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
|
||||
Cancel
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
getRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
change(event) {
|
||||
const select = this.$('select');
|
||||
if (!select || !select.value || select.value.length === 0) {
|
||||
return this.props.onNotify('The select field is empty. Please add an url.', true);
|
||||
}
|
||||
if (!select.value.startsWith('http')) {
|
||||
return this.props.onNotify('The select field should start with http:// or https://.', true);
|
||||
}
|
||||
const url = select.value.trim().replace(/\/*$/, '');
|
||||
const registryServer = getRegistryServers().filter(e => e !== url);
|
||||
localStorage.setItem('registryServer', JSON.stringify([url].concat(registryServer)));
|
||||
router.home()
|
||||
this.props.onServerChange(url);
|
||||
this.props.onClose()
|
||||
setTimeout(() => router.updateUrlQueryParam(url), 100);
|
||||
},
|
||||
getRegistryServers
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:host select {
|
||||
position: relative;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
background: 0 0;
|
||||
border: none;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
height: 24px;
|
||||
border-bottom: 1px solid #2f6975;
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
</style>
|
||||
</change-registry-url>
|
||||
108
src/components/dialogs/confirm-delete-image.riot
Normal file
@@ -0,0 +1,108 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<confirm-delete-image>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClick }">
|
||||
<div slot="title">These images will be deleted</div>
|
||||
<div slot="content">
|
||||
<ul>
|
||||
<li each="{ image in displayImagesToDelete(props.toDelete, props.tags) }">{ image.name }:{ image.tag }</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ deleteImages }">
|
||||
Delete
|
||||
</material-button>
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClick }">
|
||||
Cancel
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
displayImagesToDelete(toDelete, tags) {
|
||||
const digests = new Set();
|
||||
toDelete.forEach(image => {
|
||||
if (image.digest) {
|
||||
digests.add(image.digest);
|
||||
}
|
||||
})
|
||||
return tags.filter(image => digests.has(image.digest))
|
||||
},
|
||||
deleteImages() {
|
||||
this.props.toDelete.forEach(image => this.deleteImage(image, this.props));
|
||||
},
|
||||
deleteImage(image, opts) {
|
||||
const {
|
||||
registryUrl,
|
||||
ignoreError,
|
||||
onNotify,
|
||||
onAuthentication,
|
||||
onClick
|
||||
} = opts;
|
||||
if (!image.digest) {
|
||||
onNotify(`Information for ${name}:${tag} are not yet loaded.`);
|
||||
return;
|
||||
}
|
||||
const name = image.name;
|
||||
const tag = image.tag;
|
||||
const oReq = new Http({
|
||||
onAuthentication: onAuthentication
|
||||
});
|
||||
oReq.addEventListener('loadend', function () {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
router.taglist(name);
|
||||
onNotify(`Deleting ${name}:${tag} image. Run \`registry garbage-collect config.yml\` on your registry`);
|
||||
} else if (this.status == 404) {
|
||||
ignoreError || onNotify({
|
||||
message: 'Digest not found for this image in your registry.',
|
||||
isError: true
|
||||
});
|
||||
} else {
|
||||
onNotify(this.responseText);
|
||||
}
|
||||
onClick();
|
||||
});
|
||||
oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${image.digest}`);
|
||||
oReq.setRequestHeader('Accept',
|
||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
|
||||
oReq.addEventListener('error', function () {
|
||||
onNotify({
|
||||
message: 'An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].',
|
||||
isError: true
|
||||
});
|
||||
});
|
||||
oReq.send();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:host {
|
||||
color: #000;
|
||||
list-style-type: disc;
|
||||
margin-block-start: 0.7em;
|
||||
}
|
||||
|
||||
:host material-popup .content .material-popup-content {
|
||||
overflow-y: auto;
|
||||
max-height: 250px;
|
||||
}
|
||||
</style>
|
||||
</confirm-delete-image>
|
||||
128
src/components/dialogs/dialogs-menu.riot
Normal file
@@ -0,0 +1,128 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<dialogs-menu>
|
||||
<add-registry-url opened="{ state['add-registry-url'] }" on-close="{ onClose('add-registry-url') }"
|
||||
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></add-registry-url>
|
||||
<change-registry-url opened="{ state['change-registry-url'] }" on-close="{ onClose('change-registry-url') }"
|
||||
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></change-registry-url>
|
||||
<remove-registry-url opened="{ state['remove-registry-url'] }" on-close="{ onClose('remove-registry-url') }"
|
||||
on-notify="{ props.onNotify }" on-server-change="{ props.onServerChange }"></remove-registry-url>
|
||||
<div class="container">
|
||||
<material-button onClick="{ onClick }" waves-center="true" rounded="true" waves-opacity="0.6" waves-duration="600">
|
||||
<i class="material-icons">more_vert</i>
|
||||
</material-button>
|
||||
<material-dropdown-list items="{ dropdownItems }" onSelect="{ onDropdownSelect }"
|
||||
opened="{ state.isDropdownOpened }" />
|
||||
</div>
|
||||
<div class="overlay" onclick="{ onClick }" if="{ state.isDropdownOpened }"></div>
|
||||
<script>
|
||||
import AddRegistryUrl from './add-registry-url.riot';
|
||||
import ChangeRegistryUrl from './change-registry-url.riot';
|
||||
import RemoveRegistryUrl from './remove-registry-url.riot';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddRegistryUrl,
|
||||
ChangeRegistryUrl,
|
||||
RemoveRegistryUrl
|
||||
},
|
||||
dropdownItems: [{
|
||||
title: 'Add URL',
|
||||
name: 'add-registry-url'
|
||||
}, {
|
||||
title: 'Change URL',
|
||||
name: 'change-registry-url'
|
||||
}, {
|
||||
title: 'Remove URL',
|
||||
name: 'remove-registry-url'
|
||||
}],
|
||||
onDropdownSelect(key, item) {
|
||||
this.update({
|
||||
[item.name]: true,
|
||||
isDropdownOpened: false
|
||||
});
|
||||
},
|
||||
onClose(name) {
|
||||
return () => {
|
||||
this.update({
|
||||
[name]: false,
|
||||
isDropdownOpened: false
|
||||
})
|
||||
}
|
||||
},
|
||||
onClick() {
|
||||
this.update({
|
||||
isDropdownOpened: !this.state.isDropdownOpened
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:host > .container{
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 16px;
|
||||
color: #000;
|
||||
list-style-type: disc;
|
||||
margin-block-start: 0.7em;
|
||||
}
|
||||
|
||||
:host .overlay {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
:host material-button {
|
||||
background: rgba(255, 255, 255, 0);
|
||||
float: right;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
:host material-button .content i.material-icons {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
:host material-dropdown-list {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host material-dropdown-list ul.dropdown-content {
|
||||
min-width: 156px;
|
||||
padding: 8px 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:host material-dropdown-list ul.dropdown-content li span {
|
||||
font-size: 1rem;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
:host material-popup * {
|
||||
line-height: 1em;
|
||||
}
|
||||
|
||||
:host material-popup material-button .content {
|
||||
line-height: 36px;
|
||||
}
|
||||
</style>
|
||||
</dialogs-menu>
|
||||
62
src/components/dialogs/remove-registry-url.riot
Normal file
@@ -0,0 +1,62 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<remove-registry-url>
|
||||
<material-popup opened="{ props.opened }" onClick="{ props.onClose }">
|
||||
<div slot="title">Remove your Registry Server ?</div>
|
||||
<div slot="content">
|
||||
<ul class="list">
|
||||
<li each="{ url in getRegistryServers() }">
|
||||
<span>
|
||||
<material-button onClick="{ remove }" url="{ url }" rounded="true" waves-color="rgba(158,158,158,.4)"
|
||||
waves-center="true">
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<span class="url">{ url }</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div slot="action">
|
||||
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClose }">
|
||||
Close
|
||||
</material-button>
|
||||
</div>
|
||||
</material-popup>
|
||||
<script>
|
||||
import {
|
||||
getRegistryServers
|
||||
} from '../../scripts/utils';
|
||||
export default {
|
||||
remove(event) {
|
||||
const url = event.currentTarget.attributes.url && event.currentTarget.attributes.url.value;
|
||||
const registryServer = getRegistryServers().filter(e => e !== url);
|
||||
localStorage.setItem('registryServer', JSON.stringify(registryServer));
|
||||
setTimeout(() => this.update(), 100);
|
||||
},
|
||||
getRegistryServers
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:host material-popup .popup material-button {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
:host material-popup .popup material-button .content i.material-icons {
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</remove-registry-url>
|
||||
173
src/components/docker-registry-ui.riot
Normal file
@@ -0,0 +1,173 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<docker-registry-ui>
|
||||
<header>
|
||||
<material-navbar>
|
||||
<div class="logo">Docker Registry UI</div>
|
||||
<search-bar on-search="{ onSearch }"></search-bar>
|
||||
<dialogs-menu if="{props.singleRegistry !== 'true'}" on-notify="{ notifySnackbar }"
|
||||
on-server-change="{ onServerChange }"></dialogs-menu>
|
||||
</material-navbar>
|
||||
</header>
|
||||
<main>
|
||||
<router base="#!">
|
||||
<route path="{baseRoute}">
|
||||
<catalog registry-url="{ state.registryUrl }" registry-name="{ state.name }"
|
||||
catalog-elements-limit="{ state.catalogElementsLimit }" on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }" on-authentication="{ onAuthentication }" />
|
||||
</route>
|
||||
<route path="{baseRoute}taglist/(.*)">
|
||||
<tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagListImage() }" show-content-digest="{ truthy(props.showContentDigest) }"
|
||||
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
|
||||
filter-results="{ state.filter }" on-authentication="{ onAuthentication }"></tag-list>
|
||||
</route>
|
||||
<route path="{baseRoute}taghistory/(.*)">
|
||||
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }"
|
||||
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
|
||||
on-authentication="{ onAuthentication }"></tag-history>
|
||||
</route>
|
||||
</router>
|
||||
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
|
||||
on-close="{ onAuthenticationClose }" on-authenticated="{ state.onAuthenticated }"
|
||||
opened="{ state.authenticationDialogOpened }"></registry-authentication>
|
||||
<material-snackbar message="{ state.snackbarMessage }" is-error="{ state.snackbarIsError }"></material-snackbar>
|
||||
</main>
|
||||
<footer>
|
||||
<material-footer>
|
||||
<a slot="logo" href="https://joxit.github.io/docker-registry-ui/">Docker Registry UI { version }</a>
|
||||
<ul slot="link-list">
|
||||
<li>
|
||||
<a href="https://github.com/Joxit/docker-registry-ui">Contribute on GitHub</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/Joxit/docker-registry-ui/blob/main/LICENSE">Privacy & Terms</a>
|
||||
</li>
|
||||
</ul>
|
||||
</material-footer>
|
||||
</footer>
|
||||
<script>
|
||||
import {
|
||||
version
|
||||
} from '../../package.json';
|
||||
import {
|
||||
Router,
|
||||
Route,
|
||||
} from '@riotjs/route'
|
||||
import Catalog from './catalog/catalog.riot';
|
||||
import TagList from './tag-list/tag-list.riot';
|
||||
import TagHistory from './tag-history/tag-history.riot';
|
||||
import DialogsMenu from './dialogs/dialogs-menu.riot';
|
||||
import SearchBar from './search-bar.riot'
|
||||
import {
|
||||
stripHttps,
|
||||
getRegistryServers,
|
||||
truthy
|
||||
} from '../scripts/utils';
|
||||
import router from '../scripts/router';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Catalog,
|
||||
TagList,
|
||||
TagHistory,
|
||||
DialogsMenu,
|
||||
SearchBar,
|
||||
Router,
|
||||
Route
|
||||
},
|
||||
onUpdated(props, state) {
|
||||
state.snackbarIsError = false;
|
||||
state.snackbarMessage = undefined;
|
||||
},
|
||||
onBeforeMount(props) {
|
||||
console.log(props)
|
||||
// props.singleRegistry === 'true' means old static version
|
||||
this.state.registryUrl = props.registryUrl ||
|
||||
(props.singleRegistry === 'true' ? undefined : (router.getUrlQueryParam() || getRegistryServers(0))) ||
|
||||
(window.location.origin + window.location.pathname.replace(/\/+$/, ''));
|
||||
this.state.name = props.name || stripHttps(props.registryUrl);
|
||||
this.state.catalogElementsLimit = props.catalogElementsLimit || 100000;
|
||||
this.state.pullUrl = this.pullUrl(this.state.registryUrl, props.pullUrl);
|
||||
},
|
||||
onServerChange(registryUrl) {
|
||||
this.update({
|
||||
registryUrl,
|
||||
name: stripHttps(registryUrl),
|
||||
pullUrl: this.pullUrl(registryUrl),
|
||||
snackbarMessage: 'Registry server changed to `' + registryUrl + '`.'
|
||||
})
|
||||
},
|
||||
onAuthentication(opts, onAuthenticated) {
|
||||
if (opts && opts.realm && opts.service && opts.scope) {
|
||||
const {
|
||||
realm,
|
||||
service,
|
||||
scope,
|
||||
} = opts;
|
||||
const req = new XMLHttpRequest()
|
||||
req.addEventListener('loadend', () => {
|
||||
try {
|
||||
const bearer = JSON.parse(req.responseText);
|
||||
onAuthenticated(bearer)
|
||||
} catch (e) {
|
||||
this.notifySnackbar(`Failed to log in: ${e.message}`, true)
|
||||
}
|
||||
})
|
||||
req.open('GET', `${realm}?service=${service}&scope=${scope}`)
|
||||
req.send()
|
||||
} else {
|
||||
onAuthenticated()
|
||||
}
|
||||
},
|
||||
onAuthenticationClose() {
|
||||
this.update({
|
||||
authenticationDialogOpened: false
|
||||
})
|
||||
},
|
||||
pullUrl(registryUrl, pullUrl) {
|
||||
const url = pullUrl ||
|
||||
(registryUrl && registryUrl.length > 0 && registryUrl) ||
|
||||
window.location.host;
|
||||
return stripHttps(url);
|
||||
},
|
||||
notifySnackbar(message, isError) {
|
||||
if (typeof message === 'string') {
|
||||
this.update({
|
||||
snackbarMessage: message,
|
||||
snackbarIsError: isError || false
|
||||
});
|
||||
} else if (message && message.message) {
|
||||
this.update({
|
||||
snackbarMessage: message.message,
|
||||
snackbarIsError: message.isError
|
||||
});
|
||||
}
|
||||
},
|
||||
onSearch(value) {
|
||||
this.update({
|
||||
filter: value
|
||||
})
|
||||
},
|
||||
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
|
||||
router,
|
||||
version,
|
||||
truthy
|
||||
}
|
||||
</script>
|
||||
</docker-registry-ui>
|
||||
48
src/components/search-bar.riot
Normal file
@@ -0,0 +1,48 @@
|
||||
<search-bar>
|
||||
<material-input placeholder="Search in page"></material-input>
|
||||
<script>
|
||||
import {
|
||||
router
|
||||
} from '@riotjs/route';
|
||||
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
const input = this.$('input');
|
||||
let value = '';
|
||||
const notify = () => {
|
||||
if (value !== input.value) {
|
||||
props.onSearch(input.value.toLowerCase())
|
||||
}
|
||||
value = input.value;
|
||||
}
|
||||
input.addEventListener('keyup', notify);
|
||||
router.on.value(() => {
|
||||
input.value = '';
|
||||
notify();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function matchSearch(search, value) {
|
||||
return !search || (value && value.toLowerCase().indexOf(search) >= 0);
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
:host material-input {
|
||||
position: absolute;
|
||||
top: 0em;
|
||||
right: 64px;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
:host material-input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:host material-input input {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</search-bar>
|
||||
46
src/components/tag-history/tag-history-element.riot
Normal file
@@ -0,0 +1,46 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<tag-history-element class="{ state.key }">
|
||||
<div class="headline"><i class="material-icons">{ state.icon }</i>
|
||||
<p>{ state.name }</p>
|
||||
</div>
|
||||
<div class="value" if="{ state.value }"> { state.value }</div>
|
||||
<div class="value" each="{ value in state.values }" if="{ state.values }"> { value }</div>
|
||||
<script>
|
||||
import {
|
||||
getHistoryIcon
|
||||
} from '../../scripts/utils';
|
||||
export default {
|
||||
onBeforeStar(props, state) {
|
||||
state.key = props.entry.key;
|
||||
state.icon = getHistoryIcon(props.entry.key);
|
||||
state.name = props.entry.key.replace('_', ' ');
|
||||
if (props.value instanceof Array) {
|
||||
state.values = props.entry.value;
|
||||
} else {
|
||||
state.value = props.entry.value;
|
||||
}
|
||||
},
|
||||
onBeforeMount(props, state) {
|
||||
this.onBeforeStar(props, state);
|
||||
},
|
||||
onBeforeUpdate(props, state) {
|
||||
this.onBeforeStar(props, state);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</tag-history-element>
|
||||
192
src/components/tag-history/tag-history.riot
Normal file
@@ -0,0 +1,192 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<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" onClick="{ toTaglist }">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
History of { props.image }:{ props.tag } <i class="material-icons">history</i>
|
||||
</h2>
|
||||
</div>
|
||||
</material-card>
|
||||
<div if="{ !state.loadend }" class="spinner-wrapper">
|
||||
<material-spinner />
|
||||
</div>
|
||||
|
||||
<material-tabs if="{ state.archs && state.loadend }" useLine="{ true }" tabs="{ state.archs }"
|
||||
onTabChanged="{ onTabChanged }" />
|
||||
|
||||
<material-card each="{ element in state.elements }" class="tag-history-element">
|
||||
<tag-history-element each="{ entry in element }" if="{ entry.value && entry.value.length > 0}" entry="{ entry }" />
|
||||
</material-card>
|
||||
<script>
|
||||
import {
|
||||
DockerImage
|
||||
} from '../../scripts/docker-image';
|
||||
import {
|
||||
bytesToSize
|
||||
} from '../../scripts/utils';
|
||||
import router from '../../scripts/router';
|
||||
import TagHistoryElement from './tag-history-element.riot'
|
||||
export default {
|
||||
components: {
|
||||
TagHistoryElement
|
||||
},
|
||||
onBeforeMount(props, state) {
|
||||
state.elements = [];
|
||||
state.image = new DockerImage(props.image, props.tag, {
|
||||
list: true,
|
||||
registryUrl: props.registryUrl,
|
||||
onNotify: props.onNotify,
|
||||
onAuthentication: props.onAuthentication
|
||||
});
|
||||
state.image.fillInfo()
|
||||
},
|
||||
onMounted(props, state) {
|
||||
state.image.on('blobs', this.processBlobs);
|
||||
state.image.on('list', this.multiArchList);
|
||||
},
|
||||
onTabChanged(arch, idx) {
|
||||
const state = this.state;
|
||||
state.elements = []
|
||||
state.image.variants[idx] = state.image.variants[idx] ||
|
||||
new DockerImage(this.props.image, arch.digest, false, this.props.registryUrl, this.props.onNotify);
|
||||
if (state.image.variants[idx].blobs) {
|
||||
return this.processBlobs(state.image.variants[idx].blobs);
|
||||
}
|
||||
state.image.variants[idx].fillInfo();
|
||||
state.image.variants[idx].on('blobs', this.processBlobs);
|
||||
},
|
||||
processBlobs(blobs) {
|
||||
const state = this.state;
|
||||
|
||||
function exec(elt) {
|
||||
const guiElements = [];
|
||||
for (var attribute in elt) {
|
||||
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
|
||||
const value = elt[attribute];
|
||||
const guiElement = {
|
||||
"key": attribute,
|
||||
"value": modifySpecificAttributeTypes(attribute, value)
|
||||
};
|
||||
guiElements.push(guiElement);
|
||||
}
|
||||
}
|
||||
return guiElements.sort(eltSort);
|
||||
}
|
||||
const elements = new Array(blobs.history.length + 1);
|
||||
elements[0] = exec(getConfig(blobs));
|
||||
blobs.history.forEach(function (elt, i) {
|
||||
elements[blobs.history.length - i] = exec(elt)
|
||||
});
|
||||
this.update({
|
||||
elements,
|
||||
loadend: true
|
||||
});
|
||||
},
|
||||
multiArchList(manifests) {
|
||||
manifests = manifests.manifests || manifests;
|
||||
const archs = manifests.map(function (manifest) {
|
||||
return {
|
||||
title: manifest.platform.os + '/' + manifest.platform.architecture + (manifest.platform.variant ?
|
||||
manifest.platform.variant : ''),
|
||||
digest: manifest.digest
|
||||
}
|
||||
});
|
||||
this.update({
|
||||
archs
|
||||
});
|
||||
},
|
||||
toTaglist() {
|
||||
router.taglist(this.props.image);
|
||||
}
|
||||
}
|
||||
const eltIdx = function (e) {
|
||||
switch (e) {
|
||||
case 'created':
|
||||
return 1;
|
||||
case 'created_by':
|
||||
return 2;
|
||||
case 'size':
|
||||
return 3;
|
||||
case 'os':
|
||||
return 4;
|
||||
case 'architecture':
|
||||
return 5;
|
||||
case 'id':
|
||||
return 6;
|
||||
case 'linux':
|
||||
return 7;
|
||||
case 'docker_version':
|
||||
return 8;
|
||||
default:
|
||||
return 10;
|
||||
}
|
||||
};
|
||||
|
||||
const eltSort = function (e1, e2) {
|
||||
return eltIdx(e1.key) - eltIdx(e2.key);
|
||||
};
|
||||
|
||||
const modifySpecificAttributeTypes = function (attribute, value) {
|
||||
switch (attribute) {
|
||||
case 'created':
|
||||
return new Date(value).toLocaleString();
|
||||
case 'created_by':
|
||||
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
|
||||
return (cmd && cmd[1]) || 'RUN'
|
||||
case 'size':
|
||||
return bytesToSize(value);
|
||||
case 'Entrypoint':
|
||||
case 'Cmd':
|
||||
return (value || []).join(' ');
|
||||
case 'Labels':
|
||||
return Object.keys(value || {}).map(function (elt) {
|
||||
return value[elt] ? elt + '=' + value[elt] : '';
|
||||
});
|
||||
case 'Volumes':
|
||||
case 'ExposedPorts':
|
||||
return Object.keys(value);
|
||||
}
|
||||
return value || '';
|
||||
};
|
||||
|
||||
const getConfig = function (blobs) {
|
||||
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels',
|
||||
'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts'
|
||||
]
|
||||
.reduce(function (acc, e) {
|
||||
const value = blobs[e] || blobs.config[e];
|
||||
if (value && e === 'architecture' && blobs.variant) {
|
||||
acc[e] = value + blobs.variant;
|
||||
} else if (value) {
|
||||
acc[e] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!res.author && (res.Labels && res.Labels.maintainer)) {
|
||||
res.author = blobs.config.Labels.maintainer;
|
||||
delete res.Labels.maintainer;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
</script>
|
||||
</tag-history>
|
||||
67
src/components/tag-list/copy-to-clipboard.riot
Normal file
@@ -0,0 +1,67 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<copy-to-clipboard>
|
||||
<div class="copy-to-clipboard">
|
||||
<input style="display: none; width: 1px; height: 1px;" value="{ getDockerCmd(props) }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ copy }"
|
||||
title="Copy pull command.">
|
||||
<i class="material-icons">content_copy</i>
|
||||
</material-button>
|
||||
</div>
|
||||
<script>
|
||||
import {
|
||||
ERROR_CAN_NOT_READ_CONTENT_DIGEST
|
||||
} from '../../scripts/utils';
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
this.load(props, state);
|
||||
},
|
||||
onUpdated(props, state) {
|
||||
this.load(props, state);
|
||||
},
|
||||
getDockerCmd(props) {
|
||||
if (props.target === 'tag') {
|
||||
return `docker pull ${props.pullUrl}/${props.image.name}:${props.image.tag}`;
|
||||
} else {
|
||||
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.digest}`
|
||||
}
|
||||
},
|
||||
load(props, state) {
|
||||
if (props.target !== 'tag' && !props.image.digest) {
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.update()
|
||||
});
|
||||
props.image.trigger('get-content-digest');
|
||||
}
|
||||
},
|
||||
copy() {
|
||||
const copyText = this.$('input');
|
||||
if (!copyText.value) {
|
||||
this.props.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
|
||||
return;
|
||||
}
|
||||
|
||||
copyText.style.display = 'block';
|
||||
copyText.select();
|
||||
document.execCommand('copy');
|
||||
copyText.style.display = 'none';
|
||||
|
||||
this.props.onNotify('`' + copyText.value + '` has been copied to clipboard.')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</copy-to-clipboard>
|
||||
60
src/components/tag-list/image-content-digest.riot
Normal file
@@ -0,0 +1,60 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<image-content-digest>
|
||||
<div title="{ getTitle(props.image, state.chars) }">{ getDigest(props.image, state.chars) }</div>
|
||||
<script>
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
this.load(props, state);
|
||||
},
|
||||
onUpdated(props, state) {
|
||||
this.load(props, state);
|
||||
},
|
||||
load(props, state) {
|
||||
if (props.image.digest) {
|
||||
return;
|
||||
}
|
||||
state.chars = -1;
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.digest = digest;
|
||||
props.image.on('content-digest-chars', this.onResize);
|
||||
props.image.trigger('get-content-digest-chars');
|
||||
});
|
||||
props.image.trigger('get-content-digest');
|
||||
},
|
||||
onResize(chars) {
|
||||
if (chars !== this.state.chars) {
|
||||
this.update({
|
||||
chars
|
||||
});
|
||||
}
|
||||
},
|
||||
getTitle(image, chars) {
|
||||
return chars >= 70 ? '' : (image.digest || '');
|
||||
},
|
||||
getDigest(image, chars) {
|
||||
if (chars >= 70) {
|
||||
return image.digest || '';
|
||||
} else if (chars <= 0) {
|
||||
return '';
|
||||
} else {
|
||||
return image.digest && image.digest.slice(0, chars) + '...';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</image-content-digest>
|
||||
41
src/components/tag-list/image-date.riot
Normal file
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<image-date>
|
||||
<div title="Creation date { getLocalDate(props.image) }">{ getDate(props.image) } ago</div>
|
||||
<script>
|
||||
import {
|
||||
dateFormat,
|
||||
} from '../../scripts/utils';
|
||||
export default {
|
||||
onMounted(props) {
|
||||
props.image.one('creation-date', (date) => {
|
||||
this.update({
|
||||
date: date,
|
||||
localDate: date.toLocaleString()
|
||||
});
|
||||
});
|
||||
props.image.trigger('get-date');
|
||||
},
|
||||
getDate(image) {
|
||||
return dateFormat(image.creationDate)
|
||||
},
|
||||
getLocalDate(image) {
|
||||
return (image.creationDate && image.creationDate.toLocaleString()) || 'unknown'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</image-date>
|
||||
47
src/components/tag-list/image-size.riot
Normal file
@@ -0,0 +1,47 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<image-size>
|
||||
<div title="Compressed size of your image.">{ getImageSize(props.image) }</div>
|
||||
<script>
|
||||
import {
|
||||
bytesToSize,
|
||||
} from '../../scripts/utils';
|
||||
export default {
|
||||
onMounted(props, state) {
|
||||
this.load(props, state);
|
||||
},
|
||||
onUpdated(props, state) {
|
||||
this.load(props, state);
|
||||
},
|
||||
load(props, state) {
|
||||
if (typeof props.image.size === 'number') {
|
||||
return;
|
||||
}
|
||||
props.image.on('size', (size) => {
|
||||
this.update({
|
||||
size
|
||||
});
|
||||
});
|
||||
props.image.trigger('get-size');
|
||||
|
||||
},
|
||||
getImageSize(image) {
|
||||
return bytesToSize(image.size)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</image-size>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2019 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2021 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
|
||||
@@ -15,13 +15,17 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
<image-tag>
|
||||
<div title="{ this.sha256 }">{ opts.image.tag }</div>
|
||||
<script type="text/javascript">
|
||||
const self = this;
|
||||
opts.image.on('sha256', function(sha256) {
|
||||
self.sha256 = sha256.substring(0, 19);
|
||||
self.update();
|
||||
});
|
||||
opts.image.trigger('get-sha256');
|
||||
<div title="{ state.sha256 }">{ props.image.tag }</div>
|
||||
<script>
|
||||
export default {
|
||||
onMounted(props) {
|
||||
props.image.on('sha256', (sha256) => {
|
||||
this.update({
|
||||
sha256: sha256.substring(0, 19)
|
||||
});
|
||||
});
|
||||
props.image.trigger('get-sha256');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</image-tag>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2019 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2021 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
|
||||
@@ -14,10 +14,17 @@ 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/>.
|
||||
-->
|
||||
<tag-history-element class="{entry.key}">
|
||||
<div class="headline"><i class="material-icons">{ registryUI.getHistoryIcon(entry.key) }</i>
|
||||
<p>{ entry.key.replace('_', ' ') }</p>
|
||||
<pagination>
|
||||
<div class="conatianer">
|
||||
<div class="pagination-centered">
|
||||
<material-button waves-color="rgba(158,158,158,.4)" each="{p in props.pages}"
|
||||
class="{ p.current ? 'current' : ''} { p['space-left'] ? 'space-left' : '' } { p['space-right'] ? 'space-right' : ''}"
|
||||
onClick="{() => props.onPageUpdate(p.page)}">
|
||||
<i if="{ p.icon }" class="material-icons">{ p.icon }</i>
|
||||
<div if="{ !p.icon }">{ p.page }</div>
|
||||
</material-button>
|
||||
</div>
|
||||
</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>
|
||||
</tag-history-element>
|
||||
<script>
|
||||
</script>
|
||||
</pagination>
|
||||
59
src/components/tag-list/remove-image.riot
Normal file
@@ -0,0 +1,59 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<remove-image>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image."
|
||||
if="{ !props.multiDelete }" disabled="{ !state.digest }" onClick="{ deleteImage }">
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<material-checkbox if="{ props.multiDelete }" title="Select this tag to delete it." disabled="{ !state.digest }"
|
||||
onChange="{ handleCheckboxChange }" checked="{ state.checked }">
|
||||
</material-checkbox>
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import router from '../../scripts/router'
|
||||
import {
|
||||
ACTION_CHECK_TO_DELETE,
|
||||
ACTION_UNCHECK_TO_DELETE,
|
||||
ACTION_DELETE_IMAGE
|
||||
} from './tag-table.riot';
|
||||
export default {
|
||||
onBeforeMount(props, state) {
|
||||
state.checked = props.checked;
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.update({
|
||||
digest
|
||||
});
|
||||
});
|
||||
},
|
||||
onMounted(props, state) {
|
||||
props.image.trigger('get-content-digest');
|
||||
},
|
||||
onBeforeUpdate(props, state) {
|
||||
state.checked = props.checked;
|
||||
},
|
||||
deleteImage() {
|
||||
this.props.handleCheckboxChange(ACTION_DELETE_IMAGE, this.props.image);
|
||||
},
|
||||
handleCheckboxChange(checked) {
|
||||
const action = checked ? ACTION_CHECK_TO_DELETE : ACTION_UNCHECK_TO_DELETE;
|
||||
this.props.handleCheckboxChange(action, this.props.image);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</remove-image>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2019 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2021 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
|
||||
@@ -15,16 +15,16 @@ 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-button>
|
||||
<material-button ref="button" title="This will show the history of given tag" waves-center="true" rounded="true" waves-color="#ddd">
|
||||
<material-button title="This will show the history of given tag" waves-center="true" rounded="true" waves-color="#ddd"
|
||||
onClick="{ routeToHistory }">
|
||||
<i class="material-icons">history</i>
|
||||
</material-button>
|
||||
<script type="text/javascript">
|
||||
this.on('mount', function() {
|
||||
const self = this;
|
||||
this.refs.button.root.onclick = function() {
|
||||
registryUI.taghistory.go(self.opts.image.name, self.opts.image.tag);
|
||||
};
|
||||
});
|
||||
this.update()
|
||||
<script>
|
||||
import router from '../../scripts/router';
|
||||
export default {
|
||||
routeToHistory() {
|
||||
router.history(this.props.image.name, this.props.image.tag)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</tag-history-button>
|
||||
168
src/components/tag-list/tag-list.riot
Normal file
@@ -0,0 +1,168 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<tag-list>
|
||||
<material-card class="header">
|
||||
<div class="material-card-title-action ">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ router.home }">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
Tags of { props.image }
|
||||
<div class="source-hint">
|
||||
Sourced from { state.registryName + '/' + props.image }
|
||||
</div>
|
||||
<div class="item-count">{ state.tags.length } tags</div>
|
||||
</h2>
|
||||
</div>
|
||||
</material-card>
|
||||
|
||||
<div if="{ !state.loadend }" class="spinner-wrapper">
|
||||
<material-spinner></material-spinner>
|
||||
</div>
|
||||
|
||||
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
||||
|
||||
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
|
||||
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
|
||||
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" filter-results="{ props.filterResults }"
|
||||
on-authentication="{ props.onAuthentication }">
|
||||
</tag-table>
|
||||
|
||||
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
||||
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import {
|
||||
DockerImage,
|
||||
compare
|
||||
} from '../../scripts/docker-image';
|
||||
import {
|
||||
getNumPages,
|
||||
getPageLabels
|
||||
} from '../../scripts/utils'
|
||||
import Pagination from './pagination.riot'
|
||||
import TagTable from './tag-table.riot'
|
||||
import router from '../../scripts/router'
|
||||
export default {
|
||||
components: {
|
||||
Pagination,
|
||||
TagTable,
|
||||
},
|
||||
onBeforeMount(props) {
|
||||
this.state = {
|
||||
registryName: props.registryName,
|
||||
tags: [],
|
||||
loadend: false,
|
||||
asc: true,
|
||||
page: router.getPageQueryParam() || 1
|
||||
}
|
||||
},
|
||||
onMounted(props, state) {
|
||||
this.display(props, state)
|
||||
window.addEventListener('resize', this.onResize);
|
||||
// this may be run before the final document size is available, so schedule
|
||||
// a correction once everything is set up.
|
||||
window.requestAnimationFrame(this.onResize);
|
||||
},
|
||||
display(props, state) {
|
||||
state.tags = [];
|
||||
const self = this;
|
||||
const oReq = new Http({
|
||||
onAuthentication: props.onAuthentication
|
||||
});
|
||||
oReq.addEventListener('load', function () {
|
||||
if (this.status == 200) {
|
||||
const tags = (JSON.parse(this.responseText).tags || [])
|
||||
.map(tag => new DockerImage(props.image, tag, {
|
||||
registryUrl: props.registryUrl,
|
||||
onNotify: props.onNotify,
|
||||
onAuthentication: props.onAuthentication
|
||||
}))
|
||||
.sort(compare);
|
||||
window.requestAnimationFrame(self.onResize);
|
||||
self.update({
|
||||
page: Math.min(state.page, getNumPages(tags)),
|
||||
tags
|
||||
})
|
||||
} else if (this.status == 404) {
|
||||
self.props.onNotify('Server not found', true);
|
||||
} else {
|
||||
self.props.onNotify(this.responseText, true);
|
||||
}
|
||||
});
|
||||
oReq.addEventListener('error', function () {
|
||||
self.props.onNotify(this.getErrorMessage(), true);
|
||||
state.tags = [];
|
||||
});
|
||||
oReq.addEventListener('loadend', function () {
|
||||
self.update({
|
||||
loadend: true
|
||||
});
|
||||
});
|
||||
oReq.open('GET', props.registryUrl + '/v2/' + props.image + '/tags/list');
|
||||
oReq.send();
|
||||
state.asc = true;
|
||||
},
|
||||
|
||||
onPageUpdate(page) {
|
||||
this.update({
|
||||
page: page
|
||||
});
|
||||
router.updatePageQueryParam(page);
|
||||
},
|
||||
|
||||
onResize() {
|
||||
// window.innerWidth is a blocking access, cache its result.
|
||||
const innerWidth = window.innerWidth;
|
||||
let chars = 0;
|
||||
const max = this.state.tags.reduce(function (acc, e) {
|
||||
return e.tag.length > acc ? e.tag.length : acc;
|
||||
}, 0);
|
||||
if (innerWidth >= 1440) {
|
||||
chars = 71;
|
||||
} else if (innerWidth < 1024) {
|
||||
chars = 0;
|
||||
} else {
|
||||
// SHA256:12345678 + scaled between 1024 and 1440px
|
||||
chars = 15 + 56 * ((innerWidth - 1024) / 416);
|
||||
}
|
||||
if (max > 20) chars -= (max - 20);
|
||||
chars = Math.floor(chars)
|
||||
this.state.tags.map(function (image) {
|
||||
image.trigger('content-digest-chars', chars);
|
||||
});
|
||||
},
|
||||
|
||||
onReverseOrder() {
|
||||
if (this.state.asc) {
|
||||
this.state.tags.reverse();
|
||||
this.state.asc = false;
|
||||
} else {
|
||||
this.state.tags.sort(compare);
|
||||
this.state.asc = true;
|
||||
}
|
||||
this.update();
|
||||
},
|
||||
getPageLabels,
|
||||
getNumPages,
|
||||
router
|
||||
}
|
||||
</script>
|
||||
</tag-list>
|
||||
216
src/components/tag-list/tag-table.riot
Normal file
@@ -0,0 +1,216 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2021 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/>.
|
||||
-->
|
||||
<tag-table>
|
||||
<confirm-delete-image opened="{ state.confirmDeleteImage }" on-click="{ onConfirmDeleteImageClick }"
|
||||
registry-url="{ props.registryUrl }" on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }"
|
||||
tags="{ props.tags }" to-delete="{ state.toDelete }"></confirm-delete-image>
|
||||
<material-card class="taglist">
|
||||
<table style="border: none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="creation-date { (state.desc && state.orderType === 'date') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
|
||||
onclick="{() => onPageReorder('date') }">
|
||||
Creation date
|
||||
</th>
|
||||
<th
|
||||
class="image-size { (state.desc && state.orderType === 'size') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
|
||||
onclick="{() => onPageReorder('size') }">
|
||||
Size
|
||||
</th>
|
||||
<th id="image-content-digest-header" if="{ props.showContentDigest }">Content Digest</th>
|
||||
|
||||
<th id="image-tag-header"
|
||||
class="{ props.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
|
||||
onclick="{ onReverseOrder }">Tag
|
||||
</th>
|
||||
<th class="show-tag-history">History</th>
|
||||
<th class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
|
||||
if="{ props.isImageRemoveActivated }">
|
||||
<material-checkbox class="indeterminate" checked="{ state.multiDelete }"
|
||||
if="{ state.toDelete.size === 0 || state.singleDeleteAction }"
|
||||
title="Toggle multi-delete. Alt+Click to select all tags." onChange="{ onRemoveImageHeaderChange }">
|
||||
</material-checkbox>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd"
|
||||
title="This will delete selected images." onClick="{ deleteImages }"
|
||||
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }">
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr each="{ image in getPage(props.tags, props.page) }" if="{ matchSearch(props.filterResults, image.tag) }">
|
||||
<td class="creation-date">
|
||||
<image-date image="{ image }" />
|
||||
</td>
|
||||
<td class="image-size">
|
||||
<image-size image="{ image }" />
|
||||
</td>
|
||||
<td if="{ props.showContentDigest }">
|
||||
<image-content-digest image="{ image }" />
|
||||
<copy-to-clipboard target="digest" image="{ image }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" />
|
||||
</td>
|
||||
<td>
|
||||
<image-tag image="{ image }" />
|
||||
<copy-to-clipboard target="tag" image="{ image }" pull-url="{ props.pullUrl }"
|
||||
on-notify="{ props.onNotify }" />
|
||||
</td>
|
||||
<td class="show-tag-history">
|
||||
<tag-history-button image="{ image }" />
|
||||
</td>
|
||||
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
|
||||
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
|
||||
handleCheckboxChange="{ onRemoveImageChange }" checked="{ state.toDelete.has(image) }"
|
||||
on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</material-card>
|
||||
<script>
|
||||
import {
|
||||
getPage,
|
||||
} from '../../scripts/utils';
|
||||
import ImageDate from './image-date.riot';
|
||||
import ImageSize from './image-size.riot';
|
||||
import ImageTag from './image-tag.riot';
|
||||
import ImageContentDigest from './image-content-digest.riot';
|
||||
import CopyToClipboard from './copy-to-clipboard.riot';
|
||||
import TagHistoryButton from './tag-history-button.riot';
|
||||
import RemoveImage from './remove-image.riot';
|
||||
import {
|
||||
matchSearch
|
||||
} from '../search-bar.riot';
|
||||
import ConfirmDeleteImage from '../dialogs/confirm-delete-image.riot';
|
||||
const ACTION_CHECK_TO_DELETE = 'CHECK';
|
||||
const ACTION_UNCHECK_TO_DELETE = 'UNCHECK';
|
||||
const ACTION_DELETE_IMAGE = 'DELETE';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ImageDate,
|
||||
ImageSize,
|
||||
ImageTag,
|
||||
ImageContentDigest,
|
||||
CopyToClipboard,
|
||||
RemoveImage,
|
||||
TagHistoryButton,
|
||||
ConfirmDeleteImage,
|
||||
},
|
||||
onBeforeMount(props) {
|
||||
this.state = {
|
||||
toDelete: new Set(),
|
||||
multiDelete: false,
|
||||
page: props.page,
|
||||
}
|
||||
},
|
||||
onBeforeUpdate(props, state) {
|
||||
if (state.page !== props.page) {
|
||||
state.toDelete.clear();
|
||||
}
|
||||
state.page = props.page
|
||||
},
|
||||
deleteImages() {
|
||||
this.update({
|
||||
confirmDeleteImage: true
|
||||
})
|
||||
},
|
||||
onConfirmDeleteImageClick() {
|
||||
if (this.state.singleDeleteAction) {
|
||||
this.state.toDelete.clear();
|
||||
}
|
||||
this.update({
|
||||
singleDeleteAction: false,
|
||||
confirmDeleteImage: false
|
||||
})
|
||||
},
|
||||
onRemoveImageHeaderChange(checked, event) {
|
||||
if (event.altKey === true) {
|
||||
const tags = getPage(this.props.tags, this.props.page);
|
||||
tags.forEach(tag => this.state.toDelete.add(tag));
|
||||
this.update({
|
||||
multiDelete: true,
|
||||
toDelete: this.state.toDelete
|
||||
})
|
||||
} else {
|
||||
this.update({
|
||||
multiDelete: checked
|
||||
})
|
||||
}
|
||||
},
|
||||
onRemoveImageChange(action, image) {
|
||||
let confirmDeleteImage = false;
|
||||
let singleDeleteAction = false;
|
||||
switch (action) {
|
||||
case ACTION_CHECK_TO_DELETE: {
|
||||
this.state.toDelete.add(image);
|
||||
break;
|
||||
}
|
||||
case ACTION_UNCHECK_TO_DELETE: {
|
||||
this.state.toDelete.delete(image);
|
||||
break;
|
||||
}
|
||||
case ACTION_DELETE_IMAGE: {
|
||||
this.state.toDelete.clear();
|
||||
this.state.toDelete.add(image);
|
||||
confirmDeleteImage = true;
|
||||
singleDeleteAction = true;
|
||||
}
|
||||
}
|
||||
this.update({
|
||||
toDelete: this.state.toDelete,
|
||||
confirmDeleteImage,
|
||||
singleDeleteAction
|
||||
})
|
||||
},
|
||||
onReverseOrder() {
|
||||
this.state.orderType = null;
|
||||
this.state.desc = false;
|
||||
this.props.onReverseOrder();
|
||||
},
|
||||
onPageReorder(type) {
|
||||
this.update({
|
||||
orderType: type,
|
||||
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc
|
||||
})
|
||||
},
|
||||
getPage(tags, page) {
|
||||
const sortedTags = getPage(tags, page);
|
||||
if (this.state.orderType === 'date') {
|
||||
sortedTags.sort((e1, e2) =>
|
||||
!this.state.desc ?
|
||||
e2.creationDate.getTime() - e1.creationDate.getTime() :
|
||||
e1.creationDate.getTime() - e2.creationDate.getTime());
|
||||
} else if (this.state.orderType === 'size') {
|
||||
sortedTags.sort((e1, e2) =>
|
||||
!this.state.desc ?
|
||||
e2.size - e1.size :
|
||||
e1.size - e2.size);
|
||||
}
|
||||
return sortedTags;
|
||||
},
|
||||
matchSearch
|
||||
}
|
||||
export {
|
||||
ACTION_CHECK_TO_DELETE,
|
||||
ACTION_UNCHECK_TO_DELETE,
|
||||
ACTION_DELETE_IMAGE
|
||||
}
|
||||
</script>
|
||||
</tag-table>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--
|
||||
Copyright (C) 2016-2019 Jones Magloire @Joxit
|
||||
Copyright (C) 2016-2021 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
|
||||
@@ -19,10 +19,8 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- build:css vendor.css -->
|
||||
<!-- build:css docker-registry-ui.css -->
|
||||
<link href="../node_modules/riot-mui/build/styles/riot-mui.min.css" rel="stylesheet" type="text/css">
|
||||
<!-- endbuild -->
|
||||
<!-- build:css style.css -->
|
||||
<link href="style.css" rel="stylesheet" type="text/css">
|
||||
<link href="material-icons.css" rel="stylesheet" type="text/css">
|
||||
<link href="roboto.css" rel="stylesheet" type="text/css">
|
||||
@@ -36,13 +34,21 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app></app>
|
||||
<!-- build:js scripts/vendor.js -->
|
||||
<!-- build:keep production -->
|
||||
<docker-registry-ui registry-url="${REGISTRY_URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}"
|
||||
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}"
|
||||
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}">
|
||||
</docker-registry-ui>
|
||||
<!-- endbuild -->
|
||||
<!-- build:keep developement -->
|
||||
<docker-registry-ui registry-url="" name="Developement Registry" pull-url="" show-content-digest="true"
|
||||
is-image-remove-activated="true" catalog-elements-limit="1000" single-registry="false">
|
||||
</docker-registry-ui>
|
||||
<!-- endbuild -->
|
||||
<!-- build:js docker-registry-ui.js -->
|
||||
<script src="../node_modules/riot/riot+compiler.min.js"></script>
|
||||
<script src="../node_modules/riot-route/dist/route.js"></script>
|
||||
<script src="../node_modules/riot-mui/build/js/riot-mui.js"></script>
|
||||
<!-- endbuild -->
|
||||
<!-- build:js scripts/docker-registry-ui.js -->
|
||||
<script src="tags/catalog.riot" type="riot/tag"></script>
|
||||
<script src="tags/catalog-element.riot" type="riot/tag"></script>
|
||||
<script src="tags/tag-history-button.riot" type="riot/tag"></script>
|
||||
@@ -67,4 +73,4 @@
|
||||
<!-- endbuild -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
39
src/index.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { component, register } from 'riot';
|
||||
|
||||
import {
|
||||
MaterialCard,
|
||||
MaterialSpinner,
|
||||
MaterialNavbar,
|
||||
MaterialFooter,
|
||||
MaterialButton,
|
||||
MaterialWaves,
|
||||
MaterialCheckbox,
|
||||
MaterialTabs,
|
||||
MaterialSnackbar,
|
||||
MaterialDropdownList,
|
||||
MaterialPopup,
|
||||
MaterialInput,
|
||||
} from 'riot-mui';
|
||||
|
||||
import DockerRegistryUI from './components/docker-registry-ui.riot';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
register('material-card', MaterialCard);
|
||||
register('material-footer', MaterialFooter);
|
||||
register('material-navbar', MaterialNavbar);
|
||||
register('material-spinner', MaterialSpinner);
|
||||
register('material-button', MaterialButton);
|
||||
register('material-waves', MaterialWaves);
|
||||
register('material-checkbox', MaterialCheckbox);
|
||||
register('material-snackbar', MaterialSnackbar);
|
||||
register('material-tabs', MaterialTabs);
|
||||
register('material-dropdown-list', MaterialDropdownList);
|
||||
register('material-popup', MaterialPopup);
|
||||
register('material-input', MaterialInput);
|
||||
|
||||
const createApp = component(DockerRegistryUI);
|
||||
const tags = document.getElementsByTagName('docker-registry-ui');
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
createApp(tags.item(i));
|
||||
}
|
||||