feat: expose some custom labels

This commit is contained in:
Joxit 2022-03-23 09:18:20 +01:00
parent 19e72e4a5f
commit ba6d817b41
No known key found for this signature in database
GPG key ID: F526592B8E012263
7 changed files with 182 additions and 129 deletions

View file

@ -100,6 +100,7 @@ Some env options are available for use this interface for **only one server**.
- `DEFAULT_REGISTRIES`: List of comma separated registry URLs (e.g `http://registry.example.com,http://registry:5000`), available only when `SINGLE_REGISTRY=false`. (default: ` `). - `DEFAULT_REGISTRIES`: List of comma separated registry URLs (e.g `http://registry.example.com,http://registry:5000`), available only when `SINGLE_REGISTRY=false`. (default: ` `).
- `READ_ONLY_REGISTRIES`: Desactivate dialog for remove and add new registries, available only when `SINGLE_REGISTRY=false`. (default: `false`). - `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`). - `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.
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/). 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/).

View file

@ -9,6 +9,7 @@ sed -i "s~\${SHOW_CONTENT_DIGEST}~${SHOW_CONTENT_DIGEST}~" index.html
sed -i "s~\${DEFAULT_REGISTRIES}~${DEFAULT_REGISTRIES}~" index.html sed -i "s~\${DEFAULT_REGISTRIES}~${DEFAULT_REGISTRIES}~" index.html
sed -i "s~\${READ_ONLY_REGISTRIES}~${READ_ONLY_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~\${SHOW_CATALOG_NB_TAGS}~${SHOW_CATALOG_NB_TAGS}~" index.html
sed -i "s~\${HISTORY_CUSTOM_LABELS}~${HISTORY_CUSTOM_LABELS}~" index.html
if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then if [ -z "${DELETE_IMAGES}" ] || [ "${DELETE_IMAGES}" = false ] ; then
sed -i "s/\${DELETE_IMAGES}/false/" index.html sed -i "s/\${DELETE_IMAGES}/false/" index.html

View file

@ -42,7 +42,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }" <tag-history registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }" image="{ router.getTagHistoryImage() }" tag="{ router.getTagHistoryTag() }"
is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }" is-image-remove-activated="{ truthy(props.isImageRemoveActivated) }" on-notify="{ notifySnackbar }"
on-authentication="{ onAuthentication }"></tag-history> on-authentication="{ onAuthentication }" history-custom-labels="{ stringToArray(props.historyCustomLabels) }"></tag-history>
</route> </route>
</router> </router>
<registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }" <registry-authentication realm="{ state.realm }" scope="{ state.scope }" service="{ state.service }"
@ -80,7 +80,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
stripHttps, stripHttps,
getRegistryServers, getRegistryServers,
setRegistryServers, setRegistryServers,
truthy truthy,
stringToArray
} from '../scripts/utils'; } from '../scripts/utils';
import router from '../scripts/router'; import router from '../scripts/router';
@ -175,7 +176,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)', baseRoute: '([^#]*?)/(\\?[^#]*?)?(#!)?(/?)',
router, router,
version, version,
truthy truthy,
stringToArray
} }
</script> </script>
</docker-registry-ui> </docker-registry-ui>

View file

@ -15,17 +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/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
--> -->
<tag-history-element class="{ state.key }"> <tag-history-element class="{ state.key }">
<div class="headline"><i class="material-icons">{ state.icon }</i> <div class="headline">
<i class="material-icons">{ state.icon }</i>
<p>{ state.name }</p> <p>{ state.name }</p>
</div> </div>
<div class="content"> <div class="content">
<div class="value" if="{ state.value }"> { state.value }</div> <div class="value" if="{ state.value }">{ state.value }</div>
<div class="values value" each="{ value in state.values }" if="{ state.values }"> { value }</div> <div class="values value" each="{ value in state.values }" if="{ state.values }">{ value }</div>
</div> </div>
<script> <script>
import { import { getHistoryIcon } from '../../scripts/utils';
getHistoryIcon
} from '../../scripts/utils';
export default { export default {
onBeforeStart(props, state) { onBeforeStart(props, state) {
state.key = props.entry.key; state.key = props.entry.key;
@ -48,14 +47,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return name; return name;
} else if (name === 'os') { } else if (name === 'os') {
return 'OS'; return 'OS';
} else if (name.startsWith('custom-label-')) {
name = name.replace('custom-label-', '');
} }
return name.replace(/([a-z])([A-Z])/g, '$1 $2') return name
.replace('_', ' ') .replace(/([a-z])([A-Z])/g, '$1 $2')
.replace(/[_-]/g, ' ')
.split(' ') .split(' ')
.map(word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`) .map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`)
.join(' '); .join(' ');
} },
} };
</script> </script>
<style> <style>
:host.Labels .value, :host.Labels .value,
@ -70,7 +72,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:host.docker_version .headline .material-icons { :host.docker_version .headline .material-icons {
background-size: 24px auto; background-size: 24px auto;
background-image: url("images/docker-logo.svg"); background-image: url('images/docker-logo.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
} }

View file

@ -20,33 +20,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ toTaglist }"> <material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ toTaglist }">
<i class="material-icons">arrow_back</i> <i class="material-icons">arrow_back</i>
</material-button> </material-button>
<h2> <h2>History of { props.image }:{ props.tag } <i class="material-icons">history</i></h2>
History of { props.image }:{ props.tag } <i class="material-icons">history</i>
</h2>
</div> </div>
</material-card> </material-card>
<div if="{ !state.loadend }" class="spinner-wrapper"> <div if="{ !state.loadend }" class="spinner-wrapper">
<material-spinner /> <material-spinner></material-spinner>
</div> </div>
<material-tabs if="{ state.archs && state.loadend }" useLine="{ true }" tabs="{ state.archs }" <material-tabs
onTabChanged="{ onTabChanged }" /> if="{ state.archs && state.loadend }"
useLine="{ true }"
tabs="{ state.archs }"
onTabChanged="{ onTabChanged }"
></material-tabs>
<material-card each="{ element in state.elements }" class="tag-history-element"> <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 }" /> <tag-history-element
each="{ entry in element }"
if="{ entry.value && entry.value.length > 0}"
entry="{ entry }"
></tag-history-element>
</material-card> </material-card>
<script> <script>
import { import { DockerImage } from '../../scripts/docker-image';
DockerImage import { bytesToSize } from '../../scripts/utils';
} from '../../scripts/docker-image';
import {
bytesToSize
} from '../../scripts/utils';
import router from '../../scripts/router'; import router from '../../scripts/router';
import TagHistoryElement from './tag-history-element.riot' import TagHistoryElement from './tag-history-element.riot';
export default { export default {
components: { components: {
TagHistoryElement TagHistoryElement,
}, },
onBeforeMount(props, state) { onBeforeMount(props, state) {
state.elements = []; state.elements = [];
@ -54,9 +56,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
list: true, list: true,
registryUrl: props.registryUrl, registryUrl: props.registryUrl,
onNotify: props.onNotify, onNotify: props.onNotify,
onAuthentication: props.onAuthentication onAuthentication: props.onAuthentication,
}); });
state.image.fillInfo() state.image.fillInfo();
}, },
onMounted(props, state) { onMounted(props, state) {
state.image.on('blobs', this.processBlobs); state.image.on('blobs', this.processBlobs);
@ -64,16 +66,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}, },
onTabChanged(arch, idx) { onTabChanged(arch, idx) {
const state = this.state; const state = this.state;
const { const { registryUrl, onNotify } = this.props;
registryUrl, state.elements = [];
onNotify state.image.variants[idx] =
} = this.props; state.image.variants[idx] ||
state.elements = []
state.image.variants[idx] = state.image.variants[idx] ||
new DockerImage(this.props.image, arch.digest, { new DockerImage(this.props.image, arch.digest, {
list: false, list: false,
registryUrl, registryUrl,
onNotify onNotify,
}); });
if (state.image.variants[idx].blobs) { if (state.image.variants[idx].blobs) {
return this.processBlobs(state.image.variants[idx].blobs); return this.processBlobs(state.image.variants[idx].blobs);
@ -83,6 +83,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}, },
processBlobs(blobs) { processBlobs(blobs) {
const state = this.state; const state = this.state;
const { historyCustomLabels } = this.props;
function exec(elt) { function exec(elt) {
const guiElements = []; const guiElements = [];
@ -90,8 +91,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') { if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
const value = elt[attribute]; const value = elt[attribute];
const guiElement = { const guiElement = {
"key": attribute, 'key': attribute,
"value": modifySpecificAttributeTypes(attribute, value) 'value': modifySpecificAttributeTypes(attribute, value),
}; };
guiElements.push(guiElement); guiElements.push(guiElement);
} }
@ -99,32 +100,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return guiElements.sort(eltSort); return guiElements.sort(eltSort);
} }
const elements = new Array(blobs.history.length + 1); const elements = new Array(blobs.history.length + 1);
elements[0] = exec(getConfig(blobs)); elements[0] = exec(getConfig(blobs, { historyCustomLabels }));
blobs.history.forEach(function (elt, i) { blobs.history.forEach(function (elt, i) {
elements[blobs.history.length - i] = exec(elt) elements[blobs.history.length - i] = exec(elt);
}); });
this.update({ this.update({
elements, elements,
loadend: true loadend: true,
}); });
}, },
multiArchList(manifests) { multiArchList(manifests) {
manifests = manifests.manifests || manifests; manifests = manifests.manifests || manifests;
const archs = manifests.map(function (manifest) { const archs = manifests.map(function (manifest) {
return { return {
title: manifest.platform.os + '/' + manifest.platform.architecture + (manifest.platform.variant ? title:
manifest.platform.variant : ''), manifest.platform.os +
digest: manifest.digest '/' +
} manifest.platform.architecture +
(manifest.platform.variant ? manifest.platform.variant : ''),
digest: manifest.digest,
};
}); });
this.update({ this.update({
archs archs,
}); });
}, },
toTaglist() { toTaglist() {
router.taglist(this.props.image); router.taglist(this.props.image);
} },
} };
const eltIdx = function (e) { const eltIdx = function (e) {
switch (e) { switch (e) {
case 'created': case 'created':
@ -158,7 +162,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return new Date(value).toLocaleString(); return new Date(value).toLocaleString();
case 'created_by': case 'created_by':
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/); const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+)/);
return (cmd && cmd[1]) || 'RUN' return (cmd && cmd[1]) || 'RUN';
case 'size': case 'size':
return bytesToSize(value); return bytesToSize(value);
case 'Entrypoint': case 'Entrypoint':
@ -175,25 +179,46 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
return value || ''; return value || '';
}; };
const getConfig = function (blobs) { const getConfig = function (blobs, { historyCustomLabels }) {
const res = ['architecture', 'User', 'created', 'docker_version', 'os', 'Cmd', 'Entrypoint', 'Env', 'Labels', console.log(this);
'User', 'Volumes', 'WorkingDir', 'author', 'id', 'ExposedPorts' const res = [
] 'architecture',
.reduce(function (acc, e) { 'User',
const value = blobs[e] || blobs.config[e]; 'created',
if (value && e === 'architecture' && blobs.variant) { 'docker_version',
acc[e] = value + blobs.variant; 'os',
} else if (value) { 'Cmd',
acc[e] = value; 'Entrypoint',
} 'Env',
return acc; '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)) { if (!res.author && res.Labels && res.Labels.maintainer) {
res.author = blobs.config.Labels.maintainer; res.author = blobs.config.Labels.maintainer;
delete res.Labels.maintainer; delete res.Labels.maintainer;
} }
historyCustomLabels
.filter((label) => res.Labels[label])
.forEach((label) => {
res[`custom-label-${label}`] = res.Labels[label];
delete res.Labels[label];
});
return res; return res;
}; };
</script> </script>

View file

@ -16,63 +16,78 @@
--> -->
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="UTF-8" />
<!-- build:css docker-registry-ui.css -->
<link href="../node_modules/riot-mui/build/styles/riot-mui.min.css" rel="stylesheet" type="text/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" />
<!-- endbuild -->
<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>
<head> <body>
<meta charset="UTF-8"> <!-- build:keep production -->
<!-- build:css docker-registry-ui.css --> <docker-registry-ui
<link href="../node_modules/riot-mui/build/styles/riot-mui.min.css" rel="stylesheet" type="text/css"> registry-url="${REGISTRY_URL}"
<link href="style.css" rel="stylesheet" type="text/css"> name="${REGISTRY_TITLE}"
<link href="material-icons.css" rel="stylesheet" type="text/css"> pull-url="${PULL_URL}"
<link href="roboto.css" rel="stylesheet" type="text/css"> show-content-digest="${SHOW_CONTENT_DIGEST}"
<!-- endbuild --> is-image-remove-activated="${DELETE_IMAGES}"
<meta name="viewport" content="width=device-width, initial-scale=1"> catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}"
<meta property="og:site_name" content="Docker Registry UI" /> single-registry="${SINGLE_REGISTRY}"
<meta name="twitter:card" content="summary" /> default-registries="${DEFAULT_REGISTRIES}"
<meta name="twitter:site" content="@Joxit" /> read-only-registries="${READ_ONLY_REGISTRIES}"
<meta name="twitter:creator" content="@Jones Magloire" /> show-catalog-nb-tags="${SHOW_CATALOG_NB_TAGS}"
<title>Docker Registry UI</title> history-custom-labels="${HISTORY_CUSTOM_LABELS}"
</head> >
</docker-registry-ui>
<body> <!-- endbuild -->
<!-- build:keep production --> <!-- build:keep developement -->
<docker-registry-ui registry-url="${REGISTRY_URL}" name="${REGISTRY_TITLE}" pull-url="${PULL_URL}" <docker-registry-ui
show-content-digest="${SHOW_CONTENT_DIGEST}" is-image-remove-activated="${DELETE_IMAGES}" registry-url=""
catalog-elements-limit="${CATALOG_ELEMENTS_LIMIT}" single-registry="${SINGLE_REGISTRY}" name="Developement Registry"
default-registries="${DEFAULT_REGISTRIES}" read-only-registries="${READ_ONLY_REGISTRIES}" pull-url=""
show-catalog-nb-tags="${SHOW_CATALOG_NB_TAGS}"> show-content-digest="true"
</docker-registry-ui> is-image-remove-activated="true"
<!-- endbuild --> catalog-elements-limit="1000"
<!-- build:keep developement --> single-registry="false"
<docker-registry-ui registry-url="" name="Developement Registry" pull-url="" show-content-digest="true" show-catalog-nb-tags="true"
is-image-remove-activated="true" catalog-elements-limit="1000" single-registry="false" show-catalog-nb-tags="true"> history-custom-labels="first_custom_labels,second_custom_labels"
</docker-registry-ui> >
<!-- endbuild --> </docker-registry-ui>
<!-- build:js docker-registry-ui.js --> <!-- endbuild -->
<script src="../node_modules/riot/riot+compiler.min.js"></script> <!-- build:js docker-registry-ui.js -->
<script src="../node_modules/riot-route/dist/route.js"></script> <script src="../node_modules/riot/riot+compiler.min.js"></script>
<script src="../node_modules/riot-mui/build/js/riot-mui.js"></script> <script src="../node_modules/riot-route/dist/route.js"></script>
<script src="tags/catalog.riot" type="riot/tag"></script> <script src="../node_modules/riot-mui/build/js/riot-mui.js"></script>
<script src="tags/catalog-element.riot" type="riot/tag"></script> <script src="tags/catalog.riot" type="riot/tag"></script>
<script src="tags/tag-history-button.riot" type="riot/tag"></script> <script src="tags/catalog-element.riot" type="riot/tag"></script>
<script src="tags/tag-history.riot" type="riot/tag"></script> <script src="tags/tag-history-button.riot" type="riot/tag"></script>
<script src="tags/tag-history-element.riot" type="riot/tag"></script> <script src="tags/tag-history.riot" type="riot/tag"></script>
<script src="tags/taglist.riot" type="riot/tag"></script> <script src="tags/tag-history-element.riot" type="riot/tag"></script>
<script src="tags/image-tag.riot" type="riot/tag"></script> <script src="tags/taglist.riot" type="riot/tag"></script>
<script src="tags/remove-image.riot" type="riot/tag"></script> <script src="tags/image-tag.riot" type="riot/tag"></script>
<script src="tags/copy-to-clipboard.riot" type="riot/tag"></script> <script src="tags/remove-image.riot" type="riot/tag"></script>
<script src="tags/dialogs/add.riot" type="riot/tag"></script> <script src="tags/copy-to-clipboard.riot" type="riot/tag"></script>
<script src="tags/dialogs/change.riot" type="riot/tag"></script> <script src="tags/dialogs/add.riot" type="riot/tag"></script>
<script src="tags/dialogs/remove.riot" type="riot/tag"></script> <script src="tags/dialogs/change.riot" type="riot/tag"></script>
<script src="tags/dialogs/menu.riot" type="riot/tag"></script> <script src="tags/dialogs/remove.riot" type="riot/tag"></script>
<script src="tags/image-size.riot" type="riot/tag"></script> <script src="tags/dialogs/menu.riot" type="riot/tag"></script>
<script src="tags/image-date.riot" type="riot/tag"></script> <script src="tags/image-size.riot" type="riot/tag"></script>
<script src="tags/image-content-digest.riot" type="riot/tag"></script> <script src="tags/image-date.riot" type="riot/tag"></script>
<script src="tags/pagination.riot" type="riot/tag"></script> <script src="tags/image-content-digest.riot" type="riot/tag"></script>
<script src="tags/app.riot" type="riot/tag"></script> <script src="tags/pagination.riot" type="riot/tag"></script>
<script src="scripts/http.js"></script> <script src="tags/app.riot" type="riot/tag"></script>
<script src="scripts/script.js"></script> <script src="scripts/http.js"></script>
<script src="scripts/utils.js"></script> <script src="scripts/script.js"></script>
<!-- endbuild --> <script src="scripts/utils.js"></script>
</body> <!-- endbuild -->
</body>
</html> </html>

View file

@ -76,7 +76,10 @@ export function getHistoryIcon(attribute) {
case 'ExposedPorts': case 'ExposedPorts':
return 'router'; return 'router';
default: default:
''; if (attribute.startsWith('custom-label-')) {
return 'label';
}
return '';
} }
} }
@ -201,3 +204,7 @@ export function decodeURI(url) {
export function truthy(value) { export function truthy(value) {
return value === true || value === 'true'; return value === true || value === 'true';
} }
export function stringToArray(value) {
return value && typeof value === 'string' ? value.split(',') : [];
}