diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 165243c7..b866b102 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -311,8 +311,12 @@ deploy: # CI_ENVIRONMENT_URL is the URL configured in the GitLab environment - export CI_ENVIRONMENT_URL="${CI_ENVIRONMENT_URL:-https://${CI_PROJECT_PATH_SLUG}.${KUBE_INGRESS_BASE_DOMAIN}/}" - export CI_IMAGE=$RELEASE_IMAGE + - export CI_INGRESS_CLASS=${CI_INGRESS_CLASS:-traefik} + - export CI_INGRESS_MATCH=${CI_INGRESS_MATCH:-$( if [[ "$CI_INGRESS_CLASS" == "nginx" ]]; then echo '/?(.*)'; fi )} + - export CI_INGRESS_TRAEFIK_ENTRYPOINT=${CI_INGRESS_TRAEFIK_ENTRYPOINT:-websecure} - export CI_INGRESS_DOMAIN=$(echo "$CI_ENVIRONMENT_URL" | grep -oP '(?:https?://)?\K([^/]+)' | head -n1) - export CI_INGRESS_PATH=$(echo "$CI_ENVIRONMENT_URL" | grep -oP '(?:https?://)?(?:[^/])+\K(.*)') + - '[[ "${CI_INGRESS_PATH}" == /* ]] || export CI_INGRESS_PATH="/${CI_INGRESS_PATH}"' - export CI_KUBE_NAMESPACE=$KUBE_NAMESPACE # Any available storage class like default, local-path (if you know what you are doing ;), longhorn etc. - export CI_PVC_SC=${CI_PVC_SC:-"${CI_PVC_SC_LOCAL:-local-path}"} @@ -328,6 +332,7 @@ deploy: done - echo "Deploying to ${CI_ENVIRONMENT_URL}" + - kubectl diff -f deployment.yaml || true - kubectl apply -f deployment.yaml - >- kubectl -n $CI_KUBE_NAMESPACE wait --for=condition=Ready pods --timeout=${CI_WAIT_TIMEOUT:-5}m diff --git a/deployment.tpl.yaml b/deployment.tpl.yaml index 12931bb7..468000bb 100644 --- a/deployment.tpl.yaml +++ b/deployment.tpl.yaml @@ -66,7 +66,6 @@ metadata: labels: app: environment: - commit: '' spec: type: ClusterIP ports: @@ -105,6 +104,7 @@ spec: environment: tier: application commit: '' + pipeline: '' annotations: app.gitlab.com/app: app.gitlab.com/env: @@ -159,7 +159,6 @@ metadata: labels: app: environment: - commit: '' annotations: prometheus.io/port: '80' prometheus.io/scrape: 'true' @@ -181,13 +180,14 @@ metadata: name: engelsystem-ingress annotations: kubernetes.io/tls-acme: 'true' - kubernetes.io/ingress.class: 'nginx' + kubernetes.io/ingress.class: cert-manager.io/cluster-issuer: nginx.ingress.kubernetes.io/rewrite-target: /$1 + traefik.ingress.kubernetes.io/router.entrypoints: + traefik.ingress.kubernetes.io/router.tls: 'true' labels: app: environment: - commit: '' spec: tls: - hosts: @@ -197,7 +197,7 @@ spec: - host: http: paths: - - path: '/?(.*)' + - path: '' pathType: Prefix backend: service: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 0320edbb..01c5678a 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -1,13 +1,19 @@ #!/usr/bin/env sh set -e -nginx -g 'daemon off;'& - # If first arg starts with a `-` or is empty if [[ "${1#-}" != "${1}" ]] || [[ -z "${1}" ]]; then set -- php-fpm "$@" fi +# Configure app url +url=$(echo "$APP_URL" | sed -n 's~https*://[^/]\+/\(.*\)~\1~p') +url=${url%/} +if [[ -n "${url}" ]]; then + echo "Url prefix: '${url}'" + sed -i "s~location /~rewrite ^/${url}(/.*)?$ /\$1;\n location /~" /etc/nginx/nginx.conf +fi + function get_name() { echo "$1" | cut -d: -f1 } @@ -37,4 +43,6 @@ if [[ -n "${RUN_USER}" ]]; then echo "Running as $user:$group" fi + +nginx -g 'daemon off;'& exec "$@" diff --git a/docker/nginx.conf b/docker/nginx.conf index a53a9449..0f5ca4af 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -20,9 +20,9 @@ http { } server { - include mime.types; - access_log off; - listen [::]:80 ipv6only=off; + include mime.types; + access_log /dev/stdout; + listen [::]:80 ipv6only=off; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -32,7 +32,7 @@ http { root /var/www/public; location / { - try_files $uri $uri/ /index.php?$args; + try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { diff --git a/src/Http/RequestServiceProvider.php b/src/Http/RequestServiceProvider.php index 382c6cae..f38902cb 100644 --- a/src/Http/RequestServiceProvider.php +++ b/src/Http/RequestServiceProvider.php @@ -2,20 +2,31 @@ namespace Engelsystem\Http; +use Engelsystem\Config\Config; use Engelsystem\Container\ServiceProvider; +use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; class RequestServiceProvider extends ServiceProvider { + /** @var array */ + protected array $appUrl; + public function register() { + /** @var Config $config */ $config = $this->app->get('config'); $trustedProxies = $config->get('trusted_proxies', []); + $this->appUrl = parse_url($config->get('url') ?: ''); if (!is_array($trustedProxies)) { $trustedProxies = empty($trustedProxies) ? [] : explode(',', preg_replace('~\s+~', '', $trustedProxies)); } + if (!empty($this->appUrl['path'])) { + Request::setFactory([$this, 'createRequestWithoutPrefix']); + } + /** @var Request $request */ $request = $this->app->call([Request::class, 'createFromGlobals']); $this->setTrustedProxies($request, $trustedProxies); @@ -25,6 +36,46 @@ class RequestServiceProvider extends ServiceProvider $this->app->instance('request', $request); } + /** + * @param array $query GET parameters + * @param array $request POST parameters + * @param array $attributes Additional data + * @param array $cookies Cookies + * @param array $files Uploaded files + * @param array $server Server env + * @param mixed $content Request content + * @return Request + */ + public function createRequestWithoutPrefix( + array $query = [], + array $request = [], + array $attributes = [], + array $cookies = [], + array $files = [], + array $server = [], + $content = null + ): Request { + if ( + !empty($this->appUrl['path']) + && !empty($server['REQUEST_URI']) + && Str::startsWith($server['REQUEST_URI'], $this->appUrl['path']) + ) { + $requestUri = Str::substr( + $server['REQUEST_URI'], + Str::length(rtrim($this->appUrl['path'], '/')) + ); + + // Reset paths which only contain the app path + if ($requestUri && !Str::startsWith($requestUri, '/')) { + $requestUri = $server['REQUEST_URI']; + } + + $server['REQUEST_URI'] = $requestUri ?: '/'; + } + + return new Request($query, $request, $attributes, $cookies, $files, $server, $content); + } + /** * Set the trusted Proxies * diff --git a/tests/Unit/Http/RequestServiceProviderTest.php b/tests/Unit/Http/RequestServiceProviderTest.php index 07dab086..b0bd5b29 100644 --- a/tests/Unit/Http/RequestServiceProviderTest.php +++ b/tests/Unit/Http/RequestServiceProviderTest.php @@ -15,7 +15,7 @@ class RequestServiceProviderTest extends ServiceProviderTest /** * @return array */ - public function provideRegister() + public function provideRegister(): array { return [ ['', []], @@ -30,15 +30,16 @@ class RequestServiceProviderTest extends ServiceProviderTest /** * @dataProvider provideRegister - * @covers \Engelsystem\Http\RequestServiceProvider::register() + * @covers \Engelsystem\Http\RequestServiceProvider::register * * @param string|array $configuredProxies * @param array $trustedProxies */ - public function testRegister($configuredProxies, $trustedProxies) + public function testRegister($configuredProxies, array $trustedProxies) { - /** @var Config|MockObject $config */ - $config = $this->getMockBuilder(Config::class)->getMock(); + $config = new Config([ + 'trusted_proxies' => $configuredProxies, + ]); /** @var Request|MockObject $request */ $request = $this->getMockBuilder(Request::class)->getMock(); @@ -46,7 +47,6 @@ class RequestServiceProviderTest extends ServiceProviderTest $this->setExpects($app, 'call', [[Request::class, 'createFromGlobals']], $request); $this->setExpects($app, 'get', ['config'], $config); - $this->setExpects($config, 'get', ['trusted_proxies'], $configuredProxies); $app->expects($this->exactly(3)) ->method('instance') @@ -61,9 +61,67 @@ class RequestServiceProviderTest extends ServiceProviderTest ->setConstructorArgs([$app]) ->onlyMethods(['setTrustedProxies']) ->getMock(); - $serviceProvider->expects($this->once()) - ->method('setTrustedProxies') - ->with($request, $trustedProxies); + $this->setExpects($serviceProvider, 'setTrustedProxies', [$request, $trustedProxies]); $serviceProvider->register(); } + + /** + * @covers \Engelsystem\Http\RequestServiceProvider::register + */ + public function testRegisterRewritingPrefix() + { + $config = new Config([ + 'url' => 'https://some.app/subpath', + ]); + $this->app->instance('config', $config); + $request = new Request(); + + /** @var ServiceProvider|MockObject $serviceProvider */ + $serviceProvider = $this->getMockBuilder(RequestServiceProvider::class) + ->setConstructorArgs([$this->app]) + ->onlyMethods(['createRequestWithoutPrefix']) + ->getMock(); + $this->setExpects($serviceProvider, 'createRequestWithoutPrefix', null, $request); + + $serviceProvider->register(); + } + + /** + * Provide test data: [requested uri; expected rewrite, configured app url] + * + * @return string[][] + */ + public function provideRequestPathPrefix(): array + { + return [ + ['/', '/'], + ['/sub', '/sub'], + ['/subpath2', '/subpath2'], + ['/subpath2/test', '/subpath2/test'], + ['/subpath', '/'], + ['/subpath/', '/'], + ['/subpath/test', '/test'], + ['/subpath/foo/bar', '/foo/bar'], + ['/path/foo/bar', '/foo/bar', 'https://some.app/path/'], + ]; + } + + /** + * @covers \Engelsystem\Http\RequestServiceProvider::createRequestWithoutPrefix + * @dataProvider provideRequestPathPrefix + */ + public function testCreateRequestWithoutPrefix(string $requestUri, string $expected, string $url = null) + { + $_SERVER['REQUEST_URI'] = $requestUri; + $config = new Config([ + 'url' => $url ?: 'https://some.app/subpath', + ]); + $this->app->instance('config', $config); + $serviceProvider = new RequestServiceProvider($this->app); + $serviceProvider->register(); + + /** @var Request $request */ + $request = $this->app->get('request'); + $this->assertEquals($expected, $request->getPathInfo()); + } }