diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index ea9ab5e..20c432d 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -30,12 +30,7 @@ jobs: - run: composer install - name: Start SolrCloud - run: docker-compose up -d - - - name: Sleep a few seconds to wait for Solr to be ready - uses: jakejarvis/wait-action@master - with: - time: "10s" + run: docker compose up -d --wait - name: PHPUnit run: ./vendor/bin/phpunit --testdox --colors=always diff --git a/CHANGELOG.md b/CHANGELOG.md index 97db26d..effcb38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.1.0 (2025-07-07) + +This release ensures that authentication configuration of the Solr client is properly utilized also for raw Solr HTTP requests. +We do this by now using Solr test setup that requires authentication and thus force all our integration tests to be properly authenticated to work. + +- fix: Authentication credentials configured for an endpoint are not used for raw HTTP requests to Solr +- feat: Better error message in case of failed raw HTTP requests to Solr +- test: Move to [bitnami/solr](https://hub.docker.com/r/bitnami/solr) docker image for better authentication support +- ci: Change `docker-compose` to `docker compose` to fix failing job + ## 2.0.0 (2024-01-04) - feat: Support Solr Cloud collection configuration during creation (#3) diff --git a/docker-compose.yml b/docker-compose.yml index ecbdad3..66eb978 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,108 +1,109 @@ # Start a simple cluster with three ZooKeeper nodes and three Solr nodes. -# Source: https://github.com/docker-solr/docker-solr-examples/blob/master/docker-compose/docker-compose.yml +# Using the [bitnami/solr](https://hub.docker.com/r/bitnami/solr) image for easier authentication setup. + +x-zookeeper: &zookeeper + image: docker.io/bitnami/zookeeper:3.9 + ports: + - '2181' + - '2888' + - '3888' + environment: &zookeeper-environment + ZOO_SERVER_ID: 1 # NOTE: If you want to have an zookeeper ensemble, each must define their own unique ID + ALLOW_ANONYMOUS_LOGIN: yes + ZOO_4LW_COMMANDS_WHITELIST: srvr,mntr,conf,ruok + BITNAMI_DEBUG: true + + healthcheck: + test: nc -z localhost 2181 || exit -1 + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + +x-solr: &solr + image: docker.io/bitnami/solr:9.6.0 + ports: + - '8983' + depends_on: + - zoo1 + - zoo2 + - zoo3 + environment: &solr-environment + BITNAMI_DEBUG: true + + SOLR_LOG_LEVEL: WARN + SOLR_HEAP: "${SOLR_HEAP_SIZE:-512m}" + + SOLR_ZK_HOSTS: zoo1:2181,zoo2:2181,zoo3:2181 + + SOLR_ENABLE_CLOUD_MODE: yes # This will enable cloud mode and requires zookeeper (SOLR_ZK_HOSTS) + # SOLR_CLOUD_BOOTSTRAP is set per service to avoid race conditions + + SOLR_ENABLE_AUTHENTICATION: yes + + # NOTE: We need to set the SOLR_AUTH* environment variables so that solr scripts can + # still talk to the Solr instances, once authentication is enabled. + # The SOLR_AUTHENTICATION_OPTS should be specified in the Solr services that use this template. + # We don't really need this right now, but set this to avoid potential issues in the future. + # e.g. + # - Collection creation should work : docker compose exec solr-records solr create -c foo + # - Configuring invalid credentials fails collection creation : docker compose exec --env SOLR_AUTHENTICATION_OPTS='-Dbasicauth=foo:bar' solr-records solr create -c foo + SOLR_AUTH_TYPE: "basic" + SOLR_ADMIN_USERNAME: "admin" + SOLR_ADMIN_PASSWORD: "Bitnami" + SOLR_AUTHENTICATION_OPTS: "-Dbasicauth=admin:Bitnami" + + healthcheck: + # NOTE: Is available publicly, even when auth is enabled + # See `health` permission in default security.json (https://github.com/apache/solr/blob/main/solr/core/src/resources/security.json) + # See Solr docs for health endpoint: https://solr.apache.org/guide/solr/latest/configuration-guide/implicit-requesthandlers.html + test: ["CMD", "curl", "-f", "localhost:8983/api/node/health"] + start_period: 30s + interval: 30s + retries: 3 -version: '3.7' services: solr1: - image: solr:9.4 + <<: *solr container_name: solr1 ports: - "8983:8983" environment: - - ZK_HOST=zoo1:2181,zoo2:2181,zoo3:2181 - networks: - - solr - depends_on: - - zoo1 - - zoo2 - - zoo3 - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8983/solr/admin/collections?action=CLUSTERSTATUS"] - interval: 1s - retries: 30 + <<: *solr-environment + SOLR_CLOUD_BOOTSTRAP: yes # Only solr1 should bootstrap + solr2: - image: solr:9.4 + <<: *solr container_name: solr2 environment: - - ZK_HOST=zoo1:2181,zoo2:2181,zoo3:2181 - networks: - - solr - depends_on: - - zoo1 - - zoo2 - - zoo3 - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8983/solr/admin/collections?action=CLUSTERSTATUS"] - interval: 1s - retries: 30 + <<: *solr-environment + SOLR_CLOUD_BOOTSTRAP: no # Disable bootstrap for other nodes solr3: - image: solr:9.4 + <<: *solr container_name: solr3 environment: - - ZK_HOST=zoo1:2181,zoo2:2181,zoo3:2181 - networks: - - solr - depends_on: - - zoo1 - - zoo2 - - zoo3 - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8983/solr/admin/collections?action=CLUSTERSTATUS"] - interval: 1s - retries: 30 + <<: *solr-environment + SOLR_CLOUD_BOOTSTRAP: no # Disable bootstrap for other nodes zoo1: - image: zookeeper:3.9 - container_name: zoo1 - restart: always - hostname: zoo1 + <<: *zookeeper environment: - ZOO_MY_ID: 1 - ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 - ZOO_4LW_COMMANDS_WHITELIST: mntr, conf, ruok - ZOO_CFG_EXTRA: "metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider metricsProvider.httpPort=7000 metricsProvider.exportJvmInfo=true" - networks: - - solr - healthcheck: - test: nc -z localhost 2181 || exit -1 - interval: 1s - retries: 30 + <<: *zookeeper-environment + ZOO_SERVERS: zoo1:2888:3888,zoo2:2888:3888,zoo3:2888:3888 + ZOO_SERVER_ID: 1 zoo2: - image: zookeeper:3.9 - container_name: zoo2 - restart: always - hostname: zoo2 + <<: *zookeeper environment: - ZOO_MY_ID: 2 - ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 - ZOO_4LW_COMMANDS_WHITELIST: mntr, conf, ruok - ZOO_CFG_EXTRA: "metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider metricsProvider.httpPort=7000 metricsProvider.exportJvmInfo=true" - networks: - - solr - healthcheck: - test: nc -z localhost 2181 || exit -1 - interval: 1s - retries: 30 + <<: *zookeeper-environment + ZOO_SERVERS: zoo1:2888:3888,zoo2:2888:3888,zoo3:2888:3888 + ZOO_SERVER_ID: 2 zoo3: - image: zookeeper:3.9 - container_name: zoo3 - restart: always - hostname: zoo3 + <<: *zookeeper environment: - ZOO_MY_ID: 3 - ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 - ZOO_4LW_COMMANDS_WHITELIST: mntr, conf, ruok - ZOO_CFG_EXTRA: "metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider metricsProvider.httpPort=7000 metricsProvider.exportJvmInfo=true" - networks: - - solr - healthcheck: - test: nc -z localhost 2181 || exit -1 - interval: 1s - retries: 30 - -networks: - solr: + <<: *zookeeper-environment + ZOO_SERVERS: zoo1:2888:3888,zoo2:2888:3888,zoo3:2888:3888 + ZOO_SERVER_ID: 3 diff --git a/src/CollectionManager.php b/src/CollectionManager.php index fda411e..9830cf6 100644 --- a/src/CollectionManager.php +++ b/src/CollectionManager.php @@ -218,9 +218,23 @@ protected function rawApiRequest(string $path): array $base_uri = "{$scheme}://{$host}:{$port}/solr"; $url = "{$base_uri}/{$path}"; - $res = file_get_contents($url); - assert(false !== $res, "Request failed: $url"); + $auth = $endpoint->getAuthentication(); + $contextOptions = []; + if (!empty($auth['username']) && !empty($auth['password'])) { + $authHeader = 'Authorization: Basic ' . base64_encode("{$auth['username']}:{$auth['password']}"); + $contextOptions['http'] = [ + 'header' => $authHeader + ]; + } + + $context = stream_context_create($contextOptions); + $res = file_get_contents($url, false, $context); + if (false === $res) { + $error = error_get_last(); + $errorMessage = isset($error['message']) ? $error['message'] : 'Unknown error'; + throw new \RuntimeException("Request failed: $url. Error: $errorMessage"); + } $json = json_decode($res, true); diff --git a/tests/Integration/TestCase.php b/tests/Integration/TestCase.php index 2cdf82b..77e57d3 100644 --- a/tests/Integration/TestCase.php +++ b/tests/Integration/TestCase.php @@ -53,6 +53,11 @@ protected function getManager(): CollectionManager $client = new Client($adapter, $eventDispatcher, $config); + $client->getEndpoint()->setAuthentication( + SOLR_USERNAME, + SOLR_PASSWORD, + ); + return new CollectionManager($client); } } diff --git a/tests/config.php b/tests/config.php index 2176156..5086609 100644 --- a/tests/config.php +++ b/tests/config.php @@ -4,3 +4,5 @@ define('SOLR_PORT', getenv('TEST_SOLR_PORT') ? getenv('TEST_SOLR_PORT') : '8983'); define('SOLR_PATH', getenv('TEST_SOLR_PATH') ? getenv('TEST_SOLR_PATH') : '/'); define('SOLR_TIMEOUT', getenv('TEST_SOLR_TIMEOUT') ? getenv('TEST_SOLR_TIMEOUT') : '10'); +define('SOLR_USERNAME', getenv('TEST_SOLR_USERNAME') ? getenv('TEST_SOLR_USERNAME') : 'admin'); +define('SOLR_PASSWORD', getenv('TEST_SOLR_PASSWORD') ? getenv('TEST_SOLR_PASSWORD') : 'Bitnami');