292 lines
8.5 KiB
Plaintext
292 lines
8.5 KiB
Plaintext
<!--
|
|
Copyright (C) 2016-2023 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>
|
|
<div class="material-card-title-action">
|
|
<material-button
|
|
text-color="var(--neutral-text)"
|
|
color="inherit"
|
|
waves-color="var(--hover-background)"
|
|
waves-center="true"
|
|
rounded="true"
|
|
href="{ toTaglist() }"
|
|
icon
|
|
>
|
|
<i class="material-icons">arrow_back</i>
|
|
</material-button>
|
|
<h2>History of { props.image }:{ props.tag } <i class="material-icons">history</i></h2>
|
|
<material-button
|
|
text-color="var(--accent-text)"
|
|
color="inherit"
|
|
waves-color="var(--hover-background)"
|
|
waves-center="true"
|
|
rounded="true"
|
|
outlined
|
|
onClick="{ showDockerfile }"
|
|
>
|
|
Dockerfile
|
|
</material-button>
|
|
</div>
|
|
</material-card>
|
|
<div if="{ !state.loadend }" class="spinner-wrapper">
|
|
<material-spinner></material-spinner>
|
|
</div>
|
|
<dockerfile
|
|
opened="{ state.showDockerfile }"
|
|
on-close="{ onDockerfileClose }"
|
|
elements="{ state.elements }"
|
|
></dockerfile>
|
|
|
|
<material-tabs
|
|
if="{ state.archs && state.loadend }"
|
|
color="var(--background)"
|
|
text-color="var(--primary-text)"
|
|
text-selected-color="var(--accent-text)"
|
|
line-color="var(--background)"
|
|
line-selected-color="var(--accent-text)"
|
|
useLine="{ true }"
|
|
tabs="{ state.archs }"
|
|
onTabChanged="{ onTabChanged }"
|
|
></material-tabs>
|
|
|
|
<material-card each="{ element in state.elements }">
|
|
<tag-history-element
|
|
each="{ entry in element }"
|
|
if="{ entry.value && entry.value.length > 0}"
|
|
entry="{ entry }"
|
|
></tag-history-element>
|
|
</material-card>
|
|
<script>
|
|
import { DockerImage } from '../../scripts/docker-image';
|
|
import { bytesToSize } from '../../scripts/utils';
|
|
import Dockerfile from '../dialogs/dockerfile.riot';
|
|
import router from '../../scripts/router';
|
|
import TagHistoryElement from './tag-history-element.riot';
|
|
export default {
|
|
components: {
|
|
TagHistoryElement,
|
|
Dockerfile,
|
|
},
|
|
onBeforeMount(props, state) {
|
|
state.elements = [];
|
|
state.image = new DockerImage(props.image, props.tag, {
|
|
list: true,
|
|
registryUrl: props.registryUrl,
|
|
onNotify: props.onNotify,
|
|
onAuthentication: props.onAuthentication,
|
|
useControlCacheHeader: props.useControlCacheHeader,
|
|
});
|
|
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;
|
|
const { registryUrl, onNotify, useControlCacheHeader } = this.props;
|
|
state.elements = [];
|
|
state.image.variants[idx] =
|
|
state.image.variants[idx] ||
|
|
new DockerImage(this.props.image, arch.digest, {
|
|
list: false,
|
|
registryUrl,
|
|
onNotify,
|
|
useControlCacheHeader,
|
|
});
|
|
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;
|
|
const { historyCustomLabels } = this.props;
|
|
|
|
function exec(elt) {
|
|
const guiElements = [];
|
|
for (var attribute in elt) {
|
|
if (elt.hasOwnProperty(attribute) && attribute != 'empty_layer') {
|
|
const value = elt[attribute];
|
|
const guiElement = modifySpecificAttributeTypes(attribute, value);
|
|
guiElements.push(guiElement);
|
|
}
|
|
}
|
|
return guiElements.sort(eltSort);
|
|
}
|
|
const elements = new Array(blobs.history.length + 1);
|
|
elements[0] = exec(getConfig(blobs, { historyCustomLabels }));
|
|
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() {
|
|
return router.taglist(this.props.image);
|
|
},
|
|
showDockerfile() {
|
|
console.log(this);
|
|
this.update({ showDockerfile: true });
|
|
},
|
|
onDockerfileClose() {
|
|
this.update({ showDockerfile: false });
|
|
},
|
|
};
|
|
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 parseCreatedBy = (value) => {
|
|
if (value.startsWith('COPY')) {
|
|
return {
|
|
value: 'COPY',
|
|
content: value.replace(/^COPY /, ''),
|
|
};
|
|
}
|
|
let cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+) (.*)/);
|
|
return {
|
|
value: (cmd && cmd[1]) || 'RUN',
|
|
content: (cmd && cmd[2]) || value.replace(/^\/bin\/sh *-c *(#\(nop\))?/, ''),
|
|
};
|
|
};
|
|
|
|
const modifySpecificAttributeTypes = function (key, value) {
|
|
switch (key) {
|
|
case 'created':
|
|
return { key, value: new Date(value).toLocaleString() };
|
|
case 'created_by':
|
|
const cmd = value.match(/\/bin\/sh *-c *#\(nop\) *([A-Z]+) (.*)/);
|
|
return {
|
|
key,
|
|
...parseCreatedBy(value),
|
|
};
|
|
case 'size':
|
|
return { key, value: bytesToSize(value) };
|
|
case 'Entrypoint':
|
|
case 'Cmd':
|
|
return { key, value: (value || []).join(' ') };
|
|
case 'Labels':
|
|
return {
|
|
key,
|
|
value: Object.keys(value || {}).map(function (elt) {
|
|
return value[elt] ? elt + '=' + value[elt] : '';
|
|
}),
|
|
};
|
|
case 'Volumes':
|
|
case 'ExposedPorts':
|
|
return { key, value: Object.keys(value) };
|
|
}
|
|
return { key, value: value || '' };
|
|
};
|
|
|
|
const getConfig = function (blobs, { historyCustomLabels }) {
|
|
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;
|
|
}
|
|
|
|
if (res.Labels) {
|
|
historyCustomLabels
|
|
.filter((label) => res.Labels[label])
|
|
.forEach((label) => {
|
|
res[`custom-label-${label}`] = res.Labels[label];
|
|
delete res.Labels[label];
|
|
});
|
|
}
|
|
|
|
return res;
|
|
};
|
|
</script>
|
|
<style>
|
|
h2 {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
}
|
|
</style>
|
|
</tag-history>
|