k8s: Use traefik as default ingress, added improved app url handling to support subpath deployments

This commit is contained in:
Igor Scheller 2022-02-12 01:16:50 +01:00
parent f2c9e4a3d6
commit 20e389fccd
6 changed files with 142 additions and 20 deletions

View File

@ -311,8 +311,12 @@ deploy:
# CI_ENVIRONMENT_URL is the URL configured in the GitLab environment # 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_ENVIRONMENT_URL="${CI_ENVIRONMENT_URL:-https://${CI_PROJECT_PATH_SLUG}.${KUBE_INGRESS_BASE_DOMAIN}/}"
- export CI_IMAGE=$RELEASE_IMAGE - 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_DOMAIN=$(echo "$CI_ENVIRONMENT_URL" | grep -oP '(?:https?://)?\K([^/]+)' | head -n1)
- export CI_INGRESS_PATH=$(echo "$CI_ENVIRONMENT_URL" | grep -oP '(?:https?://)?(?:[^/])+\K(.*)') - 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 - export CI_KUBE_NAMESPACE=$KUBE_NAMESPACE
# Any available storage class like default, local-path (if you know what you are doing ;), longhorn etc. # 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}"} - export CI_PVC_SC=${CI_PVC_SC:-"${CI_PVC_SC_LOCAL:-local-path}"}
@ -328,6 +332,7 @@ deploy:
done done
- echo "Deploying to ${CI_ENVIRONMENT_URL}" - echo "Deploying to ${CI_ENVIRONMENT_URL}"
- kubectl diff -f deployment.yaml || true
- kubectl apply -f deployment.yaml - kubectl apply -f deployment.yaml
- >- - >-
kubectl -n $CI_KUBE_NAMESPACE wait --for=condition=Ready pods --timeout=${CI_WAIT_TIMEOUT:-5}m kubectl -n $CI_KUBE_NAMESPACE wait --for=condition=Ready pods --timeout=${CI_WAIT_TIMEOUT:-5}m

View File

@ -66,7 +66,6 @@ metadata:
labels: labels:
app: <CI_PROJECT_PATH_SLUG> app: <CI_PROJECT_PATH_SLUG>
environment: <CI_ENVIRONMENT_SLUG> environment: <CI_ENVIRONMENT_SLUG>
commit: '<CI_COMMIT_SHORT_SHA>'
spec: spec:
type: ClusterIP type: ClusterIP
ports: ports:
@ -105,6 +104,7 @@ spec:
environment: <CI_ENVIRONMENT_SLUG> environment: <CI_ENVIRONMENT_SLUG>
tier: application tier: application
commit: '<CI_COMMIT_SHORT_SHA>' commit: '<CI_COMMIT_SHORT_SHA>'
pipeline: '<CI_PIPELINE_ID>'
annotations: annotations:
app.gitlab.com/app: <CI_PROJECT_PATH_SLUG> app.gitlab.com/app: <CI_PROJECT_PATH_SLUG>
app.gitlab.com/env: <CI_ENVIRONMENT_SLUG> app.gitlab.com/env: <CI_ENVIRONMENT_SLUG>
@ -159,7 +159,6 @@ metadata:
labels: labels:
app: <CI_PROJECT_PATH_SLUG> app: <CI_PROJECT_PATH_SLUG>
environment: <CI_ENVIRONMENT_SLUG> environment: <CI_ENVIRONMENT_SLUG>
commit: '<CI_COMMIT_SHORT_SHA>'
annotations: annotations:
prometheus.io/port: '80' prometheus.io/port: '80'
prometheus.io/scrape: 'true' prometheus.io/scrape: 'true'
@ -181,13 +180,14 @@ metadata:
name: engelsystem-ingress name: engelsystem-ingress
annotations: annotations:
kubernetes.io/tls-acme: 'true' kubernetes.io/tls-acme: 'true'
kubernetes.io/ingress.class: 'nginx' kubernetes.io/ingress.class: <CI_INGRESS_CLASS>
cert-manager.io/cluster-issuer: <CI_CLUSTER_ISSUER> cert-manager.io/cluster-issuer: <CI_CLUSTER_ISSUER>
nginx.ingress.kubernetes.io/rewrite-target: /$1 nginx.ingress.kubernetes.io/rewrite-target: /$1
traefik.ingress.kubernetes.io/router.entrypoints: <CI_INGRESS_TRAEFIK_ENTRYPOINT>
traefik.ingress.kubernetes.io/router.tls: 'true'
labels: labels:
app: <CI_PROJECT_PATH_SLUG> app: <CI_PROJECT_PATH_SLUG>
environment: <CI_ENVIRONMENT_SLUG> environment: <CI_ENVIRONMENT_SLUG>
commit: '<CI_COMMIT_SHORT_SHA>'
spec: spec:
tls: tls:
- hosts: - hosts:
@ -197,7 +197,7 @@ spec:
- host: <CI_INGRESS_DOMAIN> - host: <CI_INGRESS_DOMAIN>
http: http:
paths: paths:
- path: '<CI_INGRESS_PATH>/?(.*)' - path: '<CI_INGRESS_PATH><CI_INGRESS_MATCH>'
pathType: Prefix pathType: Prefix
backend: backend:
service: service:

