Skip to content

Commit effd671

Browse files
committed
IBX-10764: Upsun env var loader
1 parent 5ea10ed commit effd671

File tree

7 files changed

+424
-0
lines changed

7 files changed

+424
-0
lines changed

src/bundle/DependencyInjection/IbexaCloudExtension.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ public function prepend(ContainerBuilder $container): void
3838
{
3939
$this->prependDefaultConfiguration($container);
4040
$this->prependJMSTranslation($container);
41+
42+
if (($_SERVER['HTTPCACHE_PURGE_TYPE'] ?? $_ENV['HTTPCACHE_PURGE_TYPE'] ?? null) === 'varnish') {
43+
$container->setParameter('ibexa.http_cache.purge_type', 'varnish');
44+
}
45+
46+
// Adapt config based on enabled PHP extensions
47+
// Get imagine to use imagick if enabled, to avoid using php memory for image conversions
48+
// Cannot be placed as env var due to how LiipImagineBundle processes its config
49+
if (\extension_loaded('imagick')) {
50+
$container->setParameter('liip_imagine_driver', 'imagick');
51+
}
4152
}
4253

4354
private function prependDefaultConfiguration(ContainerBuilder $container): void
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ibexa\Bundle\Cloud\DependencyInjection;
6+
7+
use Ibexa\Bundle\Core\Session\Handler\NativeSessionHandler;
8+
use JsonException;
9+
use Symfony\Component\DependencyInjection\EnvVarLoaderInterface;
10+
11+
final class UpsunEnvVarLoader implements EnvVarLoaderInterface
12+
{
13+
private const string MYSQL_DEFAULT_DATABASE_CHARSET = 'utf8mb4';
14+
15+
private const string PGSQL_DEFAULT_DATABASE_CHARSET = 'utf8';
16+
17+
private const string DEFAULT_DATABASE_COLLATION = 'utf8mb4_unicode_520_ci';
18+
19+
public function loadEnvVars(): array
20+
{
21+
$relationshipsEncoded = $_SERVER['PLATFORM_RELATIONSHIPS'] ?? null;
22+
$routesEncoded = $_SERVER['PLATFORM_ROUTES'] ?? null;
23+
24+
if ($relationshipsEncoded === null || $routesEncoded === null) {
25+
return [];
26+
}
27+
28+
$relationships = $this->decodePayload($relationshipsEncoded);
29+
30+
$routes = $this->decodePayload($routesEncoded);
31+
32+
if ($relationships === null || $routes === null) {
33+
return [];
34+
}
35+
36+
return array_filter(
37+
array_merge(
38+
$this->buildDfsEnvVars($relationships),
39+
$this->buildCacheEnvVars($relationships),
40+
$this->buildSessionEnvVars($relationships),
41+
$this->buildSearchEnvVars($relationships),
42+
$this->buildVarnishEnvVars($routes),
43+
),
44+
static fn (string|int|null $value): bool => $value !== null && $value !== ''
45+
);
46+
}
47+
48+
/**
49+
* @param array<string, array<array<string, mixed>>> $relationships
50+
*
51+
* @return array<string, string>
52+
*/
53+
private function buildDfsEnvVars(array $relationships): array
54+
{
55+
$dfsPath = $_SERVER['PLATFORMSH_DFS_NFS_PATH'] ?? null;
56+
if ($dfsPath === null) {
57+
return [];
58+
}
59+
60+
$envVars = [
61+
$this->envKey('dfs_nfs_path') => $dfsPath,
62+
$this->envKey('dfs_database_charset') => $_SERVER['DATABASE_CHARSET']
63+
?? self::MYSQL_DEFAULT_DATABASE_CHARSET,
64+
$this->envKey('dfs_database_collation') => $_SERVER['DATABASE_COLLATION']
65+
?? self::DEFAULT_DATABASE_COLLATION,
66+
];
67+
68+
if (isset($relationships['dfs_database'])) {
69+
foreach ($relationships['dfs_database'] as $endpoint) {
70+
if (empty($endpoint['query']['is_master'])) {
71+
continue;
72+
}
73+
74+
$pdoDriver = $this->normalizePdoDriver((string) ($endpoint['scheme'] ?? ''));
75+
$envVars[$this->envKey('dfs_database_driver')] = $pdoDriver;
76+
77+
// If driver is PGSQL, charset has to be set to utf8
78+
if ($pdoDriver === 'pdo_pgsql') {
79+
$envVars[$this->envKey('dfs_database_charset')] = self::PGSQL_DEFAULT_DATABASE_CHARSET;
80+
}
81+
82+
$envVars[$this->envKey('dfs_database_url')] = sprintf(
83+
'%s://%s:%s@%s:%d/%s',
84+
$endpoint['scheme'],
85+
$endpoint['username'],
86+
$endpoint['password'],
87+
$endpoint['host'],
88+
$endpoint['port'],
89+
ltrim((string) $endpoint['path'], '/')
90+
);
91+
92+
break;
93+
}
94+
} else {
95+
$driver = $this->guessRepositoryDriver();
96+
if ($driver !== null) {
97+
$envVars[$this->envKey('dfs_database_driver')] = $driver;
98+
}
99+
}
100+
101+
return $envVars;
102+
}
103+
104+
/**
105+
* @param array<string, array<array<string, mixed>>> $relationships
106+
*
107+
* @return array<string, string>
108+
*/
109+
private function buildCacheEnvVars(array $relationships): array
110+
{
111+
if (isset($relationships['rediscache'])) {
112+
foreach ($relationships['rediscache'] as $endpoint) {
113+
if (($endpoint['scheme'] ?? null) !== 'redis') {
114+
continue;
115+
}
116+
117+
return [
118+
$this->envKey('cache_pool') => 'cache.redis',
119+
$this->envKey('cache_dsn') => sprintf(
120+
'%s:%d?retry_interval=3',
121+
$endpoint['host'],
122+
$endpoint['port'],
123+
),
124+
];
125+
}
126+
}
127+
128+
if (isset($relationships['cache'])) {
129+
foreach ($relationships['cache'] as $endpoint) {
130+
if (($endpoint['scheme'] ?? null) !== 'memcached') {
131+
continue;
132+
}
133+
134+
@trigger_error('Usage of Memcached is deprecated, redis is recommended', \E_USER_DEPRECATED);
135+
136+
return [
137+
$this->envKey('cache_pool') => 'cache.memcached',
138+
$this->envKey('cache_dsn') => sprintf('%s:%d', $endpoint['host'], $endpoint['port']),
139+
];
140+
}
141+
}
142+
143+
return [];
144+
}
145+
146+
/**
147+
* @param array<string, array<array<string, mixed>>> $relationships
148+
*
149+
* @return array<string, string>
150+
*/
151+
private function buildSessionEnvVars(array $relationships): array
152+
{
153+
$endpoints = $relationships['redissession'] ?? $relationships['rediscache'] ?? null;
154+
if ($endpoints === null) {
155+
return [];
156+
}
157+
158+
foreach ($endpoints as $endpoint) {
159+
if (($endpoint['scheme'] ?? null) !== 'redis') {
160+
continue;
161+
}
162+
163+
return [
164+
$this->envKey('session_handler_id') => NativeSessionHandler::class,
165+
$this->envKey('session_save_path') => sprintf(
166+
'%s:%d',
167+
$endpoint['host'],
168+
$endpoint['port'],
169+
),
170+
];
171+
}
172+
173+
return [];
174+
}
175+
176+
/**
177+
* @param array<string, array<array<string, mixed>>> $relationships
178+
*
179+
* @return array<string, string>
180+
*/
181+
private function buildSearchEnvVars(array $relationships): array
182+
{
183+
$envVars = [];
184+
185+
if (isset($relationships['solr'])) {
186+
foreach ($relationships['solr'] as $endpoint) {
187+
if (($endpoint['scheme'] ?? null) !== 'solr') {
188+
continue;
189+
}
190+
191+
$envVars[$this->envKey('search_engine')] = 'solr';
192+
$envVars[$this->envKey('solr_dsn')] = sprintf(
193+
'http://%s:%d/%s',
194+
$endpoint['host'],
195+
$endpoint['port'],
196+
'solr'
197+
);
198+
$envVars[$this->envKey('solr_core')] = substr((string) $endpoint['path'], 5);
199+
}
200+
}
201+
202+
if (isset($relationships['elasticsearch'])) {
203+
foreach ($relationships['elasticsearch'] as $endpoint) {
204+
$dsn = sprintf('%s:%d', $endpoint['host'], $endpoint['port']);
205+
206+
if (($endpoint['username'] ?? null) !== null && ($endpoint['password'] ?? null) !== null) {
207+
$dsn = $endpoint['username'] . ':' . $endpoint['password'] . '@' . $dsn;
208+
}
209+
210+
if (($endpoint['path'] ?? null) !== null) {
211+
$dsn .= '/' . ltrim((string) $endpoint['path'], '/');
212+
}
213+
214+
$dsn = $endpoint['scheme'] . '://' . $dsn;
215+
216+
$envVars[$this->envKey('search_engine')] = 'elasticsearch';
217+
$envVars[$this->envKey('elasticsearch_dsn')] = $dsn;
218+
}
219+
}
220+
221+
return $envVars;
222+
}
223+
224+
/**
225+
* @param array<string, array<string, mixed>> $routes
226+
*
227+
* @return array<string, string>
228+
*/
229+
private function buildVarnishEnvVars(array $routes): array
230+
{
231+
$envVars = [];
232+
$varnishRoute = null;
233+
234+
foreach ($routes as $host => $info) {
235+
if ($varnishRoute === null && $this->isVarnishRoute($info)) {
236+
$varnishRoute = $host;
237+
}
238+
239+
if ($this->isVarnishRoute($info) && ($info['primary'] ?? false) === true) {
240+
$varnishRoute = $host;
241+
break;
242+
}
243+
}
244+
245+
if ($varnishRoute !== null && !($_SERVER['SKIP_HTTPCACHE_PURGE'] ?? false)) {
246+
$purgeServer = rtrim($varnishRoute, '/');
247+
$username = $_SERVER['HTTPCACHE_USERNAME'] ?? null;
248+
$password = $_SERVER['HTTPCACHE_PASSWORD'] ?? null;
249+
250+
if ($username !== null && $password !== null) {
251+
$domain = parse_url($purgeServer, PHP_URL_HOST);
252+
if (\is_string($domain) && $domain !== '') {
253+
$credentials = rawurlencode($username) . ':' . rawurlencode($password);
254+
$purgeServer = str_replace($domain, $credentials . '@' . $domain, $purgeServer);
255+
}
256+
}
257+
258+
$envVars[$this->envKey('httpcache_purge_type')] = 'varnish';
259+
$envVars[$this->envKey('httpcache_purge_server')] = $purgeServer;
260+
}
261+
262+
$envVars[$this->envKey('httpcache_varnish_invalidate_token')] = $_SERVER['HTTPCACHE_VARNISH_INVALIDATE_TOKEN']
263+
?? $_SERVER['PLATFORM_PROJECT_ENTROPY']
264+
?? '';
265+
266+
return $envVars;
267+
}
268+
269+
/**
270+
* @return array<string, mixed>|null
271+
*/
272+
private function decodePayload(string $payload): ?array
273+
{
274+
$decoded = base64_decode($payload, true);
275+
if ($decoded === false) {
276+
return null;
277+
}
278+
279+
try {
280+
/** @var array<string, mixed> $data */
281+
return json_decode($decoded, true, 512, JSON_THROW_ON_ERROR);
282+
} catch (JsonException) {
283+
return null;
284+
}
285+
}
286+
287+
private function normalizePdoDriver(string $scheme): string
288+
{
289+
if ($scheme === '') {
290+
return '';
291+
}
292+
293+
return str_starts_with($scheme, 'pdo_') ? $scheme : 'pdo_' . $scheme;
294+
}
295+
296+
private function guessRepositoryDriver(): ?string
297+
{
298+
$explicit = $this->getFirstNonEmptyEnv('DATABASE_DRIVER');
299+
if ($explicit !== null) {
300+
return $explicit;
301+
}
302+
303+
$databaseUrl = $this->getFirstNonEmptyEnv('DATABASE_URL');
304+
if ($databaseUrl === null) {
305+
return null;
306+
}
307+
308+
$scheme = parse_url($databaseUrl, PHP_URL_SCHEME);
309+
if (!\is_string($scheme) || $scheme === '') {
310+
return null;
311+
}
312+
313+
return $this->normalizePdoDriver($scheme);
314+
}
315+
316+
private function envKey(string $parameterName): string
317+
{
318+
return strtoupper(str_replace(['.', '-'], '_', $parameterName));
319+
}
320+
321+
/**
322+
* @param array<string, mixed> $route
323+
*/
324+
private function isVarnishRoute(array $route): bool
325+
{
326+
return ($route['type'] ?? null) === 'upstream' && ($route['upstream'] ?? null) === 'varnish';
327+
}
328+
329+
private function getFirstNonEmptyEnv(string $name): ?string
330+
{
331+
$value = $_SERVER[$name] ?? $_ENV[$name] ?? null;
332+
$value = $value === '' ? null : $value;
333+
334+
return \is_string($value) ? $value : null;
335+
}
336+
}

0 commit comments

Comments
 (0)