From 4735ee2f5a2c98a697fedd439e03eeca2091ed77 Mon Sep 17 00:00:00 2001 From: MAHDTech Date: Mon, 16 Mar 2026 12:45:51 +1100 Subject: [PATCH 1/3] feat: Add optional Docker extraHosts --- .../src/satellite/configs/emulator.config.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/config/src/satellite/configs/emulator.config.ts b/packages/config/src/satellite/configs/emulator.config.ts index ccfe08174..24519862f 100644 --- a/packages/config/src/satellite/configs/emulator.config.ts +++ b/packages/config/src/satellite/configs/emulator.config.ts @@ -116,7 +116,8 @@ const EmulatorRunnerSchema = z.strictObject({ name: z.string().optional(), volume: z.string().optional(), target: z.string().optional(), - platform: z.enum(['linux/amd64', 'linux/arm64']).optional() + platform: z.enum(['linux/amd64', 'linux/arm64']).optional(), + extraHosts: z.array(z.string()).optional() }); /** @@ -155,6 +156,25 @@ export interface EmulatorRunner { * The platform to use when running the emulator container. */ platform?: 'linux/amd64' | 'linux/arm64'; + + /** + * Additional host-to-IP mappings to inject into the container via `--add-host`. + * Format: `"hostname:ip"` or `"hostname:host-gateway"`. + * + * This is useful for making host-machine services (e.g. a local Ethereum RPC + * node) reachable from within the container under a stable DNS name such as + * `host.docker.internal`. + * + * @example + * ```ts + * runner: { + * extraHosts: ['host.docker.internal:host-gateway'] + * } + * ``` + * + * @see https://docs.docker.com/reference/cli/docker/container/run/#add-host + */ + extraHosts?: string[]; } /** From 093c36eba6df89df88029f83c456ee05e33d3598 Mon Sep 17 00:00:00 2001 From: MAHDTech Date: Mon, 16 Mar 2026 18:36:59 +1100 Subject: [PATCH 2/3] fix: use tuple method - use the cleaner tuple method - add test suite --- .../src/satellite/configs/emulator.config.ts | 13 ++- .../satellite/configs/emulator.config.spec.ts | 95 +++++++++++++++++++ 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/packages/config/src/satellite/configs/emulator.config.ts b/packages/config/src/satellite/configs/emulator.config.ts index 24519862f..a2fed8017 100644 --- a/packages/config/src/satellite/configs/emulator.config.ts +++ b/packages/config/src/satellite/configs/emulator.config.ts @@ -110,6 +110,8 @@ export interface EmulatorSatellite { /** * @see EmulatorRunner */ +const HostnameSchema = z.string().min(1); + const EmulatorRunnerSchema = z.strictObject({ type: z.enum(['docker', 'podman']), image: z.string().optional(), @@ -117,7 +119,9 @@ const EmulatorRunnerSchema = z.strictObject({ volume: z.string().optional(), target: z.string().optional(), platform: z.enum(['linux/amd64', 'linux/arm64']).optional(), - extraHosts: z.array(z.string()).optional() + extraHosts: z + .array(z.tuple([HostnameSchema, z.union([z.ipv4(), z.ipv6(), z.literal('host-gateway'), HostnameSchema])])) + .optional() }); /** @@ -159,7 +163,8 @@ export interface EmulatorRunner { /** * Additional host-to-IP mappings to inject into the container via `--add-host`. - * Format: `"hostname:ip"` or `"hostname:host-gateway"`. + * Each entry is a `[hostname, destination]` tuple where destination is an IPv4 + * address, an IPv6 address, `"host-gateway"`, or an arbitrary host string. * * This is useful for making host-machine services (e.g. a local Ethereum RPC * node) reachable from within the container under a stable DNS name such as @@ -168,13 +173,13 @@ export interface EmulatorRunner { * @example * ```ts * runner: { - * extraHosts: ['host.docker.internal:host-gateway'] + * extraHosts: [['host.docker.internal', 'host-gateway']] * } * ``` * * @see https://docs.docker.com/reference/cli/docker/container/run/#add-host */ - extraHosts?: string[]; + extraHosts?: [string, string][]; } /** diff --git a/packages/config/src/tests/satellite/configs/emulator.config.spec.ts b/packages/config/src/tests/satellite/configs/emulator.config.spec.ts index 44b27d244..f957d7989 100644 --- a/packages/config/src/tests/satellite/configs/emulator.config.spec.ts +++ b/packages/config/src/tests/satellite/configs/emulator.config.spec.ts @@ -591,5 +591,100 @@ describe('emulator.config', () => { expect(res.success).toBe(true); }); }); + + describe('runner.extraHosts', () => { + const withExtraHosts = (extraHosts: unknown) => ({ + runner: {type: 'docker', extraHosts} + }); + + it('accepts a valid IPv4 entry', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([['myhost', '192.168.1.1']]) + ); + expect(result.success).toBe(true); + }); + + it('accepts a valid IPv6 entry', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([['myhost', '::1']]) + ); + expect(result.success).toBe(true); + }); + + it('accepts "host-gateway" as a destination', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([['host.docker.internal', 'host-gateway']]) + ); + expect(result.success).toBe(true); + }); + + it('accepts an arbitrary non-empty string as a destination (fallback hostname)', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([['myhost', 'some-other-host']]) + ); + expect(result.success).toBe(true); + }); + + it('accepts multiple entries', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([ + ['host.docker.internal', 'host-gateway'], + ['eth-rpc', '192.168.0.10'], + ['ipv6host', '2001:db8::1'] + ]) + ); + expect(result.success).toBe(true); + }); + + it('accepts an empty array', () => { + const result = EmulatorConfigSchema.safeParse(withExtraHosts([])); + expect(result.success).toBe(true); + }); + + it('is optional (omitted entirely)', () => { + const result = EmulatorConfigSchema.safeParse({runner: {type: 'docker'}}); + expect(result.success).toBe(true); + }); + + it('rejects the old flat-string format', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts(['host.docker.internal:host-gateway']) + ); + expect(result.success).toBe(false); + }); + + it('rejects an entry with an empty hostname', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([['', '192.168.1.1']]) + ); + expect(result.success).toBe(false); + }); + + it('rejects an entry with an empty destination', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([['myhost', '']]) + ); + expect(result.success).toBe(false); + }); + + it('rejects a tuple with more than two elements', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([['myhost', '192.168.1.1', 'extra']]) + ); + expect(result.success).toBe(false); + }); + + it('rejects a tuple with only one element', () => { + const result = EmulatorConfigSchema.safeParse( + withExtraHosts([['myhost']]) + ); + expect(result.success).toBe(false); + }); + + it('rejects a non-array value', () => { + const result = EmulatorConfigSchema.safeParse(withExtraHosts('invalid')); + expect(result.success).toBe(false); + }); + }); }); }); From 76230942f6a14c27b90581510b25c3b7ab4ff339 Mon Sep 17 00:00:00 2001 From: MAHDTech Date: Mon, 16 Mar 2026 18:38:15 +1100 Subject: [PATCH 3/3] fix: get tests to pass --- .../config/src/tests/satellite/configs/emulator.config.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/config/src/tests/satellite/configs/emulator.config.spec.ts b/packages/config/src/tests/satellite/configs/emulator.config.spec.ts index f957d7989..7fef0061b 100644 --- a/packages/config/src/tests/satellite/configs/emulator.config.spec.ts +++ b/packages/config/src/tests/satellite/configs/emulator.config.spec.ts @@ -594,6 +594,7 @@ describe('emulator.config', () => { describe('runner.extraHosts', () => { const withExtraHosts = (extraHosts: unknown) => ({ + skylab: {}, runner: {type: 'docker', extraHosts} }); @@ -642,7 +643,7 @@ describe('emulator.config', () => { }); it('is optional (omitted entirely)', () => { - const result = EmulatorConfigSchema.safeParse({runner: {type: 'docker'}}); + const result = EmulatorConfigSchema.safeParse({skylab: {}, runner: {type: 'docker'}}); expect(result.success).toBe(true); });