Add Docker Registry AIO with Nuxt Token Auth and admin UI.
Compose brings up registry:3 plus a Prisma/SQLite Nuxt app for docker login, ACL management, and image browsing with shadcn-vue. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
0fd00bfa37
120 files changed
+17094
No files matched your search
@@ -0,0 +1,97 @@
|
||||
const DEFAULT_REGISTRY_URL = 'http://registry:5000'
|
||||
|
||||
export function registryBaseUrl(): string {
|
||||
return (process.env.REGISTRY_INTERNAL_URL || DEFAULT_REGISTRY_URL).replace(/\/$/, '')
|
||||
}
|
||||
|
||||
async function serviceToken(access: RegistryAccess[]): Promise<string> {
|
||||
const { token } = await signRegistryToken({
|
||||
subject: 'registry-aio-service',
|
||||
access,
|
||||
expiresInSeconds: 120,
|
||||
})
|
||||
return token
|
||||
}
|
||||
|
||||
export async function registryFetch(path: string, init: RequestInit = {}, access?: RegistryAccess[]) {
|
||||
const url = `${registryBaseUrl()}${path.startsWith('/') ? path : `/${path}`}`
|
||||
const headers = new Headers(init.headers)
|
||||
|
||||
if (!headers.has('Accept')) {
|
||||
headers.set(
|
||||
'Accept',
|
||||
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/json',
|
||||
)
|
||||
}
|
||||
|
||||
const granted =
|
||||
access ??
|
||||
([
|
||||
{ type: 'registry', name: 'catalog', actions: ['*'] },
|
||||
{ type: 'repository', name: '**', actions: ['*'] },
|
||||
] satisfies RegistryAccess[])
|
||||
|
||||
// Distribution matches exact repository names in token access — for service calls
|
||||
// we mint tokens with requested repo scopes per operation instead of '**'.
|
||||
const token = await serviceToken(granted)
|
||||
headers.set('Authorization', `Bearer ${token}`)
|
||||
|
||||
return fetch(url, { ...init, headers })
|
||||
}
|
||||
|
||||
export async function listRepositories(): Promise<string[]> {
|
||||
const res = await registryFetch(
|
||||
'/v2/_catalog?n=1000',
|
||||
{},
|
||||
[{ type: 'registry', name: 'catalog', actions: ['*'] }],
|
||||
)
|
||||
if (!res.ok) {
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
statusMessage: `Registry catalog failed: ${res.status}`,
|
||||
})
|
||||
}
|
||||
const data = (await res.json()) as { repositories?: string[] }
|
||||
return data.repositories ?? []
|
||||
}
|
||||
|
||||
export async function listTags(name: string): Promise<string[]> {
|
||||
const res = await registryFetch(
|
||||
`/v2/${encodeURI(name)}/tags/list`,
|
||||
{},
|
||||
[{ type: 'repository', name, actions: ['pull'] }],
|
||||
)
|
||||
if (res.status === 404) return []
|
||||
if (!res.ok) {
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
statusMessage: `Registry tags failed: ${res.status}`,
|
||||
})
|
||||
}
|
||||
const data = (await res.json()) as { tags?: string[] | null }
|
||||
return data.tags ?? []
|
||||
}
|
||||
|
||||
export async function getManifestDigest(name: string, reference: string): Promise<string | null> {
|
||||
const res = await registryFetch(
|
||||
`/v2/${encodeURI(name)}/manifests/${encodeURIComponent(reference)}`,
|
||||
{ method: 'HEAD' },
|
||||
[{ type: 'repository', name, actions: ['pull'] }],
|
||||
)
|
||||
if (!res.ok) return null
|
||||
return res.headers.get('docker-content-digest')
|
||||
}
|
||||
|
||||
export async function deleteManifest(name: string, digest: string): Promise<void> {
|
||||
const res = await registryFetch(
|
||||
`/v2/${encodeURI(name)}/manifests/${encodeURIComponent(digest)}`,
|
||||
{ method: 'DELETE' },
|
||||
[{ type: 'repository', name, actions: ['delete', 'pull'] }],
|
||||
)
|
||||
if (!res.ok && res.status !== 404) {
|
||||
throw createError({
|
||||
statusCode: 502,
|
||||
statusMessage: `Registry delete failed: ${res.status}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user