Compare commits

...

5 Commits
2.2.2 ... 2.3.1

Author SHA1 Message Date
Joxit
4e5b768833 fix(nginx): upgrading proxy_pass HTTP version from 1.0 to 1.1 (#270)
fixes #270
2022-10-08 01:03:13 +02:00
Joxit
c1f6c43e4a build: release 2.3.0 🚀 2022-09-20 22:29:20 +02:00
Joxit
fb8185907e fix(tag-list): missing details on images built by buildah (#264)
fixes #264
2022-09-19 08:44:09 +02:00
Joxit
636cb60ca8 docs(FAQ): improve issue about Basic auth and 401 status code (#266)
fixes #266
2022-09-18 14:20:05 +02:00
Jones Magloire
34fd13d6b7 feat(cache-control): new option USE_CONTROL_CACHE_HEADER adds Cache-Control header on requests to registry server (#265)
This option requires registry configuration: `Access-Control-Allow-Headers` with `Cache-Control`
2022-09-12 18:29:03 +02:00
17 changed files with 73 additions and 43 deletions

View File

@@ -36,4 +36,7 @@
- Ben Jackson [@bjj](https://github.com/bjj)
- 三十文 [@xfduan](https://github.com/xfduan)
- Aram Akhavan [@kaysond](https://github.com/kaysond)
- Jason Tackaberry [@jtackaberry](https://github.com/jtackaberry)
- Jason Tackaberry [@jtackaberry](https://github.com/jtackaberry)
- Maxime Loliée [@loliee](https://github.com/loliee)
- Enrico [@Enrico204](https://github.com/Enrico204)
- [@clyvari](https://github.com/clyvari)

View File

@@ -75,8 +75,8 @@ If you like my work and want to support it, don't hesitate to [sponsor me](https
- This means you are using a UI with HTTPS and your registry is using HTTP (unsecured). When you are on a HTTPS site, you can't get HTTP content. Upgrade you registry with a HTTPS connection.
- Why the default nginx `Host` is set to `$http_host` ?
- This fixes the issue [#88](https://github.com/Joxit/docker-registry-ui/issues/88). More about this in [#113](https://github.com/Joxit/docker-registry-ui/issues/113).
- 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 and use `NGINX_PROXY_PASS_URL` e.g. registry.example.com/ui/. (see [#104](https://github.com/Joxit/docker-registry-ui/issues/104), [#204](https://github.com/Joxit/docker-registry-ui/issues/204), [#207](https://github.com/Joxit/docker-registry-ui/issues/207), [#214](https://github.com/Joxit/docker-registry-ui/issues/214)).
- Why OPTIONS (aka preflight requests) and DELETE fails with 401 status code (using Basic Auth) ?
- This is caused by a bug in docker registry, it returns 401 status requests on preflight requests, this breaks [W3C preflight-request specification](https://www.w3.org/TR/cors/#preflight-request). I suggest to have your UI on the same domain than your registry e.g. registry.example.com/ui/ **or** use `NGINX_PROXY_PASS_URL` **or** configure a nginx/apache/haproxy in front of your registry that returns 200 on each OPTIONS requests. (see [#104](https://github.com/Joxit/docker-registry-ui/issues/104), [#204](https://github.com/Joxit/docker-registry-ui/issues/204), [#207](https://github.com/Joxit/docker-registry-ui/issues/207), [#214](https://github.com/Joxit/docker-registry-ui/issues/214), [#266](https://github.com/Joxit/docker-registry-ui/issues/266)).
- 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/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?
@@ -110,6 +110,7 @@ Some env options are available for use this interface for **only one server**.
- `READ_ONLY_REGISTRIES`: Desactivate dialog for remove and add new registries, available only when `SINGLE_REGISTRY=false`. (default: `false`).
- `SHOW_CATALOG_NB_TAGS`: Show number of tags per images on catalog page. This will produce + nb images requests, not recommended on large registries. (default: `false`).
- `HISTORY_CUSTOM_LABELS`: Expose custom labels in history page, custom labels will be processed like maintainer label.
- `USE_CONTROL_CACHE_HEADER`: Use `Control-Cache` header and set to `no-store, no-cache`. This will avoid some issues on multi-arch images (see [#260](https://github.com/Joxit/docker-registry-ui/issues/260)). This option requires registry configuration: `Access-Control-Allow-Headers` with `Cache-Control`. (default: `false`).
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/).
@@ -128,7 +129,7 @@ http:
headers:
Access-Control-Allow-Origin: ['http://registry.example.com']
Access-Control-Allow-Credentials: [true]
Access-Control-Allow-Headers: ['Authorization', 'Accept']
Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS'] # Optional
```
@@ -150,7 +151,7 @@ And you need to add these HEADERS:
http:
headers:
Access-Control-Allow-Methods: ['HEAD', 'GET', 'OPTIONS', 'DELETE']
Access-Control-Allow-Headers: ['Authorization', 'Accept']
Access-Control-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
Access-Control-Expose-Headers: ['Docker-Content-Digest']
```
@@ -178,7 +179,7 @@ http:
X-Content-Type-Options: [nosniff]
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-Allow-Headers: ['Authorization', 'Accept', 'Cache-Control']
Access-Control-Max-Age: [1728000]
Access-Control-Allow-Credentials: [true]
Access-Control-Expose-Headers: ['Docker-Content-Digest']

View File

@@ -10,6 +10,7 @@ sed -i "s~\${DEFAULT_REGISTRIES}~${DEFAULT_REGISTRIES}~" index.html
sed -i "s~\${READ_ONLY_REGISTRIES}~${READ_ONLY_REGISTRIES}~" index.html
sed -i "s~\${SHOW_CATALOG_NB_TAGS}~${SHOW_CATALOG_NB_TAGS}~" index.html
sed -i "s~\${HISTORY_CUSTOM_LABELS}~${HISTORY_CUSTOM_LABELS}~" index.html
sed -i "s~\${USE_CONTROL_CACHE_HEADER}~${USE_CONTROL_CACHE_HEADER}~" index.html
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
sed -i "s/\${DELETE_IMAGES}/false/" index.html

File diff suppressed because one or more lines are too long

1
dist/index.html vendored
View File

@@ -25,4 +25,5 @@
read-only-registries="${READ_ONLY_REGISTRIES}"
show-catalog-nb-tags="${SHOW_CATALOG_NB_TAGS}"
history-custom-labels="${HISTORY_CUSTOM_LABELS}"
use-control-cache-header="${USE_CONTROL_CACHE_HEADER}"
></docker-registry-ui><script src="docker-registry-ui.js"></script></body></html>

View File

@@ -26,6 +26,7 @@ server {
#! if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) {
#! return 404;
#! }
#! proxy_http_version 1.1;
#! ${NGINX_PROXY_HEADERS}
#! ${NGINX_PROXY_PASS_HEADERS}
#! proxy_pass ${NGINX_PROXY_PASS_URL};

View File

@@ -1,6 +1,6 @@
{
"name": "docker-registry-ui",
"version": "2.2.2",
"version": "2.3.1",
"scripts": {
"format": "npm run format-html && npm run format-js && npm run format-riot",
"format-html": "find src rollup rollup.config.js -name '*.html' -exec prettier --config .prettierrc -w --parser html {} \\;",

View File

@@ -36,13 +36,13 @@
import router from '../../scripts/router';
export default {
displayImagesToDelete(toDelete, tags) {
const digests = new Set();
const contentDigests = new Set();
toDelete.forEach((image) => {
if (image.digest) {
digests.add(image.digest);
if (image.contentDigest) {
contentDigests.add(image.contentDigest);
}
});
return tags.filter((image) => digests.has(image.digest));
return tags.filter((image) => contentDigests.has(image.contentDigest));
},
deleteImages() {
this.props.toDelete.forEach((image) => this.getContentDigestThenDelete(image, this.props));
@@ -53,11 +53,11 @@
const self = this;
oReq.addEventListener('loadend', function () {
if (this.status === 200 || this.status === 202) {
oReq.getContentDigest(function (digest) {
if (!digest) {
oReq.getContentDigest(function (contentDigest) {
if (!contentDigest) {
onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
} else {
self.deleteImage({ name, tag, digest }, opts);
self.deleteImage({ name, tag, contentDigest }, opts);
}
});
} else if (this.status === 404) {
@@ -73,7 +73,7 @@
);
oReq.send();
},
deleteImage({ name, tag, digest }, opts) {
deleteImage({ name, tag, contentDigest }, opts) {
const { registryUrl, ignoreError, onNotify, onAuthentication, onClick } = opts;
const oReq = new Http({ onAuthentication });
oReq.addEventListener('loadend', function () {
@@ -91,7 +91,7 @@
}
onClick();
});
oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${digest}`);
oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${contentDigest}`);
oReq.setRequestHeader(
'Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json'

View File

@@ -52,6 +52,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
on-notify="{ notifySnackbar }"
filter-results="{ state.filter }"
on-authentication="{ onAuthentication }"
use-control-cache-header="{ truthy(props.useControlCacheHeader) }"
></tag-list>
</route>
<route path="{baseRoute}taghistory/(.*)">
@@ -65,6 +66,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
on-notify="{ notifySnackbar }"
on-authentication="{ onAuthentication }"
history-custom-labels="{ stringToArray(props.historyCustomLabels) }"
use-control-cache-header="{ truthy(props.useControlCacheHeader) }"
></tag-history>
</route>
</router>
@@ -86,7 +88,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<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 &amp; Terms</a>
<a href="https://github.com/Joxit/docker-registry-ui/blob/main/LICENSE">License AGPL-3.0</a>
</li>
</ul>
</material-footer>
@@ -133,6 +135,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
this.state.name = props.name || stripHttps(props.registryUrl);
this.state.catalogElementsLimit = props.catalogElementsLimit || 100000;
this.state.pullUrl = this.pullUrl(this.state.registryUrl, props.pullUrl);
this.state.useControlCacheHeader = props.useControlCacheHeader;
},
onServerChange(registryUrl) {
this.update({

View File

@@ -57,6 +57,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
registryUrl: props.registryUrl,
onNotify: props.onNotify,
onAuthentication: props.onAuthentication,
useControlCacheHeader: props.useControlCacheHeader,
});
state.image.fillInfo();
},
@@ -66,7 +67,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
},
onTabChanged(arch, idx) {
const state = this.state;
const { registryUrl, onNotify } = this.props;
const { registryUrl, onNotify, useControlCacheHeader } = this.props;
state.elements = [];
state.image.variants[idx] =
state.image.variants[idx] ||
@@ -74,6 +75,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
list: false,
registryUrl,
onNotify,
useControlCacheHeader,
});
if (state.image.variants[idx].blobs) {
return this.processBlobs(state.image.variants[idx].blobs);

View File

@@ -40,12 +40,12 @@
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}`;
return `docker pull ${props.pullUrl}/${props.image.name}@${props.image.contentDigest}`;
}
},
load(props, state) {
if (props.target !== 'tag' && !props.image.digest) {
props.image.one('content-digest', (digest) => {
if (props.target !== 'tag' && !props.image.contentDigest) {
props.image.one('content-digest', (contentDigest) => {
this.update();
});
props.image.trigger('get-content-digest');

View File

@@ -15,7 +15,7 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
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>
<div title="{ getTitle(props.image, state.chars) }">{ getContentDigest(props.image, state.chars) }</div>
<script>
export default {
onMounted(props, state) {
@@ -25,12 +25,12 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
this.load(props, state);
},
load(props, state) {
if (props.image.digest) {
if (props.image.contentDigest) {
return;
}
state.chars = -1;
props.image.one('content-digest', (digest) => {
this.digest = digest;
props.image.one('content-digest', (contentDigest) => {
this.contentDigest = contentDigest;
props.image.on('content-digest-chars', this.onResize);
props.image.trigger('get-content-digest-chars');
});
@@ -44,15 +44,15 @@ Copyright (C) 2016-2021 Jones Magloire @Joxit
}
},
getTitle(image, chars) {
return chars >= 70 ? '' : image.digest || '';
return chars >= 70 ? '' : image.contentDigest || '';
},
getDigest(image, chars) {
getContentDigest(image, chars) {
if (chars >= 70) {
return image.digest || '';
return image.contentDigest || '';
} else if (chars <= 0) {
return '';
} else {
return image.digest && image.digest.slice(0, chars) + '...';
return image.contentDigest && image.contentDigest.slice(0, chars) + '...';
}
},
};

View File

@@ -21,7 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
waves-color="#ddd"
title="This will delete the image."
if="{ !props.multiDelete }"
disabled="{ !state.digest }"
disabled="{ !state.contentDigest }"
onClick="{ deleteImage }"
>
<i class="material-icons">delete</i>
@@ -29,7 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<material-checkbox
if="{ props.multiDelete }"
title="Select this tag to delete it."
disabled="{ !state.digest }"
disabled="{ !state.contentDigest }"
onChange="{ handleCheckboxChange }"
checked="{ state.checked }"
>
@@ -41,9 +41,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
export default {
onBeforeMount(props, state) {
state.checked = props.checked;
props.image.one('content-digest', (digest) => {
props.image.one('content-digest', (contentDigest) => {
this.update({
digest,
contentDigest,
});
});
},

View File

@@ -92,9 +92,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
.map(
(tag) =>
new DockerImage(props.image, tag, {
list: true,
registryUrl: props.registryUrl,
onNotify: props.onNotify,
onAuthentication: props.onAuthentication,
useControlCacheHeader: props.useControlCacheHeader,
})
)
.sort(compare);

View File

@@ -46,6 +46,7 @@
read-only-registries="${READ_ONLY_REGISTRIES}"
show-catalog-nb-tags="${SHOW_CATALOG_NB_TAGS}"
history-custom-labels="${HISTORY_CUSTOM_LABELS}"
use-control-cache-header="${USE_CONTROL_CACHE_HEADER}"
>
</docker-registry-ui>
<!-- endbuild -->
@@ -60,6 +61,7 @@
single-registry="false"
show-catalog-nb-tags="true"
history-custom-labels="first_custom_labels,second_custom_labels"
use-control-cache-header="false"
>
</docker-registry-ui>
<!-- endbuild -->

View File

@@ -46,7 +46,7 @@ export function compare(e1, e2) {
}
export class DockerImage {
constructor(name, tag, { list, registryUrl, onNotify, onAuthentication }) {
constructor(name, tag, { list, registryUrl, onNotify, onAuthentication, useControlCacheHeader }) {
this.name = name;
this.tag = tag;
this.chars = 0;
@@ -55,6 +55,7 @@ export class DockerImage {
registryUrl,
onNotify,
onAuthentication,
useControlCacheHeader,
};
this.ociImage = false;
observable(this);
@@ -83,8 +84,8 @@ export class DockerImage {
return this.trigger('content-digest-chars', this.chars);
});
this.on('get-content-digest', function () {
if (this.digest !== undefined) {
return this.trigger('content-digest', this.digest);
if (this.contentDigest !== undefined) {
return this.trigger('content-digest', this.contentDigest);
}
return this.fillInfo();
});
@@ -116,10 +117,10 @@ export class DockerImage {
self.sha256 = response.config && response.config.digest;
self.trigger('size', self.size);
self.trigger('sha256', self.sha256);
oReq.getContentDigest(function (digest) {
self.digest = digest;
self.trigger('content-digest', digest);
if (!digest) {
oReq.getContentDigest(function (contentDigest) {
self.contentDigest = contentDigest;
self.trigger('content-digest', contentDigest);
if (!contentDigest) {
self.opts.onNotify(ERROR_CAN_NOT_READ_CONTENT_DIGEST);
}
});
@@ -143,6 +144,9 @@ export class DockerImage {
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json' +
(self.opts.list ? ', application/vnd.docker.distribution.manifest.list.v2+json' : '')
);
if (self.opts.useControlCacheHeader) {
oReq.setRequestHeader('Cache-Control', 'no-store, no-cache');
}
oReq.send();
}
getBlobs(blob) {
@@ -165,7 +169,12 @@ export class DockerImage {
self.trigger('creation-date', self.creationDate);
self.trigger('blobs', self.blobs);
} else if (this.status === 404) {
self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found`, true);
self.opts.onNotify(`Blobs for ${self.name}:${self.tag} not found: blob '${self.blobs}'`, true);
} else if (!this.responseText) {
self.opts.onNotify(
`Can"t get blobs for ${self.name}:${self.tag}: blob '${self.blobs}' (no message error)`,
true
);
} else {
self.opts.onNotify(this.responseText);
}

View File

@@ -136,8 +136,13 @@ export function stripHttps(url) {
return url.replace(/^https?:\/\//, '');
}
function kebabToCamelCase(s) {
return s.replace(/-[a-z]/, (x) => x[1].toUpperCase());
}
export function eventTransfer(from, to) {
from.on('*', function (event, param) {
to[kebabToCamelCase(event)] = param;
to.trigger(event, param);
});
}