parse GHCR error format for errors
This commit is contained in:
@@ -26,16 +26,18 @@ const headMockNoExistingBlobs = (): object => {
|
||||
// Simulate none of the blobs existing currently
|
||||
return {
|
||||
text() {
|
||||
return 'Not Found'
|
||||
return '{"errors": [{"code": "NOT_FOUND", "message": "blob not found."}]}'
|
||||
},
|
||||
status: 404
|
||||
status: 404,
|
||||
statusText: 'Not Found'
|
||||
}
|
||||
}
|
||||
|
||||
const headMockAllExistingBlobs = (): object => {
|
||||
// Simulate all of the blobs existing currently
|
||||
return {
|
||||
status: 200
|
||||
status: 200,
|
||||
statusText: 'OK'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,15 +47,17 @@ const headMockSomeExistingBlobs = (): object => {
|
||||
// report one as existing
|
||||
if (count === 1) {
|
||||
return {
|
||||
status: 200
|
||||
status: 200,
|
||||
statusText: 'OK'
|
||||
}
|
||||
} else {
|
||||
// report all others are missing
|
||||
return {
|
||||
text() {
|
||||
return 'Not Found'
|
||||
return '{"errors": [{"code": "NOT_FOUND", "message": "blob not found."}]}'
|
||||
},
|
||||
status: 404
|
||||
status: 404,
|
||||
statusText: 'Not Found'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,9 +65,11 @@ const headMockSomeExistingBlobs = (): object => {
|
||||
const headMockFailure = (): object => {
|
||||
return {
|
||||
text() {
|
||||
return 'Failed the head request'
|
||||
// In this case we'll simulate a response which does not use the expected error format
|
||||
return '503 Service Unavailable'
|
||||
},
|
||||
status: 503
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +91,11 @@ const postMockFailure = (): object => {
|
||||
// Simulate failed initiation of uploads
|
||||
return {
|
||||
text() {
|
||||
return 'Failed the post request'
|
||||
// In this case we'll simulate a response which does not use the expected error format
|
||||
return '503 Service Unavailable'
|
||||
},
|
||||
status: 503
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +131,10 @@ const putMockFailure = (): object => {
|
||||
// Simulate fails upload of all blobs & manifest
|
||||
return {
|
||||
text() {
|
||||
return 'Failed the put request'
|
||||
return '{"errors": [{"code": "BAD_REQUEST", "message": "tag already exists."}]}'
|
||||
},
|
||||
status: 500
|
||||
status: 400,
|
||||
statusText: 'Bad Request'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,9 +143,10 @@ const putMockFailureManifestUpload = (url: string): object => {
|
||||
if (url.includes('manifest')) {
|
||||
return {
|
||||
text() {
|
||||
return 'Failed the put request'
|
||||
return '{"errors": [{"code": "BAD_REQUEST", "message": "tag already exists."}]}'
|
||||
},
|
||||
status: 500
|
||||
status: 400,
|
||||
statusText: 'Bad Request'
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -343,7 +353,9 @@ describe('publishOCIArtifact', () => {
|
||||
tarFile,
|
||||
testManifest
|
||||
)
|
||||
).rejects.toThrow(/^Unexpected response from blob check for layer/)
|
||||
).rejects.toThrow(
|
||||
/^Unexpected 503 Service Unavailable response from check blob/
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error if initiating layer upload fails', async () => {
|
||||
@@ -362,7 +374,9 @@ describe('publishOCIArtifact', () => {
|
||||
tarFile,
|
||||
testManifest
|
||||
)
|
||||
).rejects.toThrow('Unexpected response from POST upload 503')
|
||||
).rejects.toThrow(
|
||||
'Unexpected 503 Service Unavailable response from initiate layer upload. Response Body: 503 Service Unavailable.'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error if the upload endpoint does not return a location', async () => {
|
||||
@@ -406,7 +420,7 @@ describe('publishOCIArtifact', () => {
|
||||
tarFile,
|
||||
testManifest
|
||||
)
|
||||
).rejects.toThrow(/^Unexpected response from PUT upload 500/)
|
||||
).rejects.toThrow(/^Unexpected 400 Bad Request response from layer/)
|
||||
})
|
||||
|
||||
it('throws an error if a manifest upload fails', async () => {
|
||||
@@ -431,7 +445,9 @@ describe('publishOCIArtifact', () => {
|
||||
tarFile,
|
||||
testManifest
|
||||
)
|
||||
).rejects.toThrow(/^Unexpected response from PUT manifest 500/)
|
||||
).rejects.toThrow(
|
||||
'Unexpected 400 Bad Request response from manifest upload. Errors: BAD_REQUEST - tag already exists.'
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error if reading one of the files fails', async () => {
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="98" height="20" role="img" aria-label="Coverage: 97%"><title>Coverage: 97%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="98" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="35" height="20" fill="#4c1"/><rect width="98" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="795" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="250">97%</text><text x="795" y="140" transform="scale(.1)" fill="#fff" textLength="250">97%</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.06%"><title>Coverage: 97.06%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.06%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.06%</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
+38
-9
@@ -104749,8 +104749,7 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
|
||||
return;
|
||||
}
|
||||
if (checkExistsResponse.status !== 404) {
|
||||
const responseBody = await checkExistsResponse.text();
|
||||
throw new Error(`Unexpected response from blob check for layer ${layer.digest}: ${checkExistsResponse.status}. Response Body: ${responseBody}.`);
|
||||
throw new Error(await errorMessageForFailedRequest(`check blob (${layer.digest}) exists`, checkExistsResponse));
|
||||
}
|
||||
core.info(`Uploading layer ${layer.digest}.`);
|
||||
const initiateUploadResponse = await fetchWithDebug(uploadBlobEndpoint, {
|
||||
@@ -104761,9 +104760,7 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
|
||||
body: JSON.stringify(layer)
|
||||
});
|
||||
if (initiateUploadResponse.status !== 202) {
|
||||
const responseBody = await initiateUploadResponse.text();
|
||||
core.error(`Unexpected response from upload post ${uploadBlobEndpoint}: ${initiateUploadResponse.status}. Response Body: ${responseBody}.`);
|
||||
throw new Error(`Unexpected response from POST upload ${initiateUploadResponse.status}. Response Body: ${responseBody}.`);
|
||||
throw new Error(await errorMessageForFailedRequest(`initiate layer upload`, initiateUploadResponse));
|
||||
}
|
||||
const locationResponseHeader = initiateUploadResponse.headers.get('location');
|
||||
if (locationResponseHeader === undefined) {
|
||||
@@ -104790,8 +104787,7 @@ async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBl
|
||||
body: data
|
||||
});
|
||||
if (putResponse.status !== 201) {
|
||||
const responseBody = await putResponse.text();
|
||||
throw new Error(`Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}. Response Body: ${responseBody}.`);
|
||||
throw new Error(await errorMessageForFailedRequest(`layer (${layer.digest}) upload`, putResponse));
|
||||
}
|
||||
}
|
||||
// Uploads the manifest and returns the digest returned by GHCR
|
||||
@@ -104806,8 +104802,7 @@ async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
|
||||
body: manifestJSON
|
||||
});
|
||||
if (putResponse.status !== 201) {
|
||||
const responseBody = await putResponse.text();
|
||||
throw new Error(`Unexpected response from PUT manifest ${putResponse.status}. Response Body: ${responseBody}.`);
|
||||
throw new Error(await errorMessageForFailedRequest(`manifest upload`, putResponse));
|
||||
}
|
||||
const digestResponseHeader = putResponse.headers.get('docker-content-digest');
|
||||
if (digestResponseHeader === undefined || digestResponseHeader === null) {
|
||||
@@ -104815,6 +104810,40 @@ async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
|
||||
}
|
||||
return digestResponseHeader;
|
||||
}
|
||||
// Generate an error message for a failed HTTP request
|
||||
async function errorMessageForFailedRequest(requestDescription, response) {
|
||||
const bodyText = await response.text();
|
||||
// Try to parse the body as JSON and extract the expected fields returned from GHCR
|
||||
// Expected format: { "errors": [{"code": "BAD_REQUEST", "message": "Something went wrong."}] }
|
||||
// If the body does not match the expected format, just return the whole response body
|
||||
let errorString = `Response Body: ${bodyText}.`;
|
||||
try {
|
||||
const body = JSON.parse(bodyText);
|
||||
const errors = body.errors;
|
||||
if (Array.isArray(errors) &&
|
||||
errors.length > 0 &&
|
||||
errors.every(isGHCRError)) {
|
||||
const errorMessages = errors.map((error) => {
|
||||
return `${error.code} - ${error.message}`;
|
||||
});
|
||||
errorString = `Errors: ${errorMessages.join(', ')}`;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
return `Unexpected ${response.status} ${response.statusText} response from ${requestDescription}. ${errorString}`;
|
||||
}
|
||||
// Runtime checks that parsed JSON object is in the expected format
|
||||
// {"code": "BAD_REQUEST", "message": "Something went wrong."}
|
||||
function isGHCRError(obj) {
|
||||
return (typeof obj === 'object' &&
|
||||
obj !== null &&
|
||||
'code' in obj &&
|
||||
typeof obj.code === 'string' &&
|
||||
'message' in obj &&
|
||||
typeof obj.message === 'string');
|
||||
}
|
||||
const fetchWithDebug = async (url, config = {}) => {
|
||||
core.debug(`Request from ${url} with config: ${JSON.stringify(config)}`);
|
||||
try {
|
||||
|
||||
+64
-15
@@ -107,10 +107,11 @@ async function uploadLayer(
|
||||
}
|
||||
|
||||
if (checkExistsResponse.status !== 404) {
|
||||
const responseBody = await checkExistsResponse.text()
|
||||
|
||||
throw new Error(
|
||||
`Unexpected response from blob check for layer ${layer.digest}: ${checkExistsResponse.status}. Response Body: ${responseBody}.`
|
||||
await errorMessageForFailedRequest(
|
||||
`check blob (${layer.digest}) exists`,
|
||||
checkExistsResponse
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -125,13 +126,11 @@ async function uploadLayer(
|
||||
})
|
||||
|
||||
if (initiateUploadResponse.status !== 202) {
|
||||
const responseBody = await initiateUploadResponse.text()
|
||||
|
||||
core.error(
|
||||
`Unexpected response from upload post ${uploadBlobEndpoint}: ${initiateUploadResponse.status}. Response Body: ${responseBody}.`
|
||||
)
|
||||
throw new Error(
|
||||
`Unexpected response from POST upload ${initiateUploadResponse.status}. Response Body: ${responseBody}.`
|
||||
await errorMessageForFailedRequest(
|
||||
`initiate layer upload`,
|
||||
initiateUploadResponse
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -165,10 +164,11 @@ async function uploadLayer(
|
||||
})
|
||||
|
||||
if (putResponse.status !== 201) {
|
||||
const responseBody = await putResponse.text()
|
||||
|
||||
throw new Error(
|
||||
`Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}. Response Body: ${responseBody}.`
|
||||
await errorMessageForFailedRequest(
|
||||
`layer (${layer.digest}) upload`,
|
||||
putResponse
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -191,10 +191,8 @@ async function uploadManifest(
|
||||
})
|
||||
|
||||
if (putResponse.status !== 201) {
|
||||
const responseBody = await putResponse.text()
|
||||
|
||||
throw new Error(
|
||||
`Unexpected response from PUT manifest ${putResponse.status}. Response Body: ${responseBody}.`
|
||||
await errorMessageForFailedRequest(`manifest upload`, putResponse)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -208,6 +206,57 @@ async function uploadManifest(
|
||||
return digestResponseHeader
|
||||
}
|
||||
|
||||
interface ghcrError {
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
|
||||
// Generate an error message for a failed HTTP request
|
||||
async function errorMessageForFailedRequest(
|
||||
requestDescription: string,
|
||||
response: Response
|
||||
): Promise<string> {
|
||||
const bodyText = await response.text()
|
||||
|
||||
// Try to parse the body as JSON and extract the expected fields returned from GHCR
|
||||
// Expected format: { "errors": [{"code": "BAD_REQUEST", "message": "Something went wrong."}] }
|
||||
// If the body does not match the expected format, just return the whole response body
|
||||
let errorString = `Response Body: ${bodyText}.`
|
||||
|
||||
try {
|
||||
const body = JSON.parse(bodyText)
|
||||
const errors = body.errors
|
||||
|
||||
if (
|
||||
Array.isArray(errors) &&
|
||||
errors.length > 0 &&
|
||||
errors.every(isGHCRError)
|
||||
) {
|
||||
const errorMessages = errors.map((error: ghcrError) => {
|
||||
return `${error.code} - ${error.message}`
|
||||
})
|
||||
errorString = `Errors: ${errorMessages.join(', ')}`
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
return `Unexpected ${response.status} ${response.statusText} response from ${requestDescription}. ${errorString}`
|
||||
}
|
||||
|
||||
// Runtime checks that parsed JSON object is in the expected format
|
||||
// {"code": "BAD_REQUEST", "message": "Something went wrong."}
|
||||
function isGHCRError(obj: unknown): boolean {
|
||||
return (
|
||||
typeof obj === 'object' &&
|
||||
obj !== null &&
|
||||
'code' in obj &&
|
||||
typeof (obj as { code: unknown }).code === 'string' &&
|
||||
'message' in obj &&
|
||||
typeof (obj as { message: unknown }).message === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
const fetchWithDebug = async (
|
||||
url: string,
|
||||
config: RequestInit = {}
|
||||
|
||||
Reference in New Issue
Block a user