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:
@@ -4,7 +4,16 @@
|
||||
import { docker } from '../services/docker.js'
|
||||
import * as validation from '../utils/validation.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'
|
||||
|
||||
/**
|
||||
@@ -318,6 +327,7 @@ export function registerImageHandlers(session) {
|
||||
let pushRef = imageName
|
||||
/** Explicit tag query only when set — never pass undefined (see below). */
|
||||
let pushTag = null
|
||||
let retargeted = false
|
||||
if (args.repo) {
|
||||
const repo = validation.sanitizeString(args.repo, 255)
|
||||
const tag = validation.sanitizeString(args.tag || 'latest', 128)
|
||||
@@ -338,6 +348,35 @@ export function registerImageHandlers(session) {
|
||||
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 image = docker.getImage(pushRef)
|
||||
const stream = await image.push(pushOpts)
|
||||
@@ -378,6 +417,7 @@ export function registerImageHandlers(session) {
|
||||
message: `Image "${pushRef}" pushed`,
|
||||
image: pushRef,
|
||||
usedAuth: Boolean(authconfig),
|
||||
retargeted,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -81,6 +81,82 @@ export function authMatchesImage(auth, image) {
|
||||
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.
|
||||
* Returns null for anonymous (no usable credentials for that registry).
|
||||
|
||||
Reference in New Issue
Block a user