[
  {
    "path": ".dockerignore",
    "content": ".*\nvendor/\n"
  },
  {
    "path": ".github/workflows/DockerHub.yml",
    "content": "name: Build and Push Docker images to Docker Hub\n\non: push\njobs:\n  build_job:\n    name: Build and push\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Run Buildx and push image\n        run: |\n          docker buildx create --use --name multi-arch-builder --platform \"linux/arm64,linux/amd64\"\n          docker buildx build --platform \"linux/arm64,linux/amd64\" --tag ${{ secrets.DOCKERHUB_USERNAME }}/docker-hostmanager:latest --file Dockerfile --output type=image,push=true .\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor/\n/bin/docker-hostmanager.phar\n.idea/\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: php\nsudo: false\nphp: 5.6\n\ncache:\n  directories:\n    - vendor\n    - $HOME/.composer/cache\n\ninstall: composer install --prefer-source\n\nscript: vendor/bin/phpunit --configuration phpunit.xml.dist\n\nbefore_deploy:\n  - curl -LSs http://box-project.github.io/box2/installer.php | php\n  - php box.phar build\n\ndeploy:\n  provider: releases\n  skip_cleanup: true\n  api_key:\n    secure: \"xYPmNWQNTRqLSQinnM64dRqDGAm6bKYODFmGdVLwjNqbICMHR0/sF00EPwBgIsXF100xbnjgJVRXD7lbL7nUYXWyzR2EcaW8yOxhYC0BYQMpOGI6RF0p+sMskCI5hb8Y+FT++nQZTwHPo9MtrAv2ec5r42RO2K0YM6WS6URsCqbTQnDtWcLReszRrLGVy41tkdlse9uqk63IbJmRwLunMkQBJ/BhVSRDl5Qm+Q3aDhjckZanX4QH0UrR75azut3CUIQ1l/wVF9dhKPHuvIc5+3qwkxqgOmaBFozE2hvlviWCunQsZMpaWG9L3v19VzuvypDvvvK+rhwytXsOO2gz0JGh/AL6TsonGqePYdESE7tBZ+sJz5tZ0q0yqEOLGSlxa7i5bF3KN3PCqK8eBdgBHWWDnWgO0blmPFKLYaehxZqnDHr8w5bHlW2yS1fYq8X5zkmz1fbkjpPFXX6TWsm8imlKsqzhSPBTrF+E/6f91TOLlv7tXIA0hi7Ex4ZOuzUCSs6qYWfPYPKWnguL8kmkG9wKnFahQwxVz2CM2ZPxhNQ8j03ao+wkBr6+pt6KhghqYJQ83c5GrzDXJWXbpuNdFt+RfPVLT0w17tWj3H80b/QZFa2TKQtSZ41jnAKPHHt3m178s3DfpQ6Hf1Zi1kvru0jiGKdph94j7zcYeB7uV3A=\"\n  file: bin/docker-hostmanager.phar\n  on:\n    tags: true\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM composer:1.4.3\n\nADD . /usr/local/src/docker-hostmanager\n\nRUN composer install --no-interaction --no-dev --prefer-dist --working-dir=/usr/local/src/docker-hostmanager \\\n    && ln -s /usr/local/src/docker-hostmanager/bin/docker-hostmanager /usr/local/bin/docker-hostmanager\n\nENV HOSTS_FILE=/hosts\n\nENTRYPOINT [\"/usr/local/bin/docker-hostmanager\"]\n"
  },
  {
    "path": "LICENCE",
    "content": "Copyright 2016 Luc Vieillescazes\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "docker-hostmanager\n==================\n\n### ABOUT\n\nUpdate automatically your `/etc/hosts` to access running containers.\nInspired by `vagrant-hostmanager`.\n\nProject homepage: [https://github.com/iamluc/docker-hostmanager](https://github.com/iamluc/docker-hostmanager)\n\n### USAGE\n\n#### Linux\n\nThe easiest way is to use the docker image\n\n```console\n$ docker run -d --name docker-hostmanager --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /etc/hosts:/hosts iamluc/docker-hostmanager\n```\n\n*Note: the `--restart=always` option will make the container start automatically with your computer (recommended).*\n\n#### Mac OS\n\nDownload the PHAR executable here : https://github.com/iamluc/docker-hostmanager/releases\n\nAnd then run it:\n\n```console\n$ sudo php docker-hostmanager.phar synchronize-hosts\n```\n\nNote: We run the command as root as we need the permission to write file `/etc/hosts`.\nIf you don't want to run the command as root, grant the correct permission to you user.\n\nBefore running the command, don't forget to export your docker environment variables.\ni.e.\n\n```\n$ eval $(docker-machine env mybox)\n```\n\nAlso, you should add a route to access containers inside your VM.\n\n```\n$ sudo route -n add 172.0.0.0/8 $(docker-machine ip $(docker-machine active))\n```\n\n#### Windows\n\nIf the host, dont use Docker ToolBox or not a Windows 10 PRO, then needs to mount the /c/Windows folder onto VirtualBox.\n\n```console\n$ docker run -d --name docker-hostmanager --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /c/Windows/System32/drivers/etc/hosts:/hosts iamluc/docker-hostmanager\n```\n\nAfter run the container we need to add a route to access container subnets.\n\n```\n$ route /P add 172.17.0.0/12 192.168.99.100\n```\n\n### CONFIGURATION\n\n#### With networks\n\nWhen a container belongs to at least one network (typically when using a `docker-compose.yml` file in version >= 2), the name defined to access the container is `CONTAINER_NAME.CONTAINER_NETWORK`. It works also with the alias defined for the network.\n\nAs a container can belongs to several networks at the same time, and thanks to alias, you can define how you want to access your container.\n\n**Example 1 (default network):**\n```yaml\nversion: '2'\n\nservices:\n    web:\n        image: iamluc/symfony\n        volumes:\n            - .:/var/www/html\n```\n\nThe container `web` will be accessible with `web.myapp_default` (if the docker-compose project name is `myapp`)\n\n**Example 2 (custom network name and alias):**\n```yaml\nversion: '3.5'\n\nnetworks:\n    default:\n        name: myapp\n\nservices:\n    web:\n        image: iamluc/symfony\n        volumes:\n            - .:/var/www/html\n\n    mysql:\n        image: mysql\n        networks:\n            default:\n                aliases:\n                    - bdd\n```\n\nThe `web` container will be accessible with `web.myapp`.\nThe `mysql` container will be accessible with `mysql.myapp` or `bdd.myapp`\n\n#### Without networks\n\nWhen a container has no defined network (only the default \"bridge\" one), it is accessible by its container name, concatened with the defined TLD (`.docker` by default).\nIt is the case when you run a single container with the `docker` command or when you use a `docker-compose.yml` file in version 1.\n\nThe `DOMAIN_NAME` environment variable lets you define additional hosts for your container.\ne.g.:\n```\n$ docker run -d -e DOMAIN_NAME=test.com,www.test.com my_image\n```\n\n### Tests\n\nTo run test, execute the following command : `vendor/bin/phpunit`\n\n### LICENSE\n\n[MIT](https://opensource.org/licenses/MIT)\n"
  },
  {
    "path": "bin/docker-hostmanager",
    "content": "#!/usr/bin/env php\n<?php\n\nif (PHP_SAPI !== 'cli') {\n    die('Warning: docker-hostmanager must be invoked via the CLI version of PHP, not the '.PHP_SAPI.' SAPI'.PHP_EOL);\n}\n\nforeach ([__DIR__.'/../vendor/autoload.php', __DIR__.'/../../../autoload.php'] as $file) {\n    if (file_exists($file)) {\n        require $file;\n        break;\n    }\n}\n\nuse DockerHostManager\\Command\\SynchronizeHostsCommand;\nuse Symfony\\Component\\Console\\Application;\n\n$application = new Application('DockerHostManager', '@package_version@');\n$application->add(new SynchronizeHostsCommand());\n$application->setDefaultCommand('synchronize-hosts');\n$application->run();\n"
  },
  {
    "path": "box.json",
    "content": "{\n  \"main\": \"bin/docker-hostmanager\",\n  \"output\": \"bin/docker-hostmanager.phar\",\n  \"finder\": [\n    {\n      \"name\": [\"*.php\"],\n      \"exclude\": [\"Tests\", \"tests\"],\n      \"in\": [\"src\", \"vendor\"]\n    }\n  ],\n  \"chmod\": \"0755\",\n  \"stub\": true,\n  \"git-version\": \"package_version\"\n}\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"iamluc/docker-hostmanager\",\n    \"license\": \"MIT\",\n    \"version\": \"0.0.4\",\n    \"type\": \"project\",\n    \"description\": \"Update /etc/hosts to access running containers\",\n    \"keywords\": [\"docker\", \"hosts\"],\n    \"repositories\": [\n        {\"type\": \"vcs\", \"url\": \"https://github.com/iamluc/docker-php.git\"}\n    ],\n    \"require\": {\n        \"symfony/console\": \"^2.8|^3.0\",\n        \"docker-php/docker-php\": \"dev-compat-docker-1.12\"\n    },\n    \"require-dev\": {\n        \"phpunit/phpunit\": \"^5.1\"\n    },\n    \"bin\": [\n        \"bin/docker-hostmanager\"\n    ],\n    \"authors\": [\n        {\n            \"name\": \"Luc Vieillescazes\",\n            \"email\": \"luc@vieillescazes.net\"\n        }\n    ],\n    \"autoload\": {\n        \"psr-4\": {\"DockerHostManager\\\\\": \"src/\", \"Test\\\\\": \"tests\"}\n    }\n}\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<phpunit\n        backupGlobals=\"false\"\n        backupStaticAttributes=\"false\"\n        colors=\"true\"\n        convertErrorsToExceptions=\"true\"\n        convertNoticesToExceptions=\"true\"\n        convertWarningsToExceptions=\"true\"\n        processIsolation=\"false\"\n        stopOnFailure=\"false\"\n        syntaxCheck=\"false\"\n        bootstrap=\"tests/bootstrap.php\">\n    <testsuites>\n        <testsuite name=\"DockerHostManager Test Suite\">\n            <directory suffix=\"Test.php\">tests</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>\n"
  },
  {
    "path": "src/Command/SynchronizeHostsCommand.php",
    "content": "<?php\n\nnamespace DockerHostManager\\Command;\n\nuse DockerHostManager\\Docker\\Docker;\nuse DockerHostManager\\Synchronizer;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass SynchronizeHostsCommand extends Command\n{\n    protected function configure()\n    {\n        $this\n            ->setName('synchronize-hosts')\n            ->setDescription('Run the application')\n            ->addOption(\n                'hosts_file',\n                'f',\n                InputOption::VALUE_REQUIRED,\n                'The host file to update',\n                getenv('HOSTS_FILE') ?: '/etc/hosts'\n            )\n            ->addOption(\n                'tld',\n                't',\n                InputOption::VALUE_REQUIRED,\n                'The TLD to use',\n                getenv('TLD') ?: '.docker'\n            )\n        ;\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output)\n    {\n        $app = new Synchronizer(\n            new Docker(),\n            $input->getOption('hosts_file'),\n            $input->getOption('tld')\n        );\n\n        $app->run();\n    }\n}\n"
  },
  {
    "path": "src/Docker/Docker.php",
    "content": "<?php\n\nnamespace DockerHostManager\\Docker;\n\nuse Docker\\Docker as DockerBase;\nuse Docker\\DockerClient;\nuse Http\\Client\\HttpClient;\nuse Http\\Message\\MessageFactory;\nuse Http\\Message\\MessageFactory\\GuzzleMessageFactory;\nuse Symfony\\Component\\Serializer\\Serializer;\n\nclass Docker extends DockerBase\n{\n    /**\n     * @var HttpClient\n     */\n    private $httpClient;\n\n    /**\n     * @var MessageFactory\n     */\n    private $messageFactory;\n\n    public function __construct(HttpClient $httpClient = null, Serializer $serializer = null, MessageFactory $messageFactory = null)\n    {\n        $this->httpClient = $httpClient ?: DockerClient::createFromEnv();\n        $this->messageFactory = $messageFactory ?: new GuzzleMessageFactory();\n\n        parent::__construct($this->httpClient, $serializer, $this->messageFactory);\n    }\n\n    /**\n     * @param callable $callback\n     */\n    public function listenEvents(callable $callback)\n    {\n        $request = $this->messageFactory->createRequest('GET', '/events');\n        $response = $this->httpClient->sendRequest($request);\n\n        $stream = $response->getBody();\n        while (!$stream->eof()) {\n            $line = \\GuzzleHttp\\Psr7\\readline($stream);\n            if (null !== ($raw = json_decode($line, true))) {\n                call_user_func($callback, new Event($raw));\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Docker/Event.php",
    "content": "<?php\n\nnamespace DockerHostManager\\Docker;\n\nclass Event\n{\n    /** @var string */\n    protected $status;\n\n    /** @var string */\n    protected $id;\n\n    /** @var string */\n    protected $from;\n\n    /** @var string */\n    protected $time;\n\n    /**\n     * @param string $raw\n     */\n    public function __construct($raw)\n    {\n        $this->status = $raw['status'] ?? $raw['Action'] ?? null;\n        $this->id = $raw['id'] ?? $raw['Actor']['ID'] ?? null;\n        $this->from = $raw['from'] ?? $raw['Actor']['Attributes']['image'] ?? null;\n        $this->time = $raw['time'] ?? null;\n    }\n\n    /**\n     * @return string\n     */\n    public function getStatus()\n    {\n        return $this->status;\n    }\n\n    /**\n     * @return string\n     */\n    public function getId()\n    {\n        return $this->id;\n    }\n\n    /**\n     * @return string\n     */\n    public function getFrom()\n    {\n        return $this->from;\n    }\n\n    /**\n     * @return string\n     */\n    public function getTime()\n    {\n        return $this->time;\n    }\n}\n"
  },
  {
    "path": "src/Synchronizer.php",
    "content": "<?php\n\nnamespace DockerHostManager;\n\nuse Docker\\API\\Model\\Container;\nuse Docker\\Manager\\ContainerManager;\nuse DockerHostManager\\Docker\\Docker;\nuse DockerHostManager\\Docker\\Event;\n\nclass Synchronizer\n{\n    const START_TAG = '## docker-hostmanager-start';\n    const END_TAG = '## docker-hostmanager-end';\n\n    /** @var Docker  */\n    private $docker;\n    /** @var string */\n    private $hostsFile;\n    /** @var string */\n    private $tld;\n\n    /** @var array */\n    private $activeContainers = [];\n\n    /**\n     * @param Docker $docker\n     * @param string $hostsFile\n     * @param string $tld\n     */\n    public function __construct(Docker $docker, $hostsFile, $tld)\n    {\n        $this->docker = $docker;\n        $this->hostsFile = $hostsFile;\n        $this->tld = $tld;\n    }\n\n    public function run()\n    {\n        if (!is_writable($this->hostsFile)) {\n            throw new \\RuntimeException(sprintf('File \"%s\" is not writable.', $this->hostsFile));\n        }\n\n        $this->init();\n        $this->listen();\n    }\n\n    private function init()\n    {\n        foreach ($this->docker->getContainerManager()->findAll() as $containerConfig) {\n            $response = $this->docker->getContainerManager()->find($containerConfig->getId(), [], ContainerManager::FETCH_RESPONSE);\n            $container = json_decode(\\GuzzleHttp\\Psr7\\copy_to_string($response->getBody()), true);\n\n            if ($this->isExposed($container)) {\n                $this->activeContainers[$container['Id']] = $container;\n            }\n        }\n\n        $this->write();\n    }\n\n    private function listen()\n    {\n        $this->docker->listenEvents(function (Event $event) {\n            if (null === $event->getId()) {\n                return;\n            }\n\n            try  {\n                $response = $this->docker->getContainerManager()->find($event->getId(), [], ContainerManager::FETCH_RESPONSE);\n                $container = json_decode(\\GuzzleHttp\\Psr7\\copy_to_string($response->getBody()), true);\n            } catch (\\Exception $e) {\n                return;\n            }\n\n            if (null === $container) {\n                return;\n            }\n\n            if ($this->isExposed($container)) {\n                $this->activeContainers[$container['Id']] = $container;\n            } else {\n                unset($this->activeContainers[$container['Id']]);\n            }\n\n            $this->write();\n        });\n    }\n\n    private function write()\n    {\n        $content = array_map('trim', file($this->hostsFile));\n        $res = preg_grep('/^'.self::START_TAG.'/', $content);\n        $start = count($res) ? key($res) : count($content) + 1;\n        $res = preg_grep('/^'.self::END_TAG.'/', $content);\n        $end = count($res) ? key($res) : count($content) + 1;\n        $hosts = array_merge(\n            [self::START_TAG],\n            array_map(\n                function ($container) {\n                    return implode(\"\\n\", $this->getHostsLines($container));\n                },\n                $this->activeContainers\n            ),\n            [self::END_TAG]\n        );\n        array_splice($content, $start, $end - $start + 1, $hosts);\n        file_put_contents($this->hostsFile, implode(\"\\n\", $content));\n    }\n\n    /**\n     * @param $container\n     *\n     * @return array\n     */\n    private function getHostsLines($container)\n    {\n        $lines = [];\n\n        // Global\n        if (!empty($container['NetworkSettings']['IPAddress'])) {\n            $ip = $container['NetworkSettings']['IPAddress'];\n\n            $lines[$ip] = implode(' ', $this->getContainerHosts($container));\n        }\n\n        // Networks\n        if (isset($container['NetworkSettings']['Networks']) && is_array($container['NetworkSettings']['Networks'])) {\n            foreach ($container['NetworkSettings']['Networks'] as $networkName => $conf) {\n                $ip = $conf['IPAddress'];\n\n                $aliases = isset($conf['Aliases']) && is_array($conf['Aliases']) ? $conf['Aliases'] : [];\n                $aliases[] = substr($container['Name'], 1);\n\n                $hosts = [];\n                foreach (array_unique($aliases) as $alias) {\n                    $hosts[] = $alias.'.'.$networkName;\n                }\n\n                $lines[$ip] = sprintf('%s%s', isset($lines[$ip]) ? $lines[$ip].' ' : '', implode(' ', $hosts));\n            }\n        }\n\n        array_walk($lines, function (&$host, $ip) {\n            $host = $ip.' '.$host;\n        });\n\n        return $lines;\n    }\n\n    /**\n     * @param Container $container\n     *\n     * @return array\n     */\n    private function getContainerHosts($container)\n    {\n        $hosts = [substr($container['Name'], 1).$this->tld];\n        if (isset($container['Config']['Env']) && is_array($container['Config']['Env'])) {\n            $env = $container['Config']['Env'];\n            foreach (preg_grep('/DOMAIN_NAME=/', $env) as $row) {\n                $row = substr($row, strlen('DOMAIN_NAME='));\n                $hosts = array_merge($hosts, explode(',', $row));\n            }\n        }\n\n        return $hosts;\n    }\n\n    /**\n     * @param Container $container\n     *\n     * @return bool\n     */\n    private function isExposed($container)\n    {\n        if (empty($container['NetworkSettings']['Ports']) || empty($container['State']['Running'])) {\n            return false;\n        }\n\n        return $container['State']['Running'];\n    }\n}\n"
  },
  {
    "path": "tests/DockerHostManager/SynchronizerTest.php",
    "content": "<?php\n\nnamespace Test\\DockerHostManager;\n\nuse Docker\\Docker;\nuse DockerHostManager\\Synchronizer;\nuse Test\\Utils\\PropertyAccessor;\n\nclass SynchronizerTest extends \\PHPUnit_Framework_TestCase\n{\n    public function testThatAppCanBeConstructed()\n    {\n        $docker = $this->prophesize('DockerHostManager\\Docker\\Docker');\n        $docker = $docker->reveal();\n\n        $application = new Synchronizer($docker, '/etc/hosts', 'docker');\n\n        $this->assertSame($docker, PropertyAccessor::getProperty($application, 'docker'));\n        $this->assertSame('/etc/hosts', PropertyAccessor::getProperty($application, 'hostsFile'));\n        $this->assertSame('docker', PropertyAccessor::getProperty($application, 'tld'));\n        $this->assertInstanceOf(Docker::class, PropertyAccessor::getProperty($application, 'docker'));\n        $this->assertInternalType('array', PropertyAccessor::getProperty($application, 'activeContainers'));\n    }\n}\n"
  },
  {
    "path": "tests/Utils/PropertyAccessor.php",
    "content": "<?php\n\nnamespace Test\\Utils;\n\nclass PropertyAccessor\n{\n    public static function getProperty($object, $property)\n    {\n        if (!is_object($object)) {\n            throw new \\InvalidArgumentException(\n                sprintf('The first parameter must be an object: \"%s\" given.', gettype($object))\n            );\n        }\n\n        $reflection = new \\ReflectionProperty($object, $property);\n        $reflection->setAccessible(true);\n\n        return $reflection->getValue($object);\n    }\n}\n"
  },
  {
    "path": "tests/bootstrap.php",
    "content": "<?php\n\nrequire __DIR__.'/../vendor/autoload.php';\n"
  }
]