From 34fd13d6b702561a79b7185b92edfe800733797b Mon Sep 17 00:00:00 2001 From: Jones Magloire Date: Mon, 12 Sep 2022 18:29:03 +0200 Subject: [PATCH] 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` --- README.md | 7 ++++--- bin/90-docker-registry-ui.sh | 1 + src/components/docker-registry-ui.riot | 3 +++ src/components/tag-history/tag-history.riot | 4 +++- src/components/tag-list/tag-list.riot | 1 + src/index.html | 2 ++ src/scripts/docker-image.js | 10 ++++++++-- 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 130a79e8..0018496b 100644 --- a/README.md +++ b/README.md @@ -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'] diff --git a/bin/90-docker-registry-ui.sh b/bin/90-docker-registry-ui.sh index f8c9cf34..0f912062 100755 --- a/bin/90-docker-registry-ui.sh +++ b/bin/90-docker-registry-ui.sh @@ -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 diff --git a/src/components/docker-registry-ui.riot b/src/components/docker-registry-ui.riot index 2878922e..eb6a0ce2 100644 --- a/src/components/docker-registry-ui.riot +++ b/src/components/docker-registry-ui.riot @@ -52,6 +52,7 @@ along with this program. If not, see . on-notify="{ notifySnackbar }" filter-results="{ state.filter }" on-authentication="{ onAuthentication }" + use-control-cache-header="{ truthy(props.useControlCacheHeader) }" > @@ -65,6 +66,7 @@ along with this program. If not, see . on-notify="{ notifySnackbar }" on-authentication="{ onAuthentication }" history-custom-labels="{ stringToArray(props.historyCustomLabels) }" + use-control-cache-header="{ truthy(props.useControlCacheHeader) }" > @@ -133,6 +135,7 @@ along with this program. If not, see . 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({ diff --git a/src/components/tag-history/tag-history.riot b/src/components/tag-history/tag-history.riot index 32e76d91..52a9ddd7 100644 --- a/src/components/tag-history/tag-history.riot +++ b/src/components/tag-history/tag-history.riot @@ -57,6 +57,7 @@ along with this program. If not, see . 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 . }, 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 . list: false, registryUrl, onNotify, + useControlCacheHeader, }); if (state.image.variants[idx].blobs) { return this.processBlobs(state.image.variants[idx].blobs); diff --git a/src/components/tag-list/tag-list.riot b/src/components/tag-list/tag-list.riot index 437a90bb..354e7ee1 100644 --- a/src/components/tag-list/tag-list.riot +++ b/src/components/tag-list/tag-list.riot @@ -95,6 +95,7 @@ along with this program. If not, see . registryUrl: props.registryUrl, onNotify: props.onNotify, onAuthentication: props.onAuthentication, + useControlCacheHeader: props.useControlCacheHeader, }) ) .sort(compare); diff --git a/src/index.html b/src/index.html index d8364297..69ddeb7d 100644 --- a/src/index.html +++ b/src/index.html @@ -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}" > @@ -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" > diff --git a/src/scripts/docker-image.js b/src/scripts/docker-image.js index 0a2f2ec5..74df9b52 100644 --- a/src/scripts/docker-image.js +++ b/src/scripts/docker-image.js @@ -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); @@ -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,9 @@ 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); }