docker: fallback to quay if docker hub does not work
This commit is contained in:
105
src/docker.js
105
src/docker.js
@@ -76,6 +76,27 @@ function removePrivateFields(registryConfig) {
|
||||
return registryConfig;
|
||||
}
|
||||
|
||||
function parseImageRef(imageRef) {
|
||||
assert.strictEqual(typeof imageRef, 'string');
|
||||
|
||||
// a ref is like registry.docker.com/cloudron/base:4.2.0@sha256:46da2fffb36353ef714f97ae8e962bd2c212ca091108d768ba473078319a47f4
|
||||
// registry.docker.com is registry name . cloudron is namespace . base is image name . cloudron/base is repository path
|
||||
// registry.docker.com/cloudron/base is fullRepositoryName
|
||||
const result = { fullRepositoryName: null, registry: null, tag: null, digest: null };
|
||||
result.fullRepositoryName = imageRef.split(/[:@]/)[0];
|
||||
const parts = result.fullRepositoryName.split('/');
|
||||
result.registry = parts.length === 3 ? parts[0] : null;
|
||||
let remaining = imageRef.substr(result.fullRepositoryName.length);
|
||||
if (remaining.startsWith(':')) {
|
||||
result.tag = remaining.substr(1).split('@', 1)[0];
|
||||
remaining = remaining.substr(result.tag.length + 1); // also ':'
|
||||
}
|
||||
|
||||
if (remaining.startsWith('@sha256:')) result.digest = remaining.substr(8);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function ping() {
|
||||
// do not let the request linger
|
||||
const connection = new Docker({ socketPath: DOCKER_SOCKET_PATH, timeout: 1000 });
|
||||
@@ -88,11 +109,13 @@ async function ping() {
|
||||
throw new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon');
|
||||
}
|
||||
|
||||
async function getAuthConfig(image) {
|
||||
const parsedRef = parseImageRef(image);
|
||||
async function getAuthConfig(imageRef) {
|
||||
assert.strictEqual(typeof imageRef, 'string');
|
||||
|
||||
const parsedRef = parseImageRef(imageRef);
|
||||
|
||||
// images in our cloudron namespace are always unauthenticated to not interfere with any user limits
|
||||
if (parsedRef.registry === null && image.startsWith('cloudron/')) return null;
|
||||
if (parsedRef.registry === null && parsedRef.fullRepositoryName.startsWith('cloudron/')) return null;
|
||||
|
||||
const registryConfig = await getRegistryConfig();
|
||||
if (registryConfig.provider === 'noop') return null;
|
||||
@@ -114,18 +137,18 @@ async function getAuthConfig(image) {
|
||||
return autoConfig;
|
||||
}
|
||||
|
||||
async function pullImage(dockerImage) {
|
||||
assert.strictEqual(typeof dockerImage, 'string');
|
||||
async function pullImage(imageRef) {
|
||||
assert.strictEqual(typeof imageRef, 'string');
|
||||
|
||||
const authConfig = await getAuthConfig(dockerImage);
|
||||
const authConfig = await getAuthConfig(imageRef);
|
||||
|
||||
debug(`pullImage: will pull ${dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`);
|
||||
debug(`pullImage: will pull ${imageRef}. auth: ${authConfig ? 'yes' : 'no'}`);
|
||||
|
||||
const [error, stream] = await safe(gConnection.pull(dockerImage, { authconfig: authConfig }));
|
||||
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Unable to pull image ${dockerImage}. message: ${error.message} statusCode: ${error.statusCode}`);
|
||||
const [error, stream] = await safe(gConnection.pull(imageRef, { authconfig: authConfig }));
|
||||
if (error && error.statusCode === 404) throw new BoxError(BoxError.NOT_FOUND, `Unable to pull image ${imageRef}. message: ${error.message} statusCode: ${error.statusCode}`);
|
||||
// toomanyrequests is flagged as a 500. dockerhub appears to have 10 pulls her hour per IP limit
|
||||
if (error && error.statusCode === 500) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${dockerImage}. registry error: ${JSON.stringify(error)}`);
|
||||
if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${dockerImage}. Please check the network or if the image needs authentication. statusCode: ${error.statusCode}`);
|
||||
if (error && error.statusCode === 500) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${imageRef}. registry error: ${JSON.stringify(error)}`);
|
||||
if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${imageRef}. Please check the network or if the image needs authentication. statusCode: ${error.statusCode}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// https://github.com/dotcloud/docker/issues/1074 says each status message is emitted as a chunk
|
||||
@@ -136,13 +159,13 @@ async function pullImage(dockerImage) {
|
||||
|
||||
// The data.status here is useless because this is per layer as opposed to per image
|
||||
if (!data.status && data.error) { // data is { errorDetail: { message: xx } , error: xx }
|
||||
debug(`pullImage error ${dockerImage}: ${data.errorDetail.message}`);
|
||||
debug(`pullImage error ${imageRef}: ${data.errorDetail.message}`);
|
||||
layerError = data.errorDetail;
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
debug(`downloaded image ${dockerImage} . error: ${!!layerError}`);
|
||||
debug(`downloaded image ${imageRef} . error: ${!!layerError}`);
|
||||
|
||||
if (!layerError) return resolve();
|
||||
|
||||
@@ -150,7 +173,7 @@ async function pullImage(dockerImage) {
|
||||
});
|
||||
|
||||
stream.on('error', function (error) { // this is only hit for stream error and not for some download error
|
||||
debug(`error pulling image ${dockerImage}: %o`, error);
|
||||
debug(`error pulling image ${imageRef}: %o`, error);
|
||||
reject(new BoxError(BoxError.DOCKER_ERROR, error.message));
|
||||
});
|
||||
});
|
||||
@@ -159,15 +182,31 @@ async function pullImage(dockerImage) {
|
||||
async function downloadImage(manifest) {
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
|
||||
debug(`downloadImage ${manifest.dockerImage}`);
|
||||
debug(`downloadImage: ${manifest.dockerImage}`);
|
||||
|
||||
const image = gConnection.getImage(manifest.dockerImage);
|
||||
|
||||
const [error, result] = await safe(image.inspect());
|
||||
if (!error && result) return; // image is already present locally
|
||||
|
||||
await promiseRetry({ times: 10, interval: 5000, debug, retry: (pullError) => pullError.reason !== BoxError.NOT_FOUND && pullError.reason !== BoxError.FS_ERROR }, async () => {
|
||||
await pullImage(manifest.dockerImage);
|
||||
const parsedManifestRef = parseImageRef(manifest.dockerImage);
|
||||
|
||||
await promiseRetry({ times: 10, interval: 5000, debug, retry: (pullError) => pullError.reason !== BoxError.FS_ERROR }, async () => {
|
||||
if (parsedManifestRef.registry !== null || !parsedManifestRef.fullRepositoryName.startsWith('cloudron/')) return await pullImage(parsedManifestRef);
|
||||
|
||||
let upstreamRef = `registry.docker.com/${manifest.dockerImage}`;
|
||||
const [pullError] = await safe(pullImage(upstreamRef));
|
||||
if (pullError) {
|
||||
debug(`downloadImage: failed to download image from dockerhub, trying quay.io`);
|
||||
upstreamRef = `quay.io/${manifest.dockerImage}`;
|
||||
await pullImage(upstreamRef);
|
||||
}
|
||||
|
||||
// retag the downloaded image to not have the registry name. this prevents 'docker run' from redownloading it
|
||||
debug(`downloadImage: tagging ${upstreamRef} as ${parsedManifestRef.fullRepositoryName}:${parsedManifestRef.tag}`);
|
||||
await gConnection.getImage(upstreamRef).tag({ repo: parsedManifestRef.fullRepositoryName, tag: parsedManifestRef.tag });
|
||||
debug(`downloadImage: untagging ${upstreamRef}`);
|
||||
await deleteImage(upstreamRef);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -532,12 +571,11 @@ async function stopContainers(appId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteImage(manifest) {
|
||||
assert(!manifest || typeof manifest === 'object');
|
||||
async function deleteImage(imageRef) {
|
||||
assert.strictEqual(typeof imageRef, 'string');
|
||||
|
||||
const dockerImage = manifest ? manifest.dockerImage : null;
|
||||
if (!dockerImage) return;
|
||||
if (dockerImage.includes('//') || dockerImage.startsWith('/')) return; // a common mistake is to paste a https:// as docker image. this results in a crash at runtime in dockerode module (https://github.com/apocas/dockerode/issues/548)
|
||||
if (!imageRef) return;
|
||||
if (imageRef.includes('//') || imageRef.startsWith('/')) return; // a common mistake is to paste a https:// as docker image. this results in a crash at runtime in dockerode module (https://github.com/apocas/dockerode/issues/548)
|
||||
|
||||
const removeOptions = {
|
||||
force: false, // might be shared with another instance of this app
|
||||
@@ -547,13 +585,13 @@ async function deleteImage(manifest) {
|
||||
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
|
||||
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
|
||||
// not created anymore after https://github.com/docker/docker/pull/10571
|
||||
const [error] = await safe(gConnection.getImage(dockerImage).remove(removeOptions));
|
||||
const [error] = await safe(gConnection.getImage(imageRef).remove(removeOptions));
|
||||
if (error && error.statusCode === 400) return; // invalid image format. this can happen if user installed with a bad --docker-image
|
||||
if (error && error.statusCode === 404) return; // not found
|
||||
if (error && error.statusCode === 409) return; // another container using the image
|
||||
|
||||
if (error) {
|
||||
debug('Error removing image %s : %o', dockerImage, error);
|
||||
debug(`Error removing image ${imageRef} : %o`, error);
|
||||
throw new BoxError(BoxError.DOCKER_ERROR, error);
|
||||
}
|
||||
}
|
||||
@@ -691,22 +729,3 @@ async function setRegistryConfig(registryConfig) {
|
||||
|
||||
await settings.setJson(settings.REGISTRY_CONFIG_KEY, registryConfig);
|
||||
}
|
||||
|
||||
function parseImageRef(ref) {
|
||||
// a ref is like registry.docker.com/cloudron/base:4.2.0@sha256:46da2fffb36353ef714f97ae8e962bd2c212ca091108d768ba473078319a47f4
|
||||
// registry.docker.com is registry name . cloudron is namespace . base is image name . cloudron/base is repository path
|
||||
// registry.docker.com/cloudron/base is fullRepositoryName
|
||||
const result = { fullRepositoryName: null, registry: null, tag: null, digest: null };
|
||||
result.fullRepositoryName = ref.split(/[:@]/)[0];
|
||||
const parts = result.fullRepositoryName.split('/');
|
||||
result.registry = parts.length === 3 ? parts[0] : null;
|
||||
let remaining = ref.substr(result.fullRepositoryName.length);
|
||||
if (remaining.startsWith(':')) {
|
||||
result.tag = remaining.substr(1).split('@', 1)[0];
|
||||
remaining = remaining.substr(result.tag.length + 1); // also ':'
|
||||
}
|
||||
|
||||
if (remaining.startsWith('@sha256:')) result.digest = remaining.substr(8);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user