Skip to content

Commit 4b08e20

Browse files
authored
fix(cdk-assets): Error when building Docker Image Assets with Podman (#24003)
Encountering the same error as #16209 (comment). When using `podman inspect` to check whether or not an image exists, the exit code for when an image does not exist is `125`, while Docker's is `1`. This change will treat either of these exit codes as meaning that the image with the given tag does not currently exist. Closes #16209 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 643042b commit 4b08e20

File tree

3 files changed

+127
-3
lines changed

3 files changed

+127
-3
lines changed

packages/cdk-assets/lib/private/docker.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as fs from 'fs';
22
import * as os from 'os';
33
import * as path from 'path';
44
import { cdkCredentialsConfig, obtainEcrCredentials } from './docker-credentials';
5-
import { Logger, shell, ShellOptions } from './shell';
5+
import { Logger, shell, ShellOptions, ProcessFailedError } from './shell';
66
import { createCriticalSection } from './util';
77

88
interface BuildOptions {
@@ -31,6 +31,11 @@ export interface DockerDomainCredentials {
3131
readonly ecrRepository?: string;
3232
}
3333

34+
enum InspectImageErrorCode {
35+
Docker = 1,
36+
Podman = 125
37+
}
38+
3439
export class Docker {
3540

3641
private configDir: string | undefined = undefined;
@@ -46,8 +51,31 @@ export class Docker {
4651
await this.execute(['inspect', tag], { quiet: true });
4752
return true;
4853
} catch (e) {
49-
if (e.code !== 'PROCESS_FAILED' || e.exitCode !== 1) { throw e; }
50-
return false;
54+
const error: ProcessFailedError = e;
55+
56+
/**
57+
* The only error we expect to be thrown will have this property and value.
58+
* If it doesn't, it's unrecognized so re-throw it.
59+
*/
60+
if (error.code !== 'PROCESS_FAILED') {
61+
throw error;
62+
}
63+
64+
/**
65+
* If we know the shell command above returned an error, check to see
66+
* if the exit code is one we know to actually mean that the image doesn't
67+
* exist.
68+
*/
69+
switch (error.exitCode) {
70+
case InspectImageErrorCode.Docker:
71+
case InspectImageErrorCode.Podman:
72+
// Docker and Podman will return this exit code when an image doesn't exist, return false
73+
// context: https://github.com/aws/aws-cdk/issues/16209
74+
return false;
75+
default:
76+
// This is an error but it's not an exit code we recognize, throw.
77+
throw error;
78+
}
5179
}
5280
}
5381

packages/cdk-assets/lib/private/shell.ts

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export async function shell(command: string[], options: ShellOptions = {}): Prom
6161
});
6262
}
6363

64+
export type ProcessFailedError = ProcessFailed
65+
6466
class ProcessFailed extends Error {
6567
public readonly code = 'PROCESS_FAILED';
6668

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { Docker } from '../../lib/private/docker';
2+
import { ShellOptions, ProcessFailedError } from '../../lib/private/shell';
3+
4+
type ShellExecuteMock = jest.SpyInstance<ReturnType<Docker['execute']>, Parameters<Docker['execute']>>;
5+
6+
describe('Docker', () => {
7+
describe('exists', () => {
8+
let docker: Docker;
9+
10+
const makeShellExecuteMock = (
11+
fn: (params: string[]) => void,
12+
): ShellExecuteMock =>
13+
jest.spyOn<{ execute: Docker['execute'] }, 'execute'>(Docker.prototype as any, 'execute').mockImplementation(
14+
async (params: string[], _options?: ShellOptions) => fn(params),
15+
);
16+
17+
afterEach(() => {
18+
jest.restoreAllMocks();
19+
});
20+
21+
beforeEach(() => {
22+
docker = new Docker();
23+
});
24+
25+
test('returns true when image inspect command does not throw', async () => {
26+
const spy = makeShellExecuteMock(() => undefined);
27+
28+
const imageExists = await docker.exists('foo');
29+
30+
expect(imageExists).toBe(true);
31+
expect(spy.mock.calls[0][0]).toEqual(['inspect', 'foo']);
32+
});
33+
34+
test('throws when an arbitrary error is caught', async () => {
35+
makeShellExecuteMock(() => {
36+
throw new Error();
37+
});
38+
39+
await expect(docker.exists('foo')).rejects.toThrow();
40+
});
41+
42+
test('throws when the error is a shell failure but the exit code is unrecognized', async () => {
43+
makeShellExecuteMock(() => {
44+
throw new (class extends Error implements ProcessFailedError {
45+
public readonly code = 'PROCESS_FAILED'
46+
public readonly exitCode = 47
47+
public readonly signal = null
48+
49+
constructor() {
50+
super('foo');
51+
}
52+
});
53+
});
54+
55+
await expect(docker.exists('foo')).rejects.toThrow();
56+
});
57+
58+
test('returns false when the error is a shell failure and the exit code is 1 (Docker)', async () => {
59+
makeShellExecuteMock(() => {
60+
throw new (class extends Error implements ProcessFailedError {
61+
public readonly code = 'PROCESS_FAILED'
62+
public readonly exitCode = 1
63+
public readonly signal = null
64+
65+
constructor() {
66+
super('foo');
67+
}
68+
});
69+
});
70+
71+
const imageExists = await docker.exists('foo');
72+
73+
expect(imageExists).toBe(false);
74+
});
75+
76+
test('returns false when the error is a shell failure and the exit code is 125 (Podman)', async () => {
77+
makeShellExecuteMock(() => {
78+
throw new (class extends Error implements ProcessFailedError {
79+
public readonly code = 'PROCESS_FAILED'
80+
public readonly exitCode = 125
81+
public readonly signal = null
82+
83+
constructor() {
84+
super('foo');
85+
}
86+
});
87+
});
88+
89+
const imageExists = await docker.exists('foo');
90+
91+
expect(imageExists).toBe(false);
92+
});
93+
});
94+
});

0 commit comments

Comments
 (0)