Skip to content

Commit 1649afe

Browse files
Deletion and more details
1 parent f9abc53 commit 1649afe

13 files changed

+302
-50
lines changed

entrypoint.sh

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ BASE_URL=${BASE_URL:=/}
55
REGISTRY_HOST=${REGISTRY_HOST:=""}
66
REGISTRY_API=${REGISTRY_API:=""}
77

8+
[[ "$DELETE_ENABLED" != "true" ]] && [[ "$DELETE_ENABLED" != "false" ]] && DELETE_ENABLED=false
89
REPOSITORIES_PER_PAGE=${REPOSITORIES_PER_PAGE:=0}
910
TAGS_PER_PAGE=${TAGS_PER_PAGE:=30}
1011

@@ -27,6 +28,7 @@ cat > /srv/config.json << EOF
2728
{
2829
"registryHost": "$REGISTRY_HOST",
2930
"registryAPI": "$REGISTRY_API",
31+
"deleteEnabled": $DELETE_ENABLED,
3032
"repositoriesPerPage": $REPOSITORIES_PER_PAGE,
3133
"tagsPerPage": $TAGS_PER_PAGE,
3234
"usePortusExplore": $USE_PORTUS_EXPLORE

registry-config-testing.yml

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
version: 0.1
2+
log:
3+
fields:
4+
service: registry
5+
storage:
6+
cache:
7+
blobdescriptor: inmemory
8+
filesystem:
9+
rootdirectory: /var/lib/registry
10+
delete:
11+
enabled: true
12+
http:
13+
addr: :5000
14+
headers:
15+
X-Content-Type-Options: [nosniff]
16+
Access-Control-Allow-Origin: ['*']
17+
Access-Control-Allow-Methods: ['GET, DELETE']
18+
Access-Control-Allow-Headers: ['Authorization']
19+
Access-Control-Expose-Headers: ['Content-Length, Www-Authenticate', 'Docker-Content-Digest']
20+
health:
21+
storagedriver:
22+
enabled: true
23+
interval: 10s
24+
threshold: 3

src/api.js

+76-22
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import parseLink from 'parse-link-header';
22

3-
import { registryAPI, repositoriesPerPage, tagsPerPage, usePortusExplore } from '@/options';
3+
import { registryAPI, deleteEnabled, repositoriesPerPage, tagsPerPage, usePortusExplore } from '@/options';
44

55
function parseWWWAuthenticate(text) {
66
const result = {};
@@ -86,29 +86,41 @@ async function paginatable(path, scope, n, last = null) {
8686
return Object.assign(await response.json(), { nextLast });
8787
}
8888

89-
async function get(path, scope) {
89+
async function request(method, path, scope, accept) {
9090
const url = new URL(`${await registryAPI()}${path}`);
9191
const headers = {};
92-
if (scope) {
93-
const token = await doAuth(scope);
94-
if (token) headers.Authorization = `Bearer ${token}`;
92+
if (accept) {
93+
headers.Accept = accept;
9594
}
96-
const response = await fetch(url, { headers });
97-
return response.json();
98-
}
99-
100-
async function head(path, scope) {
101-
const url = new URL(`${await registryAPI()}${path}`);
102-
const headers = {};
10395
if (scope) {
10496
const token = await doAuth(scope);
10597
if (token) headers.Authorization = `Bearer ${token}`;
10698
}
107-
const response = await fetch(url, { method: 'HEAD', headers });
108-
return response.headers;
99+
const response = await fetch(url, { method, headers });
100+
if (!response.ok) {
101+
if (method === 'HEAD') {
102+
throw new Error(`${response.statusText}`);
103+
} else if (response.headers.get('Content-Type').startsWith('application/json')) {
104+
const r = await response.json();
105+
const firstError = r.errors[0];
106+
if (firstError) {
107+
throw new Error(`${firstError.code}: ${firstError.message}`);
108+
}
109+
}
110+
throw new Error(`${response.statusText}`);
111+
}
112+
if (!response.headers.get('Content-Type').startsWith('application/json')) {
113+
console.warn('response returned was not JSON, parsing may fail');
114+
}
115+
if (method === 'HEAD' || parseInt(response.headers.get('Content-Length'), 10) < 1) {
116+
return { headers: response.headers };
117+
}
118+
return {
119+
...(await response.json()),
120+
headers: response.headers,
121+
};
109122
}
110123

111-
112124
async function portus() {
113125
// TODO: Use the Portus API when it enables anonymous access
114126
const response = await fetch(`${await registryAPI()}/explore?explore%5Bsearch%5D=`);
@@ -135,10 +147,6 @@ async function repos(last = null) {
135147
return paginatable('/v2/_catalog', null, await repositoriesPerPage(), last);
136148
}
137149

138-
async function repo(name) {
139-
return get(`/v2/${name}`, `repository:${name}:pull`);
140-
}
141-
142150
async function tags(name, last = null) {
143151
if (await usePortusExplore()) {
144152
const p = await portus();
@@ -151,20 +159,66 @@ async function tags(name, last = null) {
151159
}
152160

153161
async function tag(name, ref) {
154-
return get(`/v2/${name}/manifests/${ref}`, `repository:${name}:pull`);
162+
return request('GET', `/v2/${name}/manifests/${ref}`, `repository:${name}:pull`, 'application/vnd.docker.distribution.manifest.v2+json');
163+
}
164+
165+
async function tagCanDelete(name, ref) {
166+
if (!await deleteEnabled()) {
167+
return false;
168+
}
169+
try {
170+
const { headers } = await request('HEAD', `/v2/${name}/manifests/${ref}`, `repository:${name}:delete`, 'application/vnd.docker.distribution.manifest.v2+json');
171+
request('HEAD', `/v2/${name}/manifests/${headers.get('Docker-Content-Digest')}`, `repository:${name}:delete`);
172+
return true;
173+
} catch (e) {
174+
return false;
175+
}
176+
}
177+
178+
async function tagDelete(name, ref) {
179+
const tagManifest = await tag(name, ref);
180+
// delete each blob
181+
// await Promise.all(tagManifest.layers.map(l =>
182+
// request('DELETE', `/v2/${name}/blobs/${l.digest}`, `repository:${name}:delete`)));
183+
return request('DELETE', `/v2/${name}/manifests/${tagManifest.headers.get('Docker-Content-Digest')}`, `repository:${name}:delete`);
184+
}
185+
186+
async function repoCanDelete(name) {
187+
if (!await deleteEnabled()) {
188+
return false;
189+
}
190+
const r = await request('GET', `/v2/${name}/tags/list`, `repository:${name}:delete`);
191+
if (!r.tags) {
192+
return false;
193+
}
194+
return Promise.race(r.tags.map(t => tagCanDelete(name, t)));
195+
}
196+
197+
async function repoDelete(name) {
198+
const r = await request('GET', `/v2/${name}/tags/list`, `repository:${name}:delete`);
199+
return Promise.all(r.tags.map(t => tagDelete(name, t)));
155200
}
156201

157202
async function blob(name, digest) {
158-
const headers = await head(`/v2/${name}/blobs/${digest}`, `repository:${name}:pull`);
203+
const { headers } = await request('HEAD', `/v2/${name}/blobs/${digest}`, `repository:${name}:pull`);
159204
return {
205+
dockerContentDigest: headers.get('Docker-Content-Digest'),
160206
contentLength: parseInt(headers.get('Content-Length'), 10),
161207
};
162208
}
163209

210+
async function configBlob(name, digest) {
211+
return request('GET', `/v2/${name}/blobs/${digest}`, `repository:${name}:pull`);
212+
}
213+
164214
export {
165215
repos,
166-
repo,
167216
tags,
168217
tag,
218+
tagCanDelete,
219+
tagDelete,
220+
repoCanDelete,
221+
repoDelete,
169222
blob,
223+
configBlob,
170224
};

src/components/BlobSize.vue

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<LoadableText :text="size" />
2+
<LoadableText :text="text" />
33
</template>
44

55
<script>
@@ -14,15 +14,22 @@ export default {
1414
props: {
1515
repo: String,
1616
blob: String,
17+
size: Number,
1718
},
1819
data() {
1920
return {
20-
size: '',
21+
text: '',
2122
};
2223
},
2324
async created() {
25+
if (this.size) {
26+
this.text = filesize(this.size);
27+
return;
28+
}
2429
const size = await blob(this.repo, this.blob);
25-
this.size = filesize(size.contentLength);
30+
if (size.contentLength) {
31+
this.text = filesize(size.contentLength);
32+
}
2633
},
2734
};
2835
</script>

src/components/Layout.vue

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ header, footer, main {
4040
header, footer, .content {
4141
padding: 1rem;
4242
}
43+
.content {
44+
width: fit-content;}
4345
header, footer {
4446
max-width: 38rem;
4547
text-align: center;

src/components/TagSize.vue

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<template>
2-
<LoadableText :text="size" />
2+
<LoadableText :text="text" />
33
</template>
44

55
<script>
66
import filesize from 'filesize';
7-
import { tag, blob } from '@/api';
7+
import { tag } from '@/api';
88
import LoadableText from '@/components/LoadableText.vue';
99
1010
export default {
@@ -14,10 +14,11 @@ export default {
1414
props: {
1515
repo: String,
1616
tag: String,
17+
size: Number,
1718
},
1819
data() {
1920
return {
20-
size: '',
21+
text: '',
2122
};
2223
},
2324
async created() {
@@ -27,13 +28,11 @@ export default {
2728
return;
2829
}
2930
if (r.schemaVersion === 1) {
30-
r.layers = r.fsLayers.map(l => ({ digest: l.blobSum }));
31+
console.error('V1 manifests not supported');
32+
return;
3133
}
32-
const sizes = await Promise.all(r.layers.map(async layer => (
33-
await blob(this.repo, layer.digest)
34-
).contentLength));
35-
const total = sizes.reduce((a, b) => a + b, 0);
36-
this.size = filesize(total);
34+
const total = r.layers.reduce((a, b) => a + b.size, 0);
35+
this.text = filesize(total);
3736
},
3837
};
3938
</script>

src/components/Toolbar.vue

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
<template>
2-
<span>No Toolbar</span>
2+
<div>
3+
<slot />
4+
</div>
35
</template>
6+
7+
<style scoped>
8+
div {
9+
display: flex;
10+
flex-direction: row;
11+
justify-content: center;
12+
}
13+
</style>

src/components/ToolbarButton.vue

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<button :class="{ danger }" @click="$emit('click', $event)">
3+
<slot />
4+
</button>
5+
</template>
6+
7+
<script>
8+
export default {
9+
props: {
10+
danger: Boolean,
11+
},
12+
};
13+
</script>
14+
15+
<style scoped>
16+
button {
17+
appearance: none;
18+
border: none;
19+
outline: none;
20+
border-radius: 0.5rem;
21+
background: #66f;
22+
padding: 0.5rem 0.75rem;
23+
color: #fff;
24+
tap-highlight-color: transparent;
25+
}
26+
button:hover {
27+
background: #44f;
28+
}
29+
button:active {
30+
background: #00f;
31+
}
32+
button:focus {
33+
box-shadow: 0 0 0 0.25rem #ccf;
34+
}
35+
button.danger {
36+
background: #f66;
37+
}
38+
button.danger:hover {
39+
background: #f44;
40+
}
41+
button.danger:active {
42+
background: #f00;
43+
}
44+
button.danger:focus {
45+
box-shadow: 0 0 0 0.25rem #fcc;
46+
}
47+
</style>

src/options.js

+15-5
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ const defaultConfig = {
77
registryHost: process.env.VUE_APP_REGISTRY_HOST,
88
registryAPI: process.env.VUE_APP_REGISTRY_API,
99

10-
repositoriesPerPage: process.env.VUE_APP_REPOSITORIES_PER_PAGE,
11-
tagsPerPage: process.env.VUE_APP_TAGS_PER_PAGE,
10+
deleteEnabled: process.env.VUE_APP_DELETE_ENABLED === 'true',
11+
repositoriesPerPage: parseInt(process.env.VUE_APP_REPOSITORIES_PER_PAGE, 10),
12+
tagsPerPage: parseInt(process.env.VUE_APP_TAGS_PER_PAGE, 10),
1213

13-
usePortusExplore: process.env.VUE_APP_USE_PORTUS_EXPLORE,
14+
usePortusExplore: process.env.VUE_APP_USE_PORTUS_EXPLORE === 'true',
1415
};
1516

1617
async function config() {
@@ -49,9 +50,9 @@ async function registryAPI() {
4950
return `${window.location.protocol}//${host}`;
5051
}
5152

52-
async function usePortusExplore() {
53+
async function deleteEnabled() {
5354
const c = await config();
54-
if (c.usePortusExplore) {
55+
if (c.deleteEnabled) {
5556
return true;
5657
}
5758
return false;
@@ -79,11 +80,20 @@ async function tagsPerPage() {
7980
return 0;
8081
}
8182

83+
async function usePortusExplore() {
84+
const c = await config();
85+
if (c.usePortusExplore) {
86+
return true;
87+
}
88+
return false;
89+
}
90+
8291
export {
8392
version,
8493
source,
8594
registryHost,
8695
registryAPI,
96+
deleteEnabled,
8797
repositoriesPerPage,
8898
tagsPerPage,
8999
usePortusExplore,

0 commit comments

Comments
 (0)