View File

@ -1,13 +1,19 @@
#!/usr/bin/env sh #!/usr/bin/env sh
set -e set -e
nginx -g 'daemon off;'&
# If first arg starts with a `-` or is empty # If first arg starts with a `-` or is empty
if [[ "${1#-}" != "${1}" ]] || [[ -z "${1}" ]]; then if [[ "${1#-}" != "${1}" ]] || [[ -z "${1}" ]]; then
set -- php-fpm "$@" set -- php-fpm "$@"
fi 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() { function get_name() {
echo "$1" | cut -d: -f1 echo "$1" | cut -d: -f1
} }
@ -37,4 +43,6 @@ if [[ -n "${RUN_USER}" ]]; then
echo "Running as $user:$group" echo "Running as $user:$group"
fi fi
nginx -g 'daemon off;'&
exec "$@" exec "$@"

View File

@ -21,7 +21,7 @@ http {
server { server {
include mime.types; include mime.types;
access_log off; access_log /dev/stdout;
listen [::]:80 ipv6only=off; listen [::]:80 ipv6only=off;
proxy_redirect off; proxy_redirect off;
proxy_set_header Host $host; proxy_set_header Host $host;
@ -32,7 +32,7 @@ http {
root /var/www/public; root /var/www/public;
location / { location / {
try_files $uri $uri/ /index.php?$args; try_files $uri $uri/ /index.php$is_args$args;
} }
location ~ \.php$ { location ~ \.php$ {

View File

@ -2,20 +2,31 @@
namespace Engelsystem\Http; namespace Engelsystem\Http;
use Engelsystem\Config\Config;
use Engelsystem\Container\ServiceProvider; use Engelsystem\Container\ServiceProvider;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest; use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class RequestServiceProvider extends ServiceProvider class RequestServiceProvider extends ServiceProvider
{ {
/** @var array */
protected array $appUrl;
public function register() public function register()
{ {
/** @var Config $config */
$config = $this->app->get('config'); $config = $this->app->get('config');
$trustedProxies = $config->get('trusted_proxies', []); $trustedProxies = $config->get('trusted_proxies', []);
$this->appUrl = parse_url($config->get('url') ?: '');
if (!is_array($trustedProxies)) { if (!is_array($trustedProxies)) {
$trustedProxies = empty($trustedProxies) ? [] : explode(',', preg_replace('~\s+~', '', $trustedProxies)); $trustedProxies = empty($trustedProxies) ? [] : explode(',', preg_replace('~\s+~', '', $trustedProxies));
} }
if (!empty($this->appUrl['path'])) {
Request::setFactory([$this, 'createRequestWithoutPrefix']);
}
/** @var Request $request */ /** @var Request $request */
$request = $this->app->call([Request::class, 'createFromGlobals']); $request = $this->app->call([Request::class, 'createFromGlobals']);
$this->setTrustedProxies($request, $trustedProxies); $this->setTrustedProxies($request, $trustedProxies);
@ -25,6 +36,46 @@ class RequestServiceProvider extends ServiceProvider
$this->app->instance('request', $request); $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 * Set the trusted Proxies
* *

View File

@ -15,7 +15,7 @@ class RequestServiceProviderTest extends ServiceProviderTest
/** /**
* @return array * @return array
*/ */
public function provideRegister() public function provideRegister(): array
{ {
return [ return [
['', []], ['', []],
@ -30,15 +30,16 @@ class RequestServiceProviderTest extends ServiceProviderTest
/** /**
* @dataProvider provideRegister * @dataProvider provideRegister
* @covers \Engelsystem\Http\RequestServiceProvider::register() * @covers \Engelsystem\Http\RequestServiceProvider::register
* *
* @param string|array $configuredProxies * @param string|array $configuredProxies
* @param array $trustedProxies * @param array $trustedProxies
*/ */
public function testRegister($configuredProxies, $trustedProxies) public function testRegister($configuredProxies, array $trustedProxies)
{ {
/** @var Config|MockObject $config */ $config = new Config([
$config = $this->getMockBuilder(Config::class)->getMock(); 'trusted_proxies' => $configuredProxies,
]);
/** @var Request|MockObject $request */ /** @var Request|MockObject $request */
$request = $this->getMockBuilder(Request::class)->getMock(); $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, 'call', [[Request::class, 'createFromGlobals']], $request);
$this->setExpects($app, 'get', ['config'], $config); $this->setExpects($app, 'get', ['config'], $config);
$this->setExpects($config, 'get', ['trusted_proxies'], $configuredProxies);
$app->expects($this->exactly(3)) $app->expects($this->exactly(3))
->method('instance') ->method('instance')
@ -61,9 +61,67 @@ class RequestServiceProviderTest extends ServiceProviderTest
->setConstructorArgs([$app]) ->setConstructorArgs([$app])
->onlyMethods(['setTrustedProxies']) ->onlyMethods(['setTrustedProxies'])
->getMock(); ->getMock();
$serviceProvider->expects($this->once()) $this->setExpects($serviceProvider, 'setTrustedProxies', [$request, $trustedProxies]);
->method('setTrustedProxies')
->with($request, $trustedProxies);
$serviceProvider->register(); $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());
}
} }