From dd9f6c305fbf7952cbe812c6051284952e5ae282 Mon Sep 17 00:00:00 2001 From: Gustavo Freze Date: Mon, 13 Apr 2026 23:50:45 -0300 Subject: [PATCH] feat: Add Docker network commands and update execution logic for container networking. --- .../ContainerCommandHandler.php | 11 ++++++ .../Commands/DockerNetworkConnect.php | 22 +++++++++++ src/Internal/Containers/HostEnvironment.php | 18 +++++++++ tests/Unit/GenericDockerContainerTest.php | 39 ++++++++++++++++++- tests/Unit/Mocks/ClientMock.php | 12 ++++-- tests/Unit/MySQLDockerContainerTest.php | 17 +------- 6 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 src/Internal/Commands/DockerNetworkConnect.php create mode 100644 src/Internal/Containers/HostEnvironment.php diff --git a/src/Internal/CommandHandler/ContainerCommandHandler.php b/src/Internal/CommandHandler/ContainerCommandHandler.php index b832d32..1da37c9 100644 --- a/src/Internal/CommandHandler/ContainerCommandHandler.php +++ b/src/Internal/CommandHandler/ContainerCommandHandler.php @@ -10,11 +10,13 @@ use TinyBlocks\DockerContainer\Internal\Commands\Command; use TinyBlocks\DockerContainer\Internal\Commands\DockerCopy; use TinyBlocks\DockerContainer\Internal\Commands\DockerList; +use TinyBlocks\DockerContainer\Internal\Commands\DockerNetworkConnect; use TinyBlocks\DockerContainer\Internal\Commands\DockerNetworkCreate; use TinyBlocks\DockerContainer\Internal\Commands\DockerRun; use TinyBlocks\DockerContainer\Internal\Containers\ContainerLookup; use TinyBlocks\DockerContainer\Internal\Containers\Definitions\ContainerDefinition; use TinyBlocks\DockerContainer\Internal\Containers\Definitions\CopyInstruction; +use TinyBlocks\DockerContainer\Internal\Containers\HostEnvironment; use TinyBlocks\DockerContainer\Internal\Containers\Models\ContainerId; use TinyBlocks\DockerContainer\Internal\Containers\ShutdownHook; use TinyBlocks\DockerContainer\Internal\Exceptions\DockerCommandExecutionFailed; @@ -53,6 +55,15 @@ public function run(DockerRun $dockerRun): ContainerStarted { if (!is_null($dockerRun->definition->network)) { $this->client->execute(command: DockerNetworkCreate::from(network: $dockerRun->definition->network)); + + if (HostEnvironment::isInsideDocker()) { + $this->client->execute( + command: DockerNetworkConnect::from( + network: $dockerRun->definition->network, + container: HostEnvironment::containerHostname() + ) + ); + } } $executionCompleted = $this->client->execute(command: $dockerRun); diff --git a/src/Internal/Commands/DockerNetworkConnect.php b/src/Internal/Commands/DockerNetworkConnect.php new file mode 100644 index 0000000..bc1f147 --- /dev/null +++ b/src/Internal/Commands/DockerNetworkConnect.php @@ -0,0 +1,22 @@ +/dev/null || true', $this->network, $this->container); + } +} diff --git a/src/Internal/Containers/HostEnvironment.php b/src/Internal/Containers/HostEnvironment.php new file mode 100644 index 0000000..05c91ba --- /dev/null +++ b/src/Internal/Containers/HostEnvironment.php @@ -0,0 +1,18 @@ +client->getExecutedCommandLines()[1]; + $runCommand = $this->client->getExecutedCommandLines()[2]; self::assertStringContainsString(needle: '--network=my-network', haystack: $runCommand); } @@ -1248,4 +1249,40 @@ public function testRunIfNotExistsSkipsReaperCreationWhenReaperAlreadyExists(): ); } } + + #[RunInSeparateProcess] + public function testRunContainerWithNetworkWhenOutsideDockerSkipsHostConnection(): void + { + require_once __DIR__ . '/Internal/Containers/Overrides/file_exists_outside_docker.php'; + + /** @Given a container configured with a network, running outside a Docker environment */ + $client = new ClientMock(); + $container = TestableGenericDockerContainer::createWith( + image: 'alpine:latest', + name: 'outside-docker', + client: $client + )->withNetwork(name: 'my-network'); + + /** @And the Docker daemon returns valid responses */ + $client->withDockerRunResponse(output: InspectResponseFixture::containerId()); + $client->withDockerInspectResponse( + inspectResult: InspectResponseFixture::build(hostname: 'outside-docker') + ); + + /** @When the container is started */ + $started = $container->run(); + + /** @Then the container should be running */ + self::assertSame(expected: 'outside-docker', actual: $started->getName()); + + /** @And no network connect command should have been executed for the host */ + $commandLines = $client->getExecutedCommandLines(); + + foreach ($commandLines as $commandLine) { + self::assertStringNotContainsString( + needle: 'docker network connect', + haystack: $commandLine + ); + } + } } diff --git a/tests/Unit/Mocks/ClientMock.php b/tests/Unit/Mocks/ClientMock.php index 75bdf9c..1736b03 100644 --- a/tests/Unit/Mocks/ClientMock.php +++ b/tests/Unit/Mocks/ClientMock.php @@ -13,6 +13,8 @@ use TinyBlocks\DockerContainer\Internal\Commands\DockerExecute; use TinyBlocks\DockerContainer\Internal\Commands\DockerInspect; use TinyBlocks\DockerContainer\Internal\Commands\DockerList; +use TinyBlocks\DockerContainer\Internal\Commands\DockerNetworkConnect; +use TinyBlocks\DockerContainer\Internal\Commands\DockerNetworkCreate; use TinyBlocks\DockerContainer\Internal\Commands\DockerPull; use TinyBlocks\DockerContainer\Internal\Commands\DockerRun; use TinyBlocks\DockerContainer\Internal\Commands\DockerStop; @@ -102,10 +104,12 @@ public function execute(Command $command): ExecutionCompleted json_encode([($inspectData = array_shift($this->inspectResponses))]), !empty($inspectData) ], - $command instanceof DockerCopy => ['', true], - $command instanceof DockerPull => ['', true], - $command instanceof DockerStop => array_shift($this->stopResponses) ?? ['', true], - default => ['', false] + $command instanceof DockerCopy => ['', true], + $command instanceof DockerPull => ['', true], + $command instanceof DockerStop => array_shift($this->stopResponses) ?? ['', true], + $command instanceof DockerNetworkCreate => ['', true], + $command instanceof DockerNetworkConnect => ['', true], + default => ['', false] }; return new ExecutionCompletedMock(output: (string)$output, successful: $isSuccessful); diff --git a/tests/Unit/MySQLDockerContainerTest.php b/tests/Unit/MySQLDockerContainerTest.php index def8aa7..35ad5a9 100644 --- a/tests/Unit/MySQLDockerContainerTest.php +++ b/tests/Unit/MySQLDockerContainerTest.php @@ -12,7 +12,6 @@ use TinyBlocks\DockerContainer\Internal\Containers\ShutdownHook; use TinyBlocks\DockerContainer\Internal\Exceptions\ContainerWaitTimeout; use TinyBlocks\DockerContainer\Internal\Exceptions\DockerCommandExecutionFailed; -use TinyBlocks\DockerContainer\MySQLDockerContainer; use TinyBlocks\DockerContainer\Waits\Conditions\ContainerReady; use TinyBlocks\DockerContainer\Waits\ContainerWaitForDependency; @@ -105,7 +104,7 @@ public function testRunMySQLContainerSuccessfully(): void /** @And the docker run command should reflect delegated configuration */ $commandLines = $this->client->getExecutedCommandLines(); - $runCommand = $commandLines[1]; + $runCommand = $commandLines[2]; self::assertStringNotContainsString(needle: '--rm', haystack: $runCommand); self::assertStringContainsString(needle: '--volume /var/lib/mysql:/var/lib/mysql', haystack: $runCommand); @@ -116,7 +115,7 @@ public function testRunMySQLContainerSuccessfully(): void self::assertStringContainsString(needle: "MYSQL_ROOT_PASSWORD='root'", haystack: $runCommand); /** @And the database setup should include CREATE DATABASE, GRANT, and FLUSH */ - $setupCommand = $commandLines[4]; + $setupCommand = $commandLines[5]; self::assertStringContainsString(needle: 'CREATE DATABASE IF NOT EXISTS test_adm', haystack: $setupCommand); self::assertStringContainsString(needle: 'GRANT ALL PRIVILEGES', haystack: $setupCommand); @@ -822,18 +821,6 @@ public function testRunMySQLContainerWithPullImage(): void self::assertTrue($hasPullCommand); } - public function testFromCreatesMySQLContainerInstance(): void - { - /** @Given a valid MySQL image name */ - $image = 'mysql:8.1'; - - /** @When creating a MySQL container from the image */ - $container = MySQLDockerContainer::from(image: $image, name: 'from-mysql'); - - /** @Then the container should be an instance of MySQLDockerContainer */ - self::assertInstanceOf(expected: MySQLDockerContainer::class, actual: $container); - } - public function testStopOnShutdownDelegatesToUnderlyingContainer(): void { /** @Given a ShutdownHook that tracks registration */