Deletion and more details
the build was successful
Details
the build was successful
Details
parent
f9abc53a30
commit
1649afe535
|
@ -5,6 +5,7 @@ BASE_URL=${BASE_URL:=/}
|
|||
REGISTRY_HOST=${REGISTRY_HOST:=""}
|
||||
REGISTRY_API=${REGISTRY_API:=""}
|
||||
|
||||
[[ "$DELETE_ENABLED" != "true" ]] && [[ "$DELETE_ENABLED" != "false" ]] && DELETE_ENABLED=false
|
||||
REPOSITORIES_PER_PAGE=${REPOSITORIES_PER_PAGE:=0}
|
||||
TAGS_PER_PAGE=${TAGS_PER_PAGE:=30}
|
||||
|
||||
|
@ -27,6 +28,7 @@ cat > /srv/config.json << EOF
|
|||
{
|
||||
"registryHost": "$REGISTRY_HOST",
|
||||
"registryAPI": "$REGISTRY_API",
|
||||
"deleteEnabled": $DELETE_ENABLED,
|
||||
"repositoriesPerPage": $REPOSITORIES_PER_PAGE,
|
||||
"tagsPerPage": $TAGS_PER_PAGE,
|
||||
"usePortusExplore": $USE_PORTUS_EXPLORE
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
version: 0.1
|
||||
log:
|
||||
fields:
|
||||
service: registry
|
||||
storage:
|
||||
cache:
|
||||
blobdescriptor: inmemory
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/registry
|
||||
delete:
|
||||
enabled: true
|
||||
http:
|
||||
addr: :5000
|
||||
headers:
|
||||
X-Content-Type-Options: [nosniff]
|
||||
Access-Control-Allow-Origin: ['*']
|
||||
Access-Control-Allow-Methods: ['GET, DELETE']
|
||||
Access-Control-Allow-Headers: ['Authorization']
|
||||
Access-Control-Expose-Headers: ['Content-Length, Www-Authenticate', 'Docker-Content-Digest']
|
||||
health:
|
||||
storagedriver:
|
||||
enabled: true
|
||||
interval: 10s
|
||||
threshold: 3
|
96
src/api.js
96
src/api.js
|
@ -1,6 +1,6 @@
|
|||
import parseLink from 'parse-link-header';
|
||||
|
||||
import { registryAPI, repositoriesPerPage, tagsPerPage, usePortusExplore } from '@/options';
|
||||
import { registryAPI, deleteEnabled, repositoriesPerPage, tagsPerPage, usePortusExplore } from '@/options';
|
||||
|
||||
function parseWWWAuthenticate(text) {
|
||||
const result = {};
|
||||
|
@ -86,28 +86,40 @@ async function paginatable(path, scope, n, last = null) {
|
|||
return Object.assign(await response.json(), { nextLast });
|
||||
}
|
||||
|
||||
async function get(path, scope) {
|
||||
async function request(method, path, scope, accept) {
|
||||
const url = new URL(`${await registryAPI()}${path}`);
|
||||
const headers = {};
|
||||
if (accept) {
|
||||
headers.Accept = accept;
|
||||
}
|
||||
if (scope) {
|
||||
const token = await doAuth(scope);
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
const response = await fetch(url, { headers });
|
||||
return response.json();
|
||||
const response = await fetch(url, { method, headers });
|
||||
if (!response.ok) {
|
||||
if (method === 'HEAD') {
|
||||
throw new Error(`${response.statusText}`);
|
||||
} else if (response.headers.get('Content-Type').startsWith('application/json')) {
|
||||
const r = await response.json();
|
||||
const firstError = r.errors[0];
|
||||
if (firstError) {
|
||||
throw new Error(`${firstError.code}: ${firstError.message}`);
|
||||
}
|
||||
|
||||
async function head(path, scope) {
|
||||
const url = new URL(`${await registryAPI()}${path}`);
|
||||
const headers = {};
|
||||
if (scope) {
|
||||
const token = await doAuth(scope);
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
const response = await fetch(url, { method: 'HEAD', headers });
|
||||
return response.headers;
|
||||
throw new Error(`${response.statusText}`);
|
||||
}
|
||||
if (!response.headers.get('Content-Type').startsWith('application/json')) {
|
||||
console.warn('response returned was not JSON, parsing may fail');
|
||||
}
|
||||
if (method === 'HEAD' || parseInt(response.headers.get('Content-Length'), 10) < 1) {
|
||||
return { headers: response.headers };
|
||||
}
|
||||
return {
|
||||
...(await response.json()),
|
||||
headers: response.headers,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async function portus() {
|
||||
// TODO: Use the Portus API when it enables anonymous access
|
||||
|
@ -135,10 +147,6 @@ async function repos(last = null) {
|
|||
return paginatable('/v2/_catalog', null, await repositoriesPerPage(), last);
|
||||
}
|
||||
|
||||
async function repo(name) {
|
||||
return get(`/v2/${name}`, `repository:${name}:pull`);
|
||||
}
|
||||
|
||||
async function tags(name, last = null) {
|
||||
if (await usePortusExplore()) {
|
||||
const p = await portus();
|
||||
|
@ -151,20 +159,66 @@ async function tags(name, last = null) {
|
|||
}
|
||||
|
||||
async function tag(name, ref) {
|
||||
return get(`/v2/${name}/manifests/${ref}`, `repository:${name}:pull`);
|
||||
return request('GET', `/v2/${name}/manifests/${ref}`, `repository:${name}:pull`, 'application/vnd.docker.distribution.manifest.v2+json');
|
||||
}
|
||||
|
||||
async function tagCanDelete(name, ref) {
|
||||
if (!await deleteEnabled()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const { headers } = await request('HEAD', `/v2/${name}/manifests/${ref}`, `repository:${name}:delete`, 'application/vnd.docker.distribution.manifest.v2+json');
|
||||
request('HEAD', `/v2/${name}/manifests/${headers.get('Docker-Content-Digest')}`, `repository:${name}:delete`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function tagDelete(name, ref) {
|
||||
const tagManifest = await tag(name, ref);
|
||||
// delete each blob
|
||||
// await Promise.all(tagManifest.layers.map(l =>
|
||||
// request('DELETE', `/v2/${name}/blobs/${l.digest}`, `repository:${name}:delete`)));
|
||||
return request('DELETE', `/v2/${name}/manifests/${tagManifest.headers.get('Docker-Content-Digest')}`, `repository:${name}:delete`);
|
||||
}
|
||||
|
||||
async function repoCanDelete(name) {
|
||||
if (!await deleteEnabled()) {
|
||||
return false;
|
||||
}
|
||||
const r = await request('GET', `/v2/${name}/tags/list`, `repository:${name}:delete`);
|
||||
if (!r.tags) {
|
||||
return false;
|
||||
}
|
||||
return Promise.race(r.tags.map(t => tagCanDelete(name, t)));
|
||||
}
|
||||
|
||||
async function repoDelete(name) {
|
||||
const r = await request('GET', `/v2/${name}/tags/list`, `repository:${name}:delete`);
|
||||
return Promise.all(r.tags.map(t => tagDelete(name, t)));
|
||||
}
|
||||
|
||||
async function blob(name, digest) {
|
||||
const headers = await head(`/v2/${name}/blobs/${digest}`, `repository:${name}:pull`);
|
||||
const { headers } = await request('HEAD', `/v2/${name}/blobs/${digest}`, `repository:${name}:pull`);
|
||||
return {
|
||||
dockerContentDigest: headers.get('Docker-Content-Digest'),
|
||||
contentLength: parseInt(headers.get('Content-Length'), 10),
|
||||
};
|
||||
}
|
||||
|
||||
async function configBlob(name, digest) {
|
||||
return request('GET', `/v2/${name}/blobs/${digest}`, `repository:${name}:pull`);
|
||||
}
|
||||
|
||||
export {
|
||||
repos,
|
||||
repo,
|
||||
tags,
|
||||
tag,
|
||||
tagCanDelete,
|
||||
tagDelete,
|
||||
repoCanDelete,
|
||||
repoDelete,
|
||||
blob,
|
||||
configBlob,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<LoadableText :text="size" />
|
||||
<LoadableText :text="text" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -14,15 +14,22 @@ export default {
|
|||
props: {
|
||||
repo: String,
|
||||
blob: String,
|
||||
size: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
size: '',
|
||||
text: '',
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
if (this.size) {
|
||||
this.text = filesize(this.size);
|
||||
return;
|
||||
}
|
||||
const size = await blob(this.repo, this.blob);
|
||||
this.size = filesize(size.contentLength);
|
||||
if (size.contentLength) {
|
||||
this.text = filesize(size.contentLength);
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -40,6 +40,8 @@ header, footer, main {
|
|||
header, footer, .content {
|
||||
padding: 1rem;
|
||||
}
|
||||
.content {
|
||||
width: fit-content;}
|
||||
header, footer {
|
||||
max-width: 38rem;
|
||||
text-align: center;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<LoadableText :text="size" />
|
||||
<LoadableText :text="text" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import filesize from 'filesize';
|
||||
import { tag, blob } from '@/api';
|
||||
import { tag } from '@/api';
|
||||
import LoadableText from '@/components/LoadableText.vue';
|
||||
|
||||
export default {
|
||||
|
@ -14,10 +14,11 @@ export default {
|
|||
props: {
|
||||
repo: String,
|
||||
tag: String,
|
||||
size: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
size: '',
|
||||
text: '',
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
|
@ -27,13 +28,11 @@ export default {
|
|||
return;
|
||||
}
|
||||
if (r.schemaVersion === 1) {
|
||||
r.layers = r.fsLayers.map(l => ({ digest: l.blobSum }));
|
||||
console.error('V1 manifests not supported');
|
||||
return;
|
||||
}
|
||||
const sizes = await Promise.all(r.layers.map(async layer => (
|
||||
await blob(this.repo, layer.digest)
|
||||
).contentLength));
|
||||
const total = sizes.reduce((a, b) => a + b, 0);
|
||||
this.size = filesize(total);
|
||||
const total = r.layers.reduce((a, b) => a + b.size, 0);
|
||||
this.text = filesize(total);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,13 @@
|
|||
<template>
|
||||
<span>No Toolbar</span>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<button :class="{ danger }" @click="$emit('click', $event)">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
danger: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: 0.5rem;
|
||||
background: #66f;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: #fff;
|
||||
tap-highlight-color: transparent;
|
||||
}
|
||||
button:hover {
|
||||
background: #44f;
|
||||
}
|
||||
button:active {
|
||||
background: #00f;
|
||||
}
|
||||
button:focus {
|
||||
box-shadow: 0 0 0 0.25rem #ccf;
|
||||
}
|
||||
button.danger {
|
||||
background: #f66;
|
||||
}
|
||||
button.danger:hover {
|
||||
background: #f44;
|
||||
}
|
||||
button.danger:active {
|
||||
background: #f00;
|
||||
}
|
||||
button.danger:focus {
|
||||
box-shadow: 0 0 0 0.25rem #fcc;
|
||||
}
|
||||
</style>
|
|
@ -7,10 +7,11 @@ const defaultConfig = {
|
|||
registryHost: process.env.VUE_APP_REGISTRY_HOST,
|
||||
registryAPI: process.env.VUE_APP_REGISTRY_API,
|
||||
|
||||
repositoriesPerPage: process.env.VUE_APP_REPOSITORIES_PER_PAGE,
|
||||
tagsPerPage: process.env.VUE_APP_TAGS_PER_PAGE,
|
||||
deleteEnabled: process.env.VUE_APP_DELETE_ENABLED === 'true',
|
||||
repositoriesPerPage: parseInt(process.env.VUE_APP_REPOSITORIES_PER_PAGE, 10),
|
||||
tagsPerPage: parseInt(process.env.VUE_APP_TAGS_PER_PAGE, 10),
|
||||
|
||||
usePortusExplore: process.env.VUE_APP_USE_PORTUS_EXPLORE,
|
||||
usePortusExplore: process.env.VUE_APP_USE_PORTUS_EXPLORE === 'true',
|
||||
};
|
||||
|
||||
async function config() {
|
||||
|
@ -49,9 +50,9 @@ async function registryAPI() {
|
|||
return `${window.location.protocol}//${host}`;
|
||||
}
|
||||
|
||||
async function usePortusExplore() {
|
||||
async function deleteEnabled() {
|
||||
const c = await config();
|
||||
if (c.usePortusExplore) {
|
||||
if (c.deleteEnabled) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -79,11 +80,20 @@ async function tagsPerPage() {
|
|||
return 0;
|
||||
}
|
||||
|
||||
async function usePortusExplore() {
|
||||
const c = await config();
|
||||
if (c.usePortusExplore) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export {
|
||||
version,
|
||||
source,
|
||||
registryHost,
|
||||
registryAPI,
|
||||
deleteEnabled,
|
||||
repositoriesPerPage,
|
||||
tagsPerPage,
|
||||
usePortusExplore,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<Layout>
|
||||
<h1 slot="title">{{ $route.params.repo }}</h1>
|
||||
<Error slot="error" :message='error' />
|
||||
<h2>Details</h2>
|
||||
<List>
|
||||
<ListItem>
|
||||
|
@ -16,7 +17,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { blob } from '@/api';
|
||||
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import Error from '@/components/Error.vue';
|
||||
import List from '@/components/List.vue';
|
||||
import ListItem from '@/components/ListItem.vue';
|
||||
import BlobSize from '@/components/BlobSize.vue';
|
||||
|
@ -24,9 +28,37 @@ import BlobSize from '@/components/BlobSize.vue';
|
|||
export default {
|
||||
components: {
|
||||
Layout,
|
||||
Error,
|
||||
List,
|
||||
ListItem,
|
||||
BlobSize,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: '',
|
||||
digest: '',
|
||||
};
|
||||
},
|
||||
async created() {
|
||||
await this.fetchBlob();
|
||||
},
|
||||
methods: {
|
||||
async fetchBlob() {
|
||||
try {
|
||||
const r = await blob(this.$route.params.repo, this.$route.params.digest);
|
||||
if (r.dockerContentDigest) {
|
||||
this.digest = r.dockerContentDigest;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = `Unable to fetch blob (${e.message})`;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async $route() {
|
||||
await this.fetchBlob();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
<Layout>
|
||||
<h1 slot="title">{{ $route.params.repo }}</h1>
|
||||
<Error slot="error" :message='error' />
|
||||
<Toolbar slot="toolbar">
|
||||
<ToolbarButton v-if="repoCanDelete" @click="deleteRepo" danger>Delete</ToolbarButton>
|
||||
</Toolbar>
|
||||
<List>
|
||||
<ListHeader slot="header">
|
||||
<span slot="title">Tag</span>
|
||||
|
@ -27,11 +30,12 @@
|
|||
|
||||
<script>
|
||||
import { registryHost } from '@/options';
|
||||
import { tags } from '@/api';
|
||||
import { tags, repoCanDelete, repoDelete } from '@/api';
|
||||
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import Error from '@/components/Error.vue';
|
||||
import Toolbar from '@/components/Toolbar.vue';
|
||||
import ToolbarButton from '@/components/ToolbarButton.vue';
|
||||
import List from '@/components/List.vue';
|
||||
import ListHeader from '@/components/ListHeader.vue';
|
||||
import ListItem from '@/components/ListItem.vue';
|
||||
|
@ -43,6 +47,7 @@ export default {
|
|||
Layout,
|
||||
Error,
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
List,
|
||||
ListHeader,
|
||||
ListItem,
|
||||
|
@ -54,6 +59,7 @@ export default {
|
|||
error: '',
|
||||
registryHost: '',
|
||||
tags: [],
|
||||
repoCanDelete: false,
|
||||
nextLast: '',
|
||||
};
|
||||
},
|
||||
|
@ -62,14 +68,24 @@ export default {
|
|||
await this.fetchTags();
|
||||
},
|
||||
methods: {
|
||||
async deleteRepo() {
|
||||
try {
|
||||
await repoDelete(this.$route.params.repo);
|
||||
this.$router.push({ name: 'repos' });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = `Unable to delete repo (${e.message})`;
|
||||
}
|
||||
},
|
||||
async fetchTags() {
|
||||
try {
|
||||
const r = await tags(this.$route.params.repo, this.$route.query.last);
|
||||
this.tags = r.tags;
|
||||
this.nextLast = r.nextLast;
|
||||
this.repoCanDelete = await repoCanDelete(this.$route.params.repo);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = `Unable to fetch tags (${e.name})`;
|
||||
this.error = `Unable to fetch tags (${e.message})`;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -65,7 +65,7 @@ export default {
|
|||
this.nextLast = r.nextLast;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = `Unable to fetch repositories (${e.name})`;
|
||||
this.error = `Unable to fetch repositories (${e.message})`;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
<Layout>
|
||||
<h1 slot="title">{{ $route.params.repo }}:{{ $route.params.tag }}</h1>
|
||||
<Error slot="error" :message='error' />
|
||||
<Toolbar slot="toolbar">
|
||||
<ToolbarButton v-if="tagCanDelete" @click="deleteTag" danger>Delete</ToolbarButton> </Toolbar>
|
||||
<h2>Details</h2>
|
||||
<List>
|
||||
<ListItem>
|
||||
|
@ -9,8 +11,32 @@
|
|||
<span slot="detail">{{ tag.schemaVersion }}</span>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<span slot="title">Architecture</span>
|
||||
<span slot="detail">{{ tag.architecture }}</span>
|
||||
<span slot="title">Full Digest</span>
|
||||
<span slot="detail">{{ tag.headers.get('Docker-Content-Digest') }}</span>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<span slot="title">Date Created</span>
|
||||
<span slot="detail">{{ config.created }}</span>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<span slot="title">Platform</span>
|
||||
<span slot="detail">{{ config.os }} {{ config.architecture }}</span>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<span slot="title">Entrypoint</span>
|
||||
<span slot="detail"><pre>{{ config.config.Entrypoint.join(' ') }}</pre></span>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<span slot="title">Command</span>
|
||||
<span slot="detail"><pre>{{ config.config.Cmd.join(' ') }}</pre></span>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<span slot="title">Labels</span>
|
||||
<span slot="detail"><pre>{{ formatLabels(config.config.Labels) }}</pre></span>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<span slot="title">Layers</span>
|
||||
<span slot="detail">{{ tag.layers.length }}</span>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<span slot="title">Size</span>
|
||||
|
@ -32,7 +58,7 @@
|
|||
:to="{ name: 'blob', params: { repo: $route.params.repo, digest: layer.digest }}">
|
||||
<span slot="title" :title="layer.digest">{{ identifier(tag, i) }}</span>
|
||||
<span slot="detail">{{ command(tag, i) }}</span>
|
||||
<BlobSize slot="size" :repo="$route.params.repo" :blob="layer.digest" />
|
||||
<BlobSize slot="size" :size="layer.size" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Layout>
|
||||
|
@ -40,11 +66,12 @@
|
|||
|
||||
<script>
|
||||
import { registryHost } from '@/options';
|
||||
import { tag } from '@/api';
|
||||
import { tag, tagCanDelete, tagDelete, configBlob } from '@/api';
|
||||
|
||||
import Layout from '@/components/Layout.vue';
|
||||
import Error from '@/components/Error.vue';
|
||||
import Toolbar from '@/components/Toolbar.vue';
|
||||
import ToolbarButton from '@/components/ToolbarButton.vue';
|
||||
import List from '@/components/List.vue';
|
||||
import ListHeader from '@/components/ListHeader.vue';
|
||||
import ListItem from '@/components/ListItem.vue';
|
||||
|
@ -56,6 +83,7 @@ export default {
|
|||
Layout,
|
||||
Error,
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
List,
|
||||
ListHeader,
|
||||
ListItem,
|
||||
|
@ -67,6 +95,8 @@ export default {
|
|||
error: '',
|
||||
registryHost: '',
|
||||
tag: { },
|
||||
config: {},
|
||||
tagCanDelete: false,
|
||||
layers: [],
|
||||
};
|
||||
},
|
||||
|
@ -75,20 +105,36 @@ export default {
|
|||
await this.fetchTag();
|
||||
},
|
||||
methods: {
|
||||
async deleteTag() {
|
||||
try {
|
||||
await tagDelete(this.$route.params.repo, this.$route.params.tag);
|
||||
this.$router.push({ name: 'repo', params: { repo: this.$route.params.repo } });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = `Unable to delete tag (${e.message})`;
|
||||
}
|
||||
},
|
||||
async fetchTag() {
|
||||
try {
|
||||
const r = await tag(this.$route.params.repo, this.$route.params.tag);
|
||||
if (r.mediaType === 'application/vnd.docker.distribution.manifest.list.v2+json') {
|
||||
this.error = 'V2 manifest lists not supported yet';
|
||||
return;
|
||||
}
|
||||
if (r.schemaVersion === 1) {
|
||||
r.layers = r.fsLayers.map(l => ({ digest: l.blobSum }));
|
||||
}
|
||||
this.tag = r;
|
||||
console.log(r);
|
||||
this.tagCanDelete = await tagCanDelete(this.$route.params.repo, this.$route.params.tag);
|
||||
|
||||
// extract configuration
|
||||
if (r.config.mediaType !== 'application/vnd.docker.container.image.v1+json') {
|
||||
this.error = 'configuration mediaType not supported';
|
||||
}
|
||||
this.config = await configBlob(this.$route.params.repo, r.config.digest);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.error = `Unable to fetch tag (${e.name})`;
|
||||
this.error = `Unable to fetch tag (${e.message})`;
|
||||
}
|
||||
},
|
||||
identifier(t, n) {
|
||||
|
@ -97,6 +143,9 @@ export default {
|
|||
command() {
|
||||
return 'not implemented';
|
||||
},
|
||||
formatLabels(l) {
|
||||
return Object.keys(l).map(k => `${k}: ${l[k]}`).join('\n');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async $route() {
|
||||
|
|
Loading…
Reference in New Issue