Fix flatten push auth by retargeting short names to vault registry
Release rolling / release (push) Has been cancelled
Release rolling / release (push) Has been cancelled
Private registry credentials were applied while pushing Docker Hub short names. Auto-retag under the credential host and prefix the flattener repo.
This commit is contained in:
+13
-2
@@ -348,12 +348,23 @@ function classifyDockerMessage(message, method) {
|
|||||||
severity: 'danger',
|
severity: 'danger',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (/pull access denied|unauthorized|authentication required/i.test(lower)) {
|
if (/Credential is for registry|REGISTRY_HOST_MISMATCH/i.test(m)) {
|
||||||
|
return {
|
||||||
|
code: 'REGISTRY_HOST_MISMATCH',
|
||||||
|
title: 'Image registry does not match credential',
|
||||||
|
message: m,
|
||||||
|
recovery:
|
||||||
|
'Include the registry host in the repository (e.g. 192.168.0.12:5555/myapp) or select a credential for that host. Short names push to Docker Hub.',
|
||||||
|
severity: 'warning',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (/pull access denied|unauthorized|authentication required|incorrect username or password/i.test(lower)) {
|
||||||
return {
|
return {
|
||||||
code: 'DOCKER_ERROR',
|
code: 'DOCKER_ERROR',
|
||||||
title: 'Registry authentication required',
|
title: 'Registry authentication required',
|
||||||
message: m,
|
message: m,
|
||||||
recovery: 'Log in to the registry on the host or store credentials in Vault, then retry.',
|
recovery:
|
||||||
|
'Confirm the vault credential username/password and server address. For private registries, the image must be tagged with that host (e.g. host:port/name:tag), not a short Docker Hub name.',
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -598,6 +598,9 @@ export async function flattenContainerJob(args) {
|
|||||||
})
|
})
|
||||||
setJobProgress(job.id, tracker.snapshot(), { stepId: 'push' })
|
setJobProgress(job.id, tracker.snapshot(), { stepId: 'push' })
|
||||||
log(`Pushing ${ref}…`)
|
log(`Pushing ${ref}…`)
|
||||||
|
if (args.credentialId) {
|
||||||
|
log(`Using vault credential ${args.credentialId} (auto-retags short names onto that registry)`)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const body = {
|
const body = {
|
||||||
image: ref,
|
image: ref,
|
||||||
@@ -611,7 +614,10 @@ export async function flattenContainerJob(args) {
|
|||||||
setJobProgress(final.jobId, final.snapshot, { stepId: 'push' })
|
setJobProgress(final.jobId, final.snapshot, { stepId: 'push' })
|
||||||
if (final.milestoneLine) appendJobLog(final.jobId, final.milestoneLine)
|
if (final.milestoneLine) appendJobLog(final.jobId, final.milestoneLine)
|
||||||
}
|
}
|
||||||
log(res?.message || `Pushed ${ref}`)
|
if (res?.retargeted && res?.image && res.image !== ref) {
|
||||||
|
log(`Retagged for registry: ${ref} → ${res.image}`)
|
||||||
|
}
|
||||||
|
log(res?.message || `Pushed ${res?.image || ref}`)
|
||||||
return res
|
return res
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
finalizePullProgress(trackKey, {
|
finalizePullProgress(trackKey, {
|
||||||
|
|||||||
+2
-1
@@ -5600,7 +5600,8 @@ services:
|
|||||||
<select id="flatten-credential" class="form-select bg-dark text-white registry-cred-select">
|
<select id="flatten-credential" class="form-select bg-dark text-white registry-cred-select">
|
||||||
<option value="">No credential (public / session auto)</option>
|
<option value="">No credential (public / session auto)</option>
|
||||||
</select>
|
</select>
|
||||||
<small class="text-muted">Uses vault credentials from <strong>Registry</strong>. Required for private registries.</small>
|
<small class="text-muted d-block">Uses vault credentials from <strong>Registry</strong>. Selecting one prefixes the repository with that registry host so the push does not go to Docker Hub.</small>
|
||||||
|
<div id="flatten-push-hint" class="flatten-push-hint small mt-2" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flatten-section-label d-flex align-items-center justify-content-between mt-1">
|
<div class="flatten-section-label d-flex align-items-center justify-content-between mt-1">
|
||||||
|
|||||||
+141
-2
@@ -14,6 +14,7 @@ import {
|
|||||||
loadAuthStatus,
|
loadAuthStatus,
|
||||||
fillCredentialSelect,
|
fillCredentialSelect,
|
||||||
credentialIdFromSelect,
|
credentialIdFromSelect,
|
||||||
|
getCachedCredentials,
|
||||||
} from './registryManager.js'
|
} from './registryManager.js'
|
||||||
|
|
||||||
/** @type {{ container: object|null, inspect: object|null }} */
|
/** @type {{ container: object|null, inspect: object|null }} */
|
||||||
@@ -22,6 +23,62 @@ const state = {
|
|||||||
inspect: null,
|
inspect: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host[:port] from vault serveraddress for image tags (empty = Docker Hub).
|
||||||
|
* @param {string} [serveraddress]
|
||||||
|
*/
|
||||||
|
function registryPrefixFromServeraddress(serveraddress) {
|
||||||
|
let raw = String(serveraddress || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/^https?:\/\//, '')
|
||||||
|
.replace(/\/+$/, '')
|
||||||
|
raw = raw.replace(/\/v2\/?$/i, '').replace(/\/v1\/?$/i, '')
|
||||||
|
const hostPort = raw.split('/')[0] || ''
|
||||||
|
if (
|
||||||
|
!hostPort ||
|
||||||
|
hostPort === 'docker.io' ||
|
||||||
|
hostPort === 'index.docker.io' ||
|
||||||
|
hostPort === 'registry-1.docker.io' ||
|
||||||
|
hostPort === 'registry.hub.docker.com' ||
|
||||||
|
hostPort.includes('docker.io')
|
||||||
|
) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return hostPort
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when first path segment looks like a registry host (has . or : or localhost).
|
||||||
|
* @param {string} repo
|
||||||
|
*/
|
||||||
|
function repoHasRegistryHost(repo) {
|
||||||
|
const first = String(repo || '').split('/')[0] || ''
|
||||||
|
if (!first) return false
|
||||||
|
if (first === 'localhost') return true
|
||||||
|
return first.includes('.') || first.includes(':')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure repo is under the credential's registry when pushing.
|
||||||
|
* @param {string} repo
|
||||||
|
* @param {string} [serveraddress]
|
||||||
|
*/
|
||||||
|
function ensureRepoUnderRegistry(repo, serveraddress) {
|
||||||
|
const prefix = registryPrefixFromServeraddress(serveraddress)
|
||||||
|
if (!prefix) return repo
|
||||||
|
let name = String(repo || '').trim()
|
||||||
|
if (!name) return `${prefix}/image`
|
||||||
|
if (name.startsWith(`${prefix}/`)) return name
|
||||||
|
// Replace a different registry host with the credential's host
|
||||||
|
if (repoHasRegistryHost(name)) {
|
||||||
|
const parts = name.split('/')
|
||||||
|
parts[0] = prefix
|
||||||
|
return parts.join('/')
|
||||||
|
}
|
||||||
|
return `${prefix}/${name}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} s
|
* @param {string} s
|
||||||
*/
|
*/
|
||||||
@@ -103,12 +160,76 @@ function updateRefPreview() {
|
|||||||
}
|
}
|
||||||
el.textContent = `${repo}:${tag || 'latest'}`
|
el.textContent = `${repo}:${tag || 'latest'}`
|
||||||
el.classList.remove('text-muted')
|
el.classList.remove('text-muted')
|
||||||
|
updatePushHint()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When push is on + vault credential selected, prefix repo with that registry.
|
||||||
|
* @param {{ force?: boolean }} [opts]
|
||||||
|
*/
|
||||||
|
function applyCredentialRegistryPrefix(opts = {}) {
|
||||||
|
const pushOn = document.getElementById('flatten-push')?.checked
|
||||||
|
if (!pushOn && !opts.force) return
|
||||||
|
const credVal = document.getElementById('flatten-credential')?.value
|
||||||
|
const credId = credentialIdFromSelect(credVal)
|
||||||
|
if (!credId) return
|
||||||
|
const cred = getCachedCredentials().find((c) => c.id === credId)
|
||||||
|
if (!cred?.serveraddress) return
|
||||||
|
const repoEl = document.getElementById('flatten-repo')
|
||||||
|
if (!repoEl) return
|
||||||
|
const next = ensureRepoUnderRegistry(repoEl.value.trim(), cred.serveraddress)
|
||||||
|
if (next && next !== repoEl.value.trim()) {
|
||||||
|
repoEl.value = next
|
||||||
|
updateRefPreview()
|
||||||
|
}
|
||||||
|
updatePushHint()
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePushHint() {
|
||||||
|
const hint = document.getElementById('flatten-push-hint')
|
||||||
|
if (!hint) return
|
||||||
|
const pushOn = document.getElementById('flatten-push')?.checked
|
||||||
|
if (!pushOn) {
|
||||||
|
hint.textContent = ''
|
||||||
|
hint.hidden = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const credVal = document.getElementById('flatten-credential')?.value
|
||||||
|
const credId = credentialIdFromSelect(credVal)
|
||||||
|
const repo = document.getElementById('flatten-repo')?.value?.trim() || ''
|
||||||
|
const tag = document.getElementById('flatten-tag')?.value?.trim() || 'latest'
|
||||||
|
if (!credId) {
|
||||||
|
hint.hidden = false
|
||||||
|
hint.innerHTML =
|
||||||
|
'<i class="fas fa-info-circle me-1"></i>No vault credential — push uses session auth or anonymous. ' +
|
||||||
|
'Short names go to <strong>Docker Hub</strong>; include <code class="flatten-code">host:port/name</code> for a private registry.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const cred = getCachedCredentials().find((c) => c.id === credId)
|
||||||
|
const prefix = registryPrefixFromServeraddress(cred?.serveraddress)
|
||||||
|
const ref = repo ? `${repo}:${tag}` : '…'
|
||||||
|
if (prefix && !repoHasRegistryHost(repo)) {
|
||||||
|
hint.hidden = false
|
||||||
|
hint.innerHTML =
|
||||||
|
`<i class="fas fa-exclamation-triangle me-1 text-warning"></i>` +
|
||||||
|
`Credential is for <strong>${escapeHtml(prefix)}</strong> but the image is a short name. ` +
|
||||||
|
`It will be retagged to <code class="flatten-code">${escapeHtml(prefix)}/${escapeHtml(repo || '…')}:${escapeHtml(tag)}</code> on push.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hint.hidden = false
|
||||||
|
hint.innerHTML =
|
||||||
|
`<i class="fas fa-cloud-upload-alt me-1"></i>` +
|
||||||
|
`Will push <code class="flatten-code">${escapeHtml(ref)}</code>` +
|
||||||
|
(prefix ? ` to <strong>${escapeHtml(prefix)}</strong>` : ' (Docker Hub / session)') +
|
||||||
|
` as <strong>${escapeHtml(cred?.username || 'user')}</strong>.`
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePushFields() {
|
function togglePushFields() {
|
||||||
const on = document.getElementById('flatten-push')?.checked
|
const on = document.getElementById('flatten-push')?.checked
|
||||||
const wrap = document.getElementById('flatten-push-fields')
|
const wrap = document.getElementById('flatten-push-fields')
|
||||||
if (wrap) wrap.style.display = on ? '' : 'none'
|
if (wrap) wrap.style.display = on ? '' : 'none'
|
||||||
|
if (on) applyCredentialRegistryPrefix()
|
||||||
|
updatePushHint()
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAdvanced() {
|
function toggleAdvanced() {
|
||||||
@@ -267,7 +388,7 @@ function collectFlattenArgs() {
|
|||||||
const container = state.container
|
const container = state.container
|
||||||
if (!container?.Id) throw new Error('No container selected')
|
if (!container?.Id) throw new Error('No container selected')
|
||||||
|
|
||||||
const repo = document.getElementById('flatten-repo')?.value?.trim() || ''
|
let repo = document.getElementById('flatten-repo')?.value?.trim() || ''
|
||||||
const tag = document.getElementById('flatten-tag')?.value?.trim() || 'latest'
|
const tag = document.getElementById('flatten-tag')?.value?.trim() || 'latest'
|
||||||
if (!repo) throw new Error('Repository name is required')
|
if (!repo) throw new Error('Repository name is required')
|
||||||
|
|
||||||
@@ -286,7 +407,21 @@ function collectFlattenArgs() {
|
|||||||
|
|
||||||
const credVal = document.getElementById('flatten-credential')?.value
|
const credVal = document.getElementById('flatten-credential')?.value
|
||||||
const credId = credentialIdFromSelect(credVal)
|
const credId = credentialIdFromSelect(credVal)
|
||||||
if (args.push && credId) args.credentialId = credId
|
if (args.push && credId) {
|
||||||
|
args.credentialId = credId
|
||||||
|
// Tag the flat image under the credential's registry so push does not hit Docker Hub
|
||||||
|
const cred = getCachedCredentials().find((c) => c.id === credId)
|
||||||
|
if (cred?.serveraddress) {
|
||||||
|
const under = ensureRepoUnderRegistry(repo, cred.serveraddress)
|
||||||
|
if (under !== repo) {
|
||||||
|
args.repo = under
|
||||||
|
repo = under
|
||||||
|
const repoEl = document.getElementById('flatten-repo')
|
||||||
|
if (repoEl) repoEl.value = under
|
||||||
|
updateRefPreview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const advanced = document.getElementById('flatten-advanced-toggle')?.checked
|
const advanced = document.getElementById('flatten-advanced-toggle')?.checked
|
||||||
const preserve = document.getElementById('flatten-preserve-config')?.checked
|
const preserve = document.getElementById('flatten-preserve-config')?.checked
|
||||||
@@ -385,6 +520,10 @@ export function initContainerFlattener() {
|
|||||||
document.getElementById('flatten-repo')?.addEventListener('input', updateRefPreview)
|
document.getElementById('flatten-repo')?.addEventListener('input', updateRefPreview)
|
||||||
document.getElementById('flatten-tag')?.addEventListener('input', updateRefPreview)
|
document.getElementById('flatten-tag')?.addEventListener('input', updateRefPreview)
|
||||||
document.getElementById('flatten-push')?.addEventListener('change', togglePushFields)
|
document.getElementById('flatten-push')?.addEventListener('change', togglePushFields)
|
||||||
|
document.getElementById('flatten-credential')?.addEventListener('change', () => {
|
||||||
|
applyCredentialRegistryPrefix()
|
||||||
|
updatePushHint()
|
||||||
|
})
|
||||||
document.getElementById('flatten-advanced-toggle')?.addEventListener('change', toggleAdvanced)
|
document.getElementById('flatten-advanced-toggle')?.addEventListener('change', toggleAdvanced)
|
||||||
|
|
||||||
document.getElementById('flatten-preserve-config')?.addEventListener('change', (e) => {
|
document.getElementById('flatten-preserve-config')?.addEventListener('change', (e) => {
|
||||||
|
|||||||
@@ -4,7 +4,16 @@
|
|||||||
import { docker } from '../services/docker.js'
|
import { docker } from '../services/docker.js'
|
||||||
import * as validation from '../utils/validation.js'
|
import * as validation from '../utils/validation.js'
|
||||||
import { Pushes } from '../../shared/protocol.js'
|
import { Pushes } from '../../shared/protocol.js'
|
||||||
import { resolveRegistryAuth, isUsableDockerAuth } from './vault.js'
|
import {
|
||||||
|
resolveRegistryAuth,
|
||||||
|
isUsableDockerAuth,
|
||||||
|
authMatchesImage,
|
||||||
|
retargetImageRefForRegistry,
|
||||||
|
parseImageRepoTag,
|
||||||
|
registryPrefixFromServeraddress,
|
||||||
|
normalizeRegistryHost,
|
||||||
|
registryHostFromImage,
|
||||||
|
} from './vault.js'
|
||||||
import logger from '../utils/logger.js'
|
import logger from '../utils/logger.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -318,6 +327,7 @@ export function registerImageHandlers(session) {
|
|||||||
let pushRef = imageName
|
let pushRef = imageName
|
||||||
/** Explicit tag query only when set — never pass undefined (see below). */
|
/** Explicit tag query only when set — never pass undefined (see below). */
|
||||||
let pushTag = null
|
let pushTag = null
|
||||||
|
let retargeted = false
|
||||||
if (args.repo) {
|
if (args.repo) {
|
||||||
const repo = validation.sanitizeString(args.repo, 255)
|
const repo = validation.sanitizeString(args.repo, 255)
|
||||||
const tag = validation.sanitizeString(args.tag || 'latest', 128)
|
const tag = validation.sanitizeString(args.tag || 'latest', 128)
|
||||||
@@ -338,6 +348,35 @@ export function registerImageHandlers(session) {
|
|||||||
image: pushRef,
|
image: pushRef,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Short name + private registry credential → auto-retag under that registry.
|
||||||
|
// Otherwise Docker pushes to docker.io with the private username/password → 401.
|
||||||
|
if (authconfig && !authMatchesImage(authconfig, pushRef)) {
|
||||||
|
const desired = retargetImageRefForRegistry(pushRef, authconfig.serveraddress)
|
||||||
|
if (desired !== pushRef) {
|
||||||
|
const { repo, tag } = parseImageRepoTag(desired)
|
||||||
|
await docker.getImage(args.id || imageName || pushRef).tag({ repo, tag })
|
||||||
|
logger.info('pushImage: retagged short name for credential registry', {
|
||||||
|
from: pushRef,
|
||||||
|
to: desired,
|
||||||
|
serveraddress: authconfig.serveraddress,
|
||||||
|
})
|
||||||
|
pushRef = desired
|
||||||
|
pushTag = null
|
||||||
|
retargeted = true
|
||||||
|
} else {
|
||||||
|
const authHost = normalizeRegistryHost(authconfig.serveraddress)
|
||||||
|
const imageHost = registryHostFromImage(pushRef) || 'unknown'
|
||||||
|
const prefix = registryPrefixFromServeraddress(authconfig.serveraddress)
|
||||||
|
throw Object.assign(
|
||||||
|
new Error(
|
||||||
|
`Credential is for registry "${authHost}" but image "${pushRef}" targets "${imageHost}". ` +
|
||||||
|
`Retag to ${prefix ? `${prefix}/` : ''}your/image:tag before pushing, or pick a matching credential.`
|
||||||
|
),
|
||||||
|
{ code: 'REGISTRY_HOST_MISMATCH' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pushOpts = buildImagePushOpts({ tag: pushTag, authconfig })
|
const pushOpts = buildImagePushOpts({ tag: pushTag, authconfig })
|
||||||
const image = docker.getImage(pushRef)
|
const image = docker.getImage(pushRef)
|
||||||
const stream = await image.push(pushOpts)
|
const stream = await image.push(pushOpts)
|
||||||
@@ -378,6 +417,7 @@ export function registerImageHandlers(session) {
|
|||||||
message: `Image "${pushRef}" pushed`,
|
message: `Image "${pushRef}" pushed`,
|
||||||
image: pushRef,
|
image: pushRef,
|
||||||
usedAuth: Boolean(authconfig),
|
usedAuth: Boolean(authconfig),
|
||||||
|
retargeted,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,82 @@ export function authMatchesImage(auth, image) {
|
|||||||
return imageHost === authHost
|
return imageHost === authHost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Host[:port] prefix for tagging images toward a vault serveraddress.
|
||||||
|
* Empty string for Docker Hub (short names push to docker.io).
|
||||||
|
* @param {string} [serveraddress]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function registryPrefixFromServeraddress(serveraddress) {
|
||||||
|
let raw = String(serveraddress || '')
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/^https?:\/\//, '')
|
||||||
|
.replace(/\/+$/, '')
|
||||||
|
// Strip API path suffixes commonly stored on credentials
|
||||||
|
raw = raw.replace(/\/v2\/?$/i, '').replace(/\/v1\/?$/i, '')
|
||||||
|
const hostPort = raw.split('/')[0] || ''
|
||||||
|
if (
|
||||||
|
!hostPort ||
|
||||||
|
hostPort === 'docker.io' ||
|
||||||
|
hostPort === 'index.docker.io' ||
|
||||||
|
hostPort === 'registry-1.docker.io' ||
|
||||||
|
hostPort === 'registry.hub.docker.com' ||
|
||||||
|
hostPort.includes('docker.io')
|
||||||
|
) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return hostPort
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split image ref into { repo, tag }.
|
||||||
|
* @param {string} ref
|
||||||
|
* @returns {{ repo: string, tag: string }}
|
||||||
|
*/
|
||||||
|
export function parseImageRepoTag(ref) {
|
||||||
|
const s = String(ref || '').trim()
|
||||||
|
if (!s) return { repo: '', tag: 'latest' }
|
||||||
|
const at = s.indexOf('@')
|
||||||
|
const noDigest = at >= 0 ? s.slice(0, at) : s
|
||||||
|
const lastSlash = noDigest.lastIndexOf('/')
|
||||||
|
const lastColon = noDigest.lastIndexOf(':')
|
||||||
|
if (lastColon > lastSlash) {
|
||||||
|
return {
|
||||||
|
repo: noDigest.slice(0, lastColon),
|
||||||
|
tag: noDigest.slice(lastColon + 1) || 'latest',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { repo: noDigest, tag: 'latest' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If image is a short (docker.io) name and auth targets a private registry,
|
||||||
|
* return the retagged destination `registry[:port]/name:tag`. Otherwise return ref unchanged.
|
||||||
|
* @param {string} imageRef
|
||||||
|
* @param {string} [serveraddress]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function retargetImageRefForRegistry(imageRef, serveraddress) {
|
||||||
|
const ref = String(imageRef || '').trim()
|
||||||
|
if (!ref) return ref
|
||||||
|
const prefix = registryPrefixFromServeraddress(serveraddress)
|
||||||
|
if (!prefix) return ref
|
||||||
|
|
||||||
|
const imageHost = registryHostFromImage(ref)
|
||||||
|
// Already points at a non-Hub registry — leave as-is (caller may error on mismatch)
|
||||||
|
if (imageHost && imageHost !== 'docker.io') return ref
|
||||||
|
|
||||||
|
const { repo, tag } = parseImageRepoTag(ref)
|
||||||
|
let name = repo
|
||||||
|
if (name.startsWith('docker.io/')) name = name.slice('docker.io/'.length)
|
||||||
|
if (name.startsWith('library/')) name = name.slice('library/'.length)
|
||||||
|
if (!name) return ref
|
||||||
|
// Avoid double-prefix if user already typed the host without it looking like a registry
|
||||||
|
if (name === prefix || name.startsWith(`${prefix}/`)) return `${name}:${tag}`
|
||||||
|
return `${prefix}/${name}:${tag}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve Docker authconfig for pull/push from vault id, inline auth, or session.
|
* Resolve Docker authconfig for pull/push from vault id, inline auth, or session.
|
||||||
* Returns null for anonymous (no usable credentials for that registry).
|
* Returns null for anonymous (no usable credentials for that registry).
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import {
|
|||||||
normalizeRegistryHost,
|
normalizeRegistryHost,
|
||||||
registryHostFromImage,
|
registryHostFromImage,
|
||||||
resolveRegistryAuth,
|
resolveRegistryAuth,
|
||||||
|
registryPrefixFromServeraddress,
|
||||||
|
retargetImageRefForRegistry,
|
||||||
|
parseImageRepoTag,
|
||||||
} from '../server/handlers/vault.js'
|
} from '../server/handlers/vault.js'
|
||||||
|
|
||||||
test('isUsableDockerAuth requires username and non-empty password', (t) => {
|
test('isUsableDockerAuth requires username and non-empty password', (t) => {
|
||||||
@@ -44,6 +47,24 @@ test('authMatchesImage only applies same-registry session auth', (t) => {
|
|||||||
t.ok(authMatchesImage({ serveraddress: 'https://ghcr.io' }, 'ghcr.io/a/b:1'))
|
t.ok(authMatchesImage({ serveraddress: 'https://ghcr.io' }, 'ghcr.io/a/b:1'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('registryPrefixFromServeraddress strips scheme and api path, keeps port', (t) => {
|
||||||
|
t.is(registryPrefixFromServeraddress('https://192.168.0.12:5555/v2/'), '192.168.0.12:5555')
|
||||||
|
t.is(registryPrefixFromServeraddress('https://ghcr.io'), 'ghcr.io')
|
||||||
|
t.is(registryPrefixFromServeraddress('https://index.docker.io/v1/'), '')
|
||||||
|
t.is(
|
||||||
|
retargetImageRefForRegistry('apache-httpd:flat-latest', 'https://192.168.0.12:5555/'),
|
||||||
|
'192.168.0.12:5555/apache-httpd:flat-latest'
|
||||||
|
)
|
||||||
|
t.is(
|
||||||
|
retargetImageRefForRegistry('ghcr.io/org/app:1', 'https://192.168.0.12:5555/'),
|
||||||
|
'ghcr.io/org/app:1'
|
||||||
|
)
|
||||||
|
t.alike(parseImageRepoTag('192.168.0.12:5555/myapp:flat'), {
|
||||||
|
repo: '192.168.0.12:5555/myapp',
|
||||||
|
tag: 'flat',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('resolveRegistryAuth returns null without session/vault (anonymous)', (t) => {
|
test('resolveRegistryAuth returns null without session/vault (anonymous)', (t) => {
|
||||||
const session = { state: new Map(), id: 'peer1' }
|
const session = { state: new Map(), id: 'peer1' }
|
||||||
const auth = resolveRegistryAuth(session, {
|
const auth = resolveRegistryAuth(session, {
|
||||||
|
|||||||
@@ -4487,6 +4487,15 @@ table.pd-col-table td.pd-col-hidden {
|
|||||||
background: var(--bg-surface, rgba(255, 255, 255, 0.02));
|
background: var(--bg-surface, rgba(255, 255, 255, 0.02));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flatten-push-hint {
|
||||||
|
padding: 0.5rem 0.65rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: rgba(56, 189, 248, 0.08);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
/* Accordion bodies must stay collapsed when not .show (defensive vs BS CDN lag) */
|
/* Accordion bodies must stay collapsed when not .show (defensive vs BS CDN lag) */
|
||||||
.accordion-collapse.collapse:not(.show) {
|
.accordion-collapse.collapse:not(.show) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user