Skip to content

Commit bfe9996

Browse files
committed
feat: #117 rebuild caddy config once the service has been deleted
1 parent 735797f commit bfe9996

File tree

5 files changed

+275
-248
lines changed

5 files changed

+275
-248
lines changed

Diff for: _ide_helper_actions.php

+12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@
1414
* @method static mixed run(\App\Models\User $user, \App\Models\Node $node, string $advertiseAddr)
1515
*/
1616
class InitCluster {}
17+
/**
18+
* @method static \Lorisleiva\Actions\Decorators\JobDecorator|\Lorisleiva\Actions\Decorators\UniqueJobDecorator makeJob(\App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
19+
* @method static \Lorisleiva\Actions\Decorators\UniqueJobDecorator makeUniqueJob(\App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
20+
* @method static \Illuminate\Foundation\Bus\PendingDispatch dispatch(\App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
21+
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchIf(bool $boolean, \App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
22+
* @method static \Illuminate\Foundation\Bus\PendingDispatch|\Illuminate\Support\Fluent dispatchUnless(bool $boolean, \App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
23+
* @method static dispatchSync(\App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
24+
* @method static dispatchNow(\App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
25+
* @method static dispatchAfterResponse(\App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
26+
* @method static mixed run(\App\Models\Team $team, \App\Models\NodeTaskGroup $taskGroup, \App\Models\Deployment $deployment)
27+
*/
28+
class RebuildCaddy {}
1729

1830
namespace App\Actions\Services;
1931

Diff for: app/Actions/Nodes/RebuildCaddy.php

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
<?php
2+
3+
namespace App\Actions\Nodes;
4+
5+
use App\Models\Deployment;
6+
use App\Models\DeploymentData\Caddy;
7+
use App\Models\DeploymentData\EnvVar;
8+
use App\Models\DeploymentData\Process;
9+
use App\Models\NodeTaskGroup;
10+
use App\Models\NodeTasks\ApplyCaddyConfig\ApplyCaddyConfigMeta;
11+
use App\Models\NodeTaskType;
12+
use App\Models\Team;
13+
use InvalidArgumentException;
14+
use Lorisleiva\Actions\Action;
15+
16+
class RebuildCaddy extends Action
17+
{
18+
// TODO: make the deployment optional? Remove it altogether?
19+
public function handle(Team $team, NodeTaskGroup $taskGroup, Deployment $deployment)
20+
{
21+
$caddyHandlers = [];
22+
23+
Deployment::latestDeployments($team)->each(function ($deployment) use (&$caddyHandlers) {
24+
foreach ($deployment->data->processes as $process) {
25+
$caddyHandlers[] = collect($process->caddy)->map(function (Caddy $caddy) use ($deployment, $process) {
26+
$routes = [];
27+
28+
$handlers = [];
29+
30+
$pathRegexps = [];
31+
foreach ($process->rewriteRules as $rewriteRule) {
32+
$pathRegexps[] = [
33+
'find' => $rewriteRule->pathFrom,
34+
'replace' => $rewriteRule->pathTo,
35+
];
36+
}
37+
38+
if (! empty($pathRegexps)) {
39+
$handlers[] = [
40+
'handler' => 'rewrite',
41+
'path_regexp' => $pathRegexps,
42+
];
43+
}
44+
45+
$handlers[] = [
46+
'handler' => 'reverse_proxy',
47+
'headers' => [
48+
'request' => [
49+
'set' => $this->getForwardedHeaders($caddy),
50+
],
51+
'response' => [
52+
'set' => [
53+
'X-Powered-By' => ['https://ptah.sh'],
54+
'X-Ptah-Rule-Id' => [$caddy->id],
55+
],
56+
],
57+
],
58+
'transport' => $this->getTransportOptions($caddy, $process),
59+
'upstreams' => [
60+
[
61+
'dial' => "{$process->name}.{$deployment->data->internalDomain}:{$caddy->targetPort}",
62+
],
63+
],
64+
];
65+
66+
$routes[] = [
67+
'match' => [
68+
[
69+
'host' => [$caddy->domain],
70+
'path' => [$caddy->path],
71+
],
72+
],
73+
'handle' => $handlers,
74+
];
75+
76+
foreach ($process->redirectRules as $redirectRule) {
77+
$regexpName = dockerize_name($redirectRule->id);
78+
79+
$pathTo = preg_replace("/\\$(\d+)/", "{http.regexp.$regexpName.$1}", $redirectRule->pathTo);
80+
81+
$routes[] = [
82+
'match' => [
83+
[
84+
'host' => [$redirectRule->domainFrom],
85+
'path_regexp' => [
86+
'name' => $regexpName,
87+
'pattern' => $redirectRule->pathFrom,
88+
],
89+
],
90+
],
91+
'handle' => [
92+
[
93+
'handler' => 'static_response',
94+
'status_code' => (string) $redirectRule->statusCode,
95+
'headers' => [
96+
'X-Powered-By' => ['https://ptah.sh'],
97+
'X-Ptah-Rule-Id' => [$redirectRule->id],
98+
'Location' => ["{http.request.scheme}://{$redirectRule->domainTo}{$pathTo}"],
99+
],
100+
],
101+
],
102+
];
103+
}
104+
105+
return [
106+
'apps' => [
107+
'http' => [
108+
'servers' => [
109+
"listen_{$caddy->publishedPort}" => [
110+
'listen' => [
111+
"0.0.0.0:{$caddy->publishedPort}",
112+
],
113+
'routes' => $routes,
114+
],
115+
],
116+
],
117+
],
118+
];
119+
})->toArray();
120+
}
121+
});
122+
123+
$caddyHandlers = array_merge(...$caddyHandlers);
124+
125+
$caddy = [
126+
'apps' => [
127+
'http' => [
128+
'servers' => (object) [],
129+
],
130+
],
131+
];
132+
133+
foreach ($caddyHandlers as $handler) {
134+
$caddy = array_merge_recursive($caddy, $handler);
135+
}
136+
137+
foreach ($caddy['apps']['http']['servers'] as $name => $value) {
138+
$caddy['apps']['http']['servers'][$name]['listen'] = array_unique($value['listen']);
139+
$caddy['apps']['http']['servers'][$name]['routes'] = $this->sortRoutes($value['routes']);
140+
141+
$caddy['apps']['http']['servers'][$name]['routes'][] = [
142+
'match' => [
143+
[
144+
'host' => ['*'],
145+
'path' => ['/*'],
146+
],
147+
],
148+
'handle' => [
149+
[
150+
'handler' => 'static_response',
151+
'status_code' => '404',
152+
'headers' => [
153+
'X-Powered-By' => ['https://ptah.sh'],
154+
'Content-Type' => ['text/html; charset=utf-8'],
155+
],
156+
'body' => file_get_contents(resource_path('support/caddy/404.html')),
157+
],
158+
],
159+
];
160+
}
161+
162+
$taskGroup->tasks()->create([
163+
'type' => NodeTaskType::ApplyCaddyConfig,
164+
'meta' => ApplyCaddyConfigMeta::from([
165+
'deploymentId' => $deployment->id,
166+
]),
167+
'payload' => [
168+
'caddy' => $caddy,
169+
],
170+
]);
171+
}
172+
173+
protected function getTransportOptions(Caddy $caddy, Process $process): array
174+
{
175+
if ($caddy->targetProtocol === 'http') {
176+
return [
177+
'protocol' => 'http',
178+
];
179+
}
180+
181+
if ($caddy->targetProtocol === 'fastcgi') {
182+
return [
183+
'protocol' => 'fastcgi',
184+
'root' => $process->fastCgi->root,
185+
'env' => (object) collect($process->fastCgi->env)->reduce(fn ($carry, EnvVar $var) => [...$carry, $var->name => $var->value], []),
186+
];
187+
}
188+
189+
throw new InvalidArgumentException("Unsupported Caddy target protocol: {$caddy->targetProtocol}");
190+
}
191+
192+
protected function getForwardedHeaders(Caddy $caddy): array
193+
{
194+
if ($caddy->publishedPort === 80) {
195+
return [
196+
'X-Forwarded-Proto' => ['http'],
197+
'X-Forwarded-Schema' => ['http'],
198+
'X-Forwarded-Host' => [$caddy->domain],
199+
'X-Forwarded-Port' => ['80'],
200+
];
201+
}
202+
203+
return [
204+
'X-Forwarded-Proto' => ['https'],
205+
'X-Forwarded-Schema' => ['https'],
206+
'X-Forwarded-Host' => [$caddy->domain],
207+
'X-Forwarded-Port' => ['443'],
208+
];
209+
}
210+
211+
protected function sortRoutes($routes): array
212+
{
213+
return collect($routes)
214+
->sort(function (array $a, array $b) {
215+
$weightsA = $this->getRouteWeights($a);
216+
$weightsB = $this->getRouteWeights($b);
217+
218+
$segmentsResult = $weightsA['segments'] <=> $weightsB['segments'];
219+
if ($segmentsResult === 0) {
220+
return $weightsA['wildcards'] <=> $weightsB['wildcards'];
221+
}
222+
223+
return $segmentsResult;
224+
})
225+
->values()
226+
->toArray();
227+
}
228+
229+
protected function getRouteWeights($route): array
230+
{
231+
// Let's prioritize regexp routes to be first to execute
232+
if (isset($route['match'][0]['path_regexp'])) {
233+
return [
234+
'segments' => -100,
235+
'wildcards' => 100,
236+
];
237+
}
238+
239+
$path = $route['match'][0]['path'][0];
240+
241+
$pathSegments = count(explode('/', $path)) * -1;
242+
$wildcardSegments = count(explode('*', $path));
243+
244+
return [
245+
'segments' => $pathSegments,
246+
'wildcards' => $wildcardSegments,
247+
];
248+
}
249+
}

Diff for: app/Actions/Services/StartDeployment.php

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace App\Actions\Services;
44

5+
use App\Actions\Nodes\RebuildCaddy;
56
use App\Models\Deployment;
67
use App\Models\DeploymentData;
78
use App\Models\NodeTaskGroup;
@@ -45,6 +46,8 @@ public function handle(User $user, Service $service, DeploymentData $deploymentD
4546

4647
$taskGroup->tasks()->createMany($deployment->asNodeTasks());
4748

49+
RebuildCaddy::run($service->team, $taskGroup, $deployment);
50+
4851
return $deployment;
4952
});
5053
}

0 commit comments

Comments
 (0)