mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-18 12:33:06 +00:00
Compare commits
49 Commits
v4.0.0-bet
...
v4.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efd2899ae3 | ||
|
|
e4b2195932 | ||
|
|
0590ed7b2e | ||
|
|
3a3c9448a4 | ||
|
|
36d65ad5a8 | ||
|
|
8db66952e8 | ||
|
|
45fa88ca4d | ||
|
|
84b74f0b57 | ||
|
|
423cf62d92 | ||
|
|
c4d9deabef | ||
|
|
776b1cb68d | ||
|
|
fc3025398e | ||
|
|
457c16c4dc | ||
|
|
ccf63c67e8 | ||
|
|
945157b30c | ||
|
|
13798392be | ||
|
|
0d05b0a3d6 | ||
|
|
e0d2f88d99 | ||
|
|
e260bfae02 | ||
|
|
5abd4a6d78 | ||
|
|
9dff1e5631 | ||
|
|
02332ade1b | ||
|
|
486de58d5b | ||
|
|
606aeb2b61 | ||
|
|
3fc264560c | ||
|
|
3dd9182281 | ||
|
|
c838ff7198 | ||
|
|
ca6db9c1a9 | ||
|
|
f27e00e80e | ||
|
|
60cf296f31 | ||
|
|
ea64e9d5ad | ||
|
|
55846c5635 | ||
|
|
7763594e6e | ||
|
|
6b5339c1c1 | ||
|
|
f2980738e4 | ||
|
|
f0e3ad0461 | ||
|
|
187050e098 | ||
|
|
9e7823795d | ||
|
|
239459dfa8 | ||
|
|
ce0f560c44 | ||
|
|
95baec99dd | ||
|
|
363e8fc0b5 | ||
|
|
e49caba920 | ||
|
|
30db2b2a09 | ||
|
|
285666e181 | ||
|
|
003934ee1d | ||
|
|
44c7958aa6 | ||
|
|
35b1a81dfe | ||
|
|
e40f397cc7 |
22
.tinkerwell/snippets/DeleteUser.php
Normal file
22
.tinkerwell/snippets/DeleteUser.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
$email = 'test@example.com';
|
||||
$user = User::whereEmail($email)->first();
|
||||
$teams = $user->teams;
|
||||
foreach ($teams as $team) {
|
||||
$servers = $team->servers;
|
||||
if ($servers->count() > 0) {
|
||||
foreach ($servers as $server) {
|
||||
dump($server);
|
||||
$server->delete();
|
||||
}
|
||||
}
|
||||
dump($team);
|
||||
$team->delete();
|
||||
}
|
||||
if ($user) {
|
||||
dump($user);
|
||||
$user->delete();
|
||||
}
|
||||
@@ -129,6 +129,7 @@ class StartDatabaseProxy
|
||||
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
|
||||
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
|
||||
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
|
||||
"docker compose --project-directory {$configuration_dir} pull",
|
||||
"docker compose --project-directory {$configuration_dir} up --build -d",
|
||||
], $server);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ class StartMariadb
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '####### {$database->name} started.'";
|
||||
return remote_process($this->commands, $database->destination->server);
|
||||
|
||||
@@ -107,6 +107,8 @@ class StartMongodb
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '####### {$database->name} started.'";
|
||||
return remote_process($this->commands, $database->destination->server);
|
||||
|
||||
@@ -91,6 +91,8 @@ class StartMysql
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '####### {$database->name} started.'";
|
||||
return remote_process($this->commands, $database->destination->server);
|
||||
|
||||
@@ -116,6 +116,8 @@ class StartPostgresql
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '####### {$database->name} started.'";
|
||||
return remote_process($this->commands, $database->destination->server);
|
||||
|
||||
@@ -101,6 +101,8 @@ class StartRedis
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
|
||||
$readme = generate_readme_file($this->database->name, now());
|
||||
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
|
||||
$this->commands[] = "echo 'Pulling {$database->image} image.'";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
|
||||
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
|
||||
$this->commands[] = "echo '####### {$database->name} started.'";
|
||||
return remote_process($this->commands, $database->destination->server);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Exceptions;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Sentry\Laravel\Integration;
|
||||
use Sentry\State\Scope;
|
||||
@@ -40,6 +41,13 @@ class Handler extends ExceptionHandler
|
||||
];
|
||||
private InstanceSettings $settings;
|
||||
|
||||
protected function unauthenticated($request, AuthenticationException $exception)
|
||||
{
|
||||
if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) {
|
||||
return response()->json(['message' => $exception->getMessage()], 401);
|
||||
}
|
||||
return redirect()->guest($exception->redirectTo() ?? route('login'));
|
||||
}
|
||||
/**
|
||||
* Register the exception handling callbacks for the application.
|
||||
*/
|
||||
@@ -47,6 +55,7 @@ class Handler extends ExceptionHandler
|
||||
{
|
||||
$this->reportable(function (Throwable $e) {
|
||||
if (isDev()) {
|
||||
ray($e);
|
||||
return;
|
||||
}
|
||||
$this->settings = InstanceSettings::get();
|
||||
|
||||
@@ -7,15 +7,39 @@ use Livewire\Component;
|
||||
class StackForm extends Component
|
||||
{
|
||||
public $service;
|
||||
public $fields = [];
|
||||
protected $listeners = ["saveCompose"];
|
||||
protected $rules = [
|
||||
public $rules = [
|
||||
'service.docker_compose_raw' => 'required',
|
||||
'service.docker_compose' => 'required',
|
||||
'service.name' => 'required',
|
||||
'service.description' => 'nullable',
|
||||
];
|
||||
public $validationAttributes = [];
|
||||
public function mount()
|
||||
{
|
||||
$extraFields = $this->service->extraFields();
|
||||
foreach ($extraFields as $serviceName => $fields) {
|
||||
foreach ($fields as $fieldKey => $field) {
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$rules = data_get($field, 'rules');
|
||||
$isPassword = data_get($field, 'isPassword');
|
||||
$this->fields[$key] = [
|
||||
"serviceName" => $serviceName,
|
||||
"key" => $key,
|
||||
"name" => $fieldKey,
|
||||
"value" => $value,
|
||||
"isPassword" => $isPassword,
|
||||
];
|
||||
$this->rules["fields.$key.value"] = $rules;
|
||||
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
public function saveCompose($raw)
|
||||
{
|
||||
|
||||
$this->service->docker_compose_raw = $raw;
|
||||
$this->submit();
|
||||
}
|
||||
@@ -25,6 +49,7 @@ class StackForm extends Component
|
||||
try {
|
||||
$this->validate();
|
||||
$this->service->save();
|
||||
$this->service->saveExtraFields($this->fields);
|
||||
$this->service->parse();
|
||||
$this->service->refresh();
|
||||
$this->service->saveComposeConfigs();
|
||||
|
||||
@@ -8,9 +8,27 @@ class Webhooks extends Component
|
||||
{
|
||||
public $resource;
|
||||
public ?string $deploywebhook = null;
|
||||
public ?string $githubManualWebhook = null;
|
||||
public ?string $gitlabManualWebhook = null;
|
||||
protected $rules = [
|
||||
'resource.manual_webhook_secret_github' => 'nullable|string',
|
||||
'resource.manual_webhook_secret_gitlab' => 'nullable|string',
|
||||
];
|
||||
public function saveSecret()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->resource->save();
|
||||
$this->emit('success','Secret Saved.');
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function mount()
|
||||
{
|
||||
$this->deploywebhook = generateDeployWebhook($this->resource);
|
||||
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
|
||||
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
|
||||
17
app/Http/Livewire/Sponsorship.php
Normal file
17
app/Http/Livewire/Sponsorship.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class Sponsorship extends Component
|
||||
{
|
||||
public function disable()
|
||||
{
|
||||
auth()->user()->update(['is_notification_sponsorship_enabled' => false]);
|
||||
}
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.sponsorship');
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
private StandaloneDocker|SwarmDocker $destination;
|
||||
private Server $server;
|
||||
private ?ApplicationPreview $preview = null;
|
||||
private ?string $git_type = null;
|
||||
|
||||
private string $container_name;
|
||||
private ?string $currently_running_container_name = null;
|
||||
@@ -99,6 +100,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->force_rebuild = $this->application_deployment_queue->force_rebuild;
|
||||
$this->restart_only = $this->application_deployment_queue->restart_only;
|
||||
|
||||
$this->git_type = data_get($this->application_deployment_queue, 'git_type');
|
||||
|
||||
$source = data_get($this->application, 'source');
|
||||
if ($source) {
|
||||
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
|
||||
@@ -119,11 +122,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
|
||||
if ($this->application->fqdn) {
|
||||
if (data_get($this->preview, 'fqdn')) {
|
||||
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
|
||||
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
|
||||
} else {
|
||||
$url = Url::fromString($this->application->fqdn);
|
||||
if (data_get($this->preview, 'fqdn')) {
|
||||
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
|
||||
}
|
||||
}
|
||||
$template = $this->application->preview_url_template;
|
||||
$url = Url::fromString($this->application->fqdn);
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$random = new Cuid2(7);
|
||||
@@ -213,12 +221,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
} finally {
|
||||
if (isset($this->docker_compose_base64)) {
|
||||
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
|
||||
$composeFileName = "$this->configuration_dir/docker-compose.yml";
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
"mkdir -p $this->configuration_dir"
|
||||
],
|
||||
[
|
||||
"echo '{$this->docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml",
|
||||
"echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
|
||||
],
|
||||
[
|
||||
"echo '{$readme}' > $this->configuration_dir/README.md",
|
||||
@@ -344,7 +356,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$this->prepare_builder_image();
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile")
|
||||
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location")
|
||||
],
|
||||
);
|
||||
$this->generate_image_names();
|
||||
@@ -647,7 +659,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
|
||||
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"));
|
||||
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name"));
|
||||
}
|
||||
return $commands->implode(' && ');
|
||||
}
|
||||
@@ -659,14 +671,28 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
throw new Exception('Private key not found. Please add a private key to the application and try again.');
|
||||
}
|
||||
$private_key = base64_encode($private_key);
|
||||
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
|
||||
$git_clone_command = $this->set_git_import_settings($git_clone_command);
|
||||
$git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
|
||||
$git_clone_command = $this->set_git_import_settings($git_clone_command_base);
|
||||
$commands = collect([
|
||||
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
|
||||
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
|
||||
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
|
||||
executeInDocker($this->deployment_uuid, $git_clone_command)
|
||||
]);
|
||||
if ($this->pull_request_id !== 0) {
|
||||
ray($this->git_type);
|
||||
if ($this->git_type === 'gitlab') {
|
||||
$this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name";
|
||||
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
|
||||
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
|
||||
}
|
||||
if ($this->git_type === 'github') {
|
||||
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
|
||||
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
|
||||
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
|
||||
}
|
||||
}
|
||||
|
||||
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
|
||||
return $commands->implode(' && ');
|
||||
}
|
||||
if ($this->application->deploymentType() === 'other') {
|
||||
@@ -770,21 +796,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
$labels = collect(generateLabelsApplication($this->application, $this->preview));
|
||||
}
|
||||
if ($this->pull_request_id !== 0) {
|
||||
$newLabels = collect(generateLabelsApplication($this->application, $this->preview));
|
||||
$newHostLabel = $newLabels->filter(function ($label) {
|
||||
return str($label)->contains('Host');
|
||||
});
|
||||
$labels = $labels->reject(function ($label) {
|
||||
return str($label)->contains('Host');
|
||||
});
|
||||
$labels = collect(generateLabelsApplication($this->application, $this->preview));
|
||||
|
||||
$labels = $labels->map(function ($label) {
|
||||
$pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
|
||||
$replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
|
||||
$newLabel = preg_replace($pattern, $replacement, $label);
|
||||
return $newLabel;
|
||||
});
|
||||
$labels = $labels->merge($newHostLabel);
|
||||
// $newHostLabel = $newLabels->filter(function ($label) {
|
||||
// return str($label)->contains('Host');
|
||||
// });
|
||||
// $labels = $labels->reject(function ($label) {
|
||||
// return str($label)->contains('Host');
|
||||
// });
|
||||
// ray($labels,$newLabels);
|
||||
// $labels = $labels->map(function ($label) {
|
||||
// $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
|
||||
// $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
|
||||
// $newLabel = preg_replace($pattern, $replacement, $label);
|
||||
// return $newLabel;
|
||||
// });
|
||||
// $labels = $labels->merge($newHostLabel);
|
||||
}
|
||||
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
|
||||
$docker_compose = [
|
||||
@@ -934,7 +961,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
return implode(' ', $generated_healthchecks_commands);
|
||||
}
|
||||
private function pull_latest_image($image)
|
||||
{
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Pulling latest image ($image) from the registry.'"],
|
||||
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
|
||||
]
|
||||
);
|
||||
}
|
||||
private function build_image()
|
||||
{
|
||||
if ($this->application->build_pack === 'static') {
|
||||
@@ -948,6 +984,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
|
||||
}
|
||||
|
||||
if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
|
||||
if ($this->application->static_image) {
|
||||
$this->pull_latest_image($this->application->static_image);
|
||||
}
|
||||
if ($this->application->build_pack === 'static') {
|
||||
$dockerfile = base64_encode("FROM {$this->application->static_image}
|
||||
WORKDIR /usr/share/nginx/html/
|
||||
@@ -974,7 +1013,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}");
|
||||
} else {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
|
||||
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
|
||||
$dockerfile = base64_encode("FROM {$this->application->static_image}
|
||||
@@ -1012,8 +1051,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
]
|
||||
);
|
||||
} else {
|
||||
// Pure Dockerfile based deployment
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
|
||||
executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1049,6 +1089,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
|
||||
private function start_by_compose_file()
|
||||
{
|
||||
if (
|
||||
!$this->application->dockerfile &&
|
||||
(
|
||||
$this->application->build_pack === 'dockerimage' ||
|
||||
$this->application->build_pack === 'dockerfile')
|
||||
) {
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Pulling latest images from the registry.'"],
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir}"), "hidden" => true],
|
||||
);
|
||||
}
|
||||
$this->execute_remote_command(
|
||||
["echo -n 'Starting application (could take a while).'"],
|
||||
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
|
||||
@@ -1074,7 +1125,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
private function add_build_env_variables_to_dockerfile()
|
||||
{
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
|
||||
]);
|
||||
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
|
||||
|
||||
@@ -1083,7 +1134,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
}
|
||||
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/{$this->dockerfile_location}"),
|
||||
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"),
|
||||
"hidden" => true
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -255,6 +255,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($projectUuid && $serviceUuid && $environmentName) {
|
||||
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
$exitedService->update(['status' => 'exited']);
|
||||
@@ -279,6 +281,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($projectUuid && $applicationUuid && $environment) {
|
||||
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
@@ -302,6 +306,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($projectUuid && $applicationUuid && $environmentName) {
|
||||
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
@@ -325,6 +331,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
|
||||
|
||||
if ($projectUuid && $databaseUuid && $environmentName) {
|
||||
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
|
||||
} else {
|
||||
$url = null;
|
||||
}
|
||||
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
@@ -141,21 +141,20 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
|
||||
} else if ($databaseType === 'standalone-mariadb') {
|
||||
$this->container_name = "{$this->database->name}-$serviceUuid";
|
||||
$this->directory_name = $serviceName . '-' . $this->container_name;
|
||||
$commands[] = "docker exec $this->container_name env | grep MARIADB_";
|
||||
$commands[] = "docker exec $this->container_name env";
|
||||
$envs = instant_remote_process($commands, $this->server);
|
||||
$envs = str($envs)->explode("\n");
|
||||
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MARIADB_ROOT_PASSWORD=');
|
||||
})->first();
|
||||
if ($rootPassword) {
|
||||
$this->database->mysql_root_password = str($rootPassword)->after('MARIADB_ROOT_PASSWORD=')->value();
|
||||
$this->database->mariadb_root_password = str($rootPassword)->after('MARIADB_ROOT_PASSWORD=')->value();
|
||||
} else {
|
||||
$rootPassword = $envs->filter(function ($env) {
|
||||
return str($env)->startsWith('MYSQL_ROOT_PASSWORD=');
|
||||
})->first();
|
||||
if ($rootPassword) {
|
||||
$this->database->mysql_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
|
||||
$this->database->mariadb_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,18 @@ class Application extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function gitWebhook(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
|
||||
return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
|
||||
}
|
||||
return $this->git_repository;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function gitCommits(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
||||
@@ -45,7 +45,228 @@ class Service extends BaseModel
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
public function extraFields()
|
||||
{
|
||||
$fields = collect([]);
|
||||
$applications = $this->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
$image = str($application->image)->before(':')->value();
|
||||
switch ($image) {
|
||||
case str($image)->contains('minio'):
|
||||
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
|
||||
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
|
||||
$fields->put('MinIO', [
|
||||
'Console URL' => [
|
||||
'key' => data_get($console_url, 'key'),
|
||||
'value' => data_get($console_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
'S3 API URL' => [
|
||||
'key' => data_get($s3_api_url, 'key'),
|
||||
'value' => data_get($s3_api_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
'Admin User' => [
|
||||
'key' => data_get($admin_user, 'key'),
|
||||
'value' => data_get($admin_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
'Admin Password' => [
|
||||
'key' => data_get($admin_password, 'key'),
|
||||
'value' => data_get($admin_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
break;
|
||||
case str($image)->contains('weblate'):
|
||||
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
|
||||
$fields->put('Weblate', [
|
||||
'Admin Email' => [
|
||||
'key' => data_get($admin_email, 'key'),
|
||||
'value' => data_get($admin_email, 'value'),
|
||||
'rules' => 'required|email',
|
||||
],
|
||||
'Admin Password' => [
|
||||
'key' => data_get($admin_password, 'key'),
|
||||
'value' => data_get($admin_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
$databases = $this->databases()->get();
|
||||
|
||||
foreach ($databases as $database) {
|
||||
$image = str($database->image)->before(':')->value();
|
||||
switch ($image) {
|
||||
case str($image)->contains('postgres'):
|
||||
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
|
||||
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
|
||||
$postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$data = collect([]);
|
||||
if ($postgres_user) {
|
||||
$data = $data->merge([
|
||||
'User' => [
|
||||
'key' => data_get($postgres_user, 'key'),
|
||||
'value' => data_get($postgres_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($postgres_password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($postgres_password, 'key'),
|
||||
'value' => data_get($postgres_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($postgres_db_name) {
|
||||
$data = $data->merge([
|
||||
'Database Name' => [
|
||||
'key' => data_get($postgres_db_name, 'key'),
|
||||
'value' => data_get($postgres_db_name, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('PostgreSQL', $data->toArray());
|
||||
break;
|
||||
case str($image)->contains('mysql'):
|
||||
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
|
||||
$dbNameVariables = ['MYSQL_DATABASE'];
|
||||
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
|
||||
$mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$data = collect([]);
|
||||
if ($mysql_user) {
|
||||
$data = $data->merge([
|
||||
'User' => [
|
||||
'key' => data_get($mysql_user, 'key'),
|
||||
'value' => data_get($mysql_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mysql_password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($mysql_password, 'key'),
|
||||
'value' => data_get($mysql_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mysql_root_password) {
|
||||
$data = $data->merge([
|
||||
'Root Password' => [
|
||||
'key' => data_get($mysql_root_password, 'key'),
|
||||
'value' => data_get($mysql_root_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mysql_db_name) {
|
||||
$data = $data->merge([
|
||||
'Database Name' => [
|
||||
'key' => data_get($mysql_db_name, 'key'),
|
||||
'value' => data_get($mysql_db_name, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('MySQL', $data->toArray());
|
||||
break;
|
||||
case str($image)->contains('mariadb'):
|
||||
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
|
||||
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
|
||||
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
|
||||
$mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$data = collect([]);
|
||||
|
||||
if ($mariadb_user) {
|
||||
$data = $data->merge([
|
||||
'User' => [
|
||||
'key' => data_get($mariadb_user, 'key'),
|
||||
'value' => data_get($mariadb_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mariadb_password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($mariadb_password, 'key'),
|
||||
'value' => data_get($mariadb_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mariadb_root_password) {
|
||||
$data = $data->merge([
|
||||
'Root Password' => [
|
||||
'key' => data_get($mariadb_root_password, 'key'),
|
||||
'value' => data_get($mariadb_root_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($mariadb_db_name) {
|
||||
$data = $data->merge([
|
||||
'Database Name' => [
|
||||
'key' => data_get($mariadb_db_name, 'key'),
|
||||
'value' => data_get($mariadb_db_name, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('MariaDB', $data->toArray());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
public function saveExtraFields($fields)
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$found = $this->environment_variables()->where('key', $key)->first();
|
||||
if ($found) {
|
||||
$found->value = $value;
|
||||
$found->save();
|
||||
} else {
|
||||
$this->environment_variables()->create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'is_build_time' => false,
|
||||
'service_id' => $this->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function documentation()
|
||||
{
|
||||
$services = getServiceTemplates();
|
||||
@@ -257,7 +478,7 @@ class Service extends BaseModel
|
||||
}
|
||||
}
|
||||
$networks = collect();
|
||||
foreach ($serviceNetworks as $key =>$serviceNetwork) {
|
||||
foreach ($serviceNetworks as $key => $serviceNetwork) {
|
||||
if (gettype($serviceNetwork) === 'string') {
|
||||
// networks:
|
||||
// - appwrite
|
||||
@@ -268,7 +489,7 @@ class Service extends BaseModel
|
||||
// ipv4_address: 192.168.203.254
|
||||
// $networks->put($serviceNetwork, null);
|
||||
ray($key);
|
||||
$networks->put($key,$serviceNetwork);
|
||||
$networks->put($key, $serviceNetwork);
|
||||
}
|
||||
}
|
||||
foreach ($definedNetwork as $key => $network) {
|
||||
@@ -453,15 +674,31 @@ class Service extends BaseModel
|
||||
'service_id' => $this->id,
|
||||
])->first();
|
||||
if ($value->startsWith('SERVICE_')) {
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
$forService = $value->afterLast('_');
|
||||
$generatedValue = null;
|
||||
// Count _ in $value
|
||||
$count = substr_count($value->value(), '_');
|
||||
if ($count === 2) {
|
||||
// SERVICE_FQDN_UMAMI
|
||||
$command = $value->after('SERVICE_')->beforeLast('_');
|
||||
$forService = $value->afterLast('_');
|
||||
$generatedValue = null;
|
||||
$port = null;
|
||||
}
|
||||
if ($count === 3) {
|
||||
// SERVICE_FQDN_UMAMI_1000
|
||||
$command = $value->after('SERVICE_')->before('_');
|
||||
$forService = $value->after('SERVICE_')->after('_')->before('_');
|
||||
$generatedValue = null;
|
||||
$port = $value->afterLast('_');
|
||||
}
|
||||
if ($command->value() === 'FQDN' || $command->value() === 'URL') {
|
||||
if (Str::lower($forService) === $serviceName) {
|
||||
$fqdn = generateFqdn($this->server, $containerName);
|
||||
} else {
|
||||
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
|
||||
}
|
||||
if ($port) {
|
||||
$fqdn = "$fqdn:$port";
|
||||
}
|
||||
if ($foundEnv) {
|
||||
$fqdn = data_get($foundEnv, 'value');
|
||||
} else {
|
||||
@@ -477,7 +714,7 @@ class Service extends BaseModel
|
||||
]);
|
||||
}
|
||||
if (!$isDatabase) {
|
||||
if ($command->value() === 'FQDN') {
|
||||
if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
|
||||
$savedService->fqdn = $fqdn;
|
||||
$savedService->save();
|
||||
}
|
||||
@@ -548,7 +785,11 @@ class Service extends BaseModel
|
||||
}
|
||||
|
||||
// Add labels to the service
|
||||
$fqdns = collect(data_get($savedService, 'fqdns'));
|
||||
if ($savedService->serviceType()) {
|
||||
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
|
||||
} else {
|
||||
$fqdns = collect(data_get($savedService, 'fqdns'));
|
||||
}
|
||||
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
|
||||
$serviceLabels = $serviceLabels->merge($defaultLabels);
|
||||
if (!$isDatabase && $fqdns->count() > 0) {
|
||||
|
||||
@@ -22,6 +22,16 @@ class ServiceApplication extends BaseModel
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
public function serviceType()
|
||||
{
|
||||
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
|
||||
return str($this->image)->before(':')->value() === $service;
|
||||
})->first());
|
||||
if ($found->isNotEmpty()) {
|
||||
return $found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function service()
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
|
||||
@@ -20,6 +20,10 @@ class ServiceDatabase extends BaseModel
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
public function serviceType()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
public function databaseType()
|
||||
{
|
||||
$image = str($this->image)->before(':');
|
||||
@@ -28,8 +32,8 @@ class ServiceDatabase extends BaseModel
|
||||
}
|
||||
return "standalone-$image";
|
||||
}
|
||||
public function getServiceDatabaseUrl() {
|
||||
// $type = $this->databaseType();
|
||||
public function getServiceDatabaseUrl()
|
||||
{
|
||||
$port = $this->public_port;
|
||||
$realIp = $this->service->server->ip;
|
||||
if ($realIp === 'host.docker.internal' || isDev()) {
|
||||
|
||||
@@ -29,6 +29,10 @@ class EmailChannel
|
||||
->html((string)$mailMessage->render())
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$error = $e->getMessage();
|
||||
if ($error === 'No email settings found.') {
|
||||
throw $e;
|
||||
}
|
||||
ray($e->getMessage());
|
||||
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
|
||||
if (isset($recepients)) {
|
||||
|
||||
@@ -16,22 +16,28 @@ class Links extends Component
|
||||
{
|
||||
$this->links = collect([]);
|
||||
$service->applications()->get()->map(function ($application) {
|
||||
if ($application->fqdn) {
|
||||
$fqdns = collect(Str::of($application->fqdn)->explode(','));
|
||||
$fqdns->map(function ($fqdn) {
|
||||
$this->links->push(getFqdnWithoutPort($fqdn));
|
||||
});
|
||||
}
|
||||
if ($application->ports) {
|
||||
$portsCollection = collect(Str::of($application->ports)->explode(','));
|
||||
$portsCollection->map(function ($port) {
|
||||
if (Str::of($port)->contains(':')) {
|
||||
$hostPort = Str::of($port)->before(':');
|
||||
} else {
|
||||
$hostPort = $port;
|
||||
}
|
||||
$this->links->push(base_url(withPort:false) . ":{$hostPort}");
|
||||
});
|
||||
$type = $application->serviceType();
|
||||
if ($type) {
|
||||
$links = generateServiceSpecificFqdns($application, false);
|
||||
$this->links = $this->links->merge($links);
|
||||
} else {
|
||||
if ($application->fqdn) {
|
||||
$fqdns = collect(Str::of($application->fqdn)->explode(','));
|
||||
$fqdns->map(function ($fqdn) {
|
||||
$this->links->push(getFqdnWithoutPort($fqdn));
|
||||
});
|
||||
}
|
||||
if ($application->ports) {
|
||||
$portsCollection = collect(Str::of($application->ports)->explode(','));
|
||||
$portsCollection->map(function ($port) {
|
||||
if (Str::of($port)->contains(':')) {
|
||||
$hostPort = Str::of($port)->before(':');
|
||||
} else {
|
||||
$hostPort = $port;
|
||||
}
|
||||
$this->links->push(base_url(withPort: false) . ":{$hostPort}");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
|
||||
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false)
|
||||
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
|
||||
{
|
||||
$deployment = ApplicationDeploymentQueue::create([
|
||||
'application_id' => $application_id,
|
||||
@@ -14,6 +14,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
|
||||
'is_webhook' => $is_webhook,
|
||||
'restart_only' => $restart_only,
|
||||
'commit' => $commit,
|
||||
'git_type' => $git_type
|
||||
]);
|
||||
$queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at');
|
||||
$running_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'in_progress')->get()->sortByDesc('created_at');
|
||||
|
||||
@@ -23,3 +23,6 @@ const DATABASE_DOCKER_IMAGES = [
|
||||
'influxdb',
|
||||
'clickhouse/clickhouse-server'
|
||||
];
|
||||
const SPECIFIC_SERVICES = [
|
||||
'quay.io/minio/minio',
|
||||
];
|
||||
|
||||
@@ -144,6 +144,42 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
function generateServiceSpecificFqdns($service, $forTraefik = false)
|
||||
{
|
||||
$variables = collect($service->service->environment_variables);
|
||||
$type = $service->serviceType();
|
||||
$payload = collect([]);
|
||||
switch ($type) {
|
||||
case $type->contains('minio'):
|
||||
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
|
||||
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
|
||||
return $payload;
|
||||
}
|
||||
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
|
||||
$MINIO_BROWSER_REDIRECT_URL?->update([
|
||||
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
|
||||
]);
|
||||
}
|
||||
if (is_null($MINIO_SERVER_URL?->value)) {
|
||||
$MINIO_SERVER_URL?->update([
|
||||
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
|
||||
]);
|
||||
}
|
||||
if ($forTraefik) {
|
||||
$payload = collect([
|
||||
$MINIO_BROWSER_REDIRECT_URL->value . ':9001',
|
||||
$MINIO_SERVER_URL->value . ':9000',
|
||||
]);
|
||||
} else {
|
||||
$payload = collect([
|
||||
$MINIO_BROWSER_REDIRECT_URL->value,
|
||||
$MINIO_SERVER_URL->value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
|
||||
{
|
||||
$labels = collect([]);
|
||||
|
||||
@@ -50,8 +50,11 @@ function generate_github_jwt_token(GithubApp $source)
|
||||
return $issuedToken;
|
||||
}
|
||||
|
||||
function githubApi(GithubApp|GitlabApp $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true)
|
||||
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true)
|
||||
{
|
||||
if (is_null($source)) {
|
||||
throw new \Exception('Not implemented yet.');
|
||||
}
|
||||
if ($source->getMorphClass() == 'App\Models\GithubApp') {
|
||||
if ($source->is_public) {
|
||||
$response = Http::github($source->api_url)->$method($endpoint);
|
||||
|
||||
@@ -85,7 +85,6 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
|
||||
} else {
|
||||
$fileLocation = $path;
|
||||
}
|
||||
ray($path,$fileLocation);
|
||||
// Exists and is a file
|
||||
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
|
||||
// Exists and is a directory
|
||||
@@ -135,19 +134,21 @@ function updateCompose($resource)
|
||||
$image = data_get($resource, 'image');
|
||||
data_set($dockerCompose, "services.{$name}.image", $image);
|
||||
|
||||
// Update FQDN
|
||||
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
|
||||
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
|
||||
if ($generatedEnv) {
|
||||
$generatedEnv->value = $resource->fqdn;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
|
||||
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
|
||||
if ($generatedEnv) {
|
||||
$url = Str::of($resource->fqdn)->after('://');
|
||||
$generatedEnv->value = $url;
|
||||
$generatedEnv->save();
|
||||
if (!str($resource->fqdn)->contains(',')) {
|
||||
// Update FQDN
|
||||
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
|
||||
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
|
||||
if ($generatedEnv) {
|
||||
$generatedEnv->value = $resource->fqdn;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
|
||||
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
|
||||
if ($generatedEnv) {
|
||||
$url = Str::of($resource->fqdn)->after('://');
|
||||
$generatedEnv->value = $url;
|
||||
$generatedEnv->save();
|
||||
}
|
||||
}
|
||||
|
||||
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
|
||||
|
||||
@@ -27,7 +27,6 @@ use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Stringable;
|
||||
use Nubs\RandomNameGenerator\All;
|
||||
use Poliander\Cron\CronExpression;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
use phpseclib3\Crypt\RSA;
|
||||
@@ -173,7 +172,11 @@ function get_latest_version_of_coolify(): string
|
||||
|
||||
function generate_random_name(?string $cuid = null): string
|
||||
{
|
||||
$generator = All::create();
|
||||
$generator = new \Nubs\RandomNameGenerator\All(
|
||||
[
|
||||
new \Nubs\RandomNameGenerator\Alliteration(),
|
||||
]
|
||||
);
|
||||
if (is_null($cuid)) {
|
||||
$cuid = new Cuid2(7);
|
||||
}
|
||||
@@ -444,20 +447,25 @@ function getServiceTemplates()
|
||||
if (isDev()) {
|
||||
$services = File::get(base_path('templates/service-templates.json'));
|
||||
$services = collect(json_decode($services))->sortKeys();
|
||||
$version = config('version');
|
||||
$services = $services->map(function ($service) use ($version) {
|
||||
if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
|
||||
$service->disabled = true;
|
||||
}
|
||||
return $service;
|
||||
});
|
||||
} else {
|
||||
$services = Http::get(config('constants.services.official'));
|
||||
if ($services->failed()) {
|
||||
throw new \Exception($services->body());
|
||||
try {
|
||||
$response = Http::retry(3, 50)->get(config('constants.services.official'));
|
||||
if ($response->failed()) {
|
||||
return collect([]);
|
||||
}
|
||||
$services = $response->json();
|
||||
$services = collect($services)->sortKeys();
|
||||
} catch (\Throwable $e) {
|
||||
$services = collect([]);
|
||||
}
|
||||
$services = collect($services->json())->sortKeys();
|
||||
}
|
||||
// $version = config('version');
|
||||
// $services = $services->map(function ($service) use ($version) {
|
||||
// if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
|
||||
// $service->disabled = true;
|
||||
// }
|
||||
// return $service;
|
||||
// });
|
||||
return $services;
|
||||
}
|
||||
|
||||
@@ -493,7 +501,8 @@ function queryResourcesByUuid(string $uuid)
|
||||
return $resource;
|
||||
}
|
||||
|
||||
function generateDeployWebhook($resource) {
|
||||
function generateDeployWebhook($resource)
|
||||
{
|
||||
$baseUrl = base_url();
|
||||
$api = Url::fromString($baseUrl) . '/api/v1';
|
||||
$endpoint = '/deploy';
|
||||
@@ -501,6 +510,18 @@ function generateDeployWebhook($resource) {
|
||||
$url = $api . $endpoint . "?uuid=$uuid&force=false";
|
||||
return $url;
|
||||
}
|
||||
function removeAnsiColors($text) {
|
||||
function generateGitManualWebhook($resource, $type) {
|
||||
if ($resource->source_id !== 0) {
|
||||
return null;
|
||||
}
|
||||
if ($resource->getMorphClass() === 'App\Models\Application') {
|
||||
$baseUrl = base_url();
|
||||
$api = Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual";
|
||||
return $api;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function removeAnsiColors($text)
|
||||
{
|
||||
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
|
||||
// The release version of your application
|
||||
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
|
||||
'release' => '4.0.0-beta.122',
|
||||
'release' => '4.0.0-beta.132',
|
||||
// When left empty or `null` the Laravel environment will be used
|
||||
'environment' => config('app.env'),
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<?php
|
||||
|
||||
return '4.0.0-beta.122';
|
||||
return '4.0.0-beta.132';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->longText('fqdn')->nullable()->change();
|
||||
});
|
||||
Schema::table('application_previews', function (Blueprint $table) {
|
||||
$table->longText('fqdn')->nullable()->change();
|
||||
});
|
||||
Schema::table('service_applications', function (Blueprint $table) {
|
||||
$table->longText('fqdn')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->string('fqdn')->nullable()->change();
|
||||
});
|
||||
Schema::table('application_previews', function (Blueprint $table) {
|
||||
$table->string('fqdn')->nullable()->change();
|
||||
});
|
||||
Schema::table('service_applications', function (Blueprint $table) {
|
||||
$table->string('fqdn')->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_notification_sponsorship_enabled')->default(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_notification_sponsorship_enabled');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->string('manual_webhook_secret_github')->nullable();
|
||||
$table->string('manual_webhook_secret_gitlab')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('applications', function (Blueprint $table) {
|
||||
$table->dropColumn('manual_webhook_secret_github');
|
||||
$table->dropColumn('manual_webhook_secret_gitlab');
|
||||
});
|
||||
}
|
||||
};
|
||||
34
database/migrations/2023_11_14_121416_add_git_type.php
Normal file
34
database/migrations/2023_11_14_121416_add_git_type.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('application_previews', function (Blueprint $table) {
|
||||
$table->string('git_type')->nullable();
|
||||
});
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->string('git_type')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('application_previews', function (Blueprint $table) {
|
||||
$table->dropColumn('git_type');
|
||||
});
|
||||
Schema::table('application_deployment_queues', function (Blueprint $table) {
|
||||
$table->dropColumn('git_type');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -44,6 +44,7 @@ services:
|
||||
- STRIPE_PRICE_ID_PRO_YEARLY
|
||||
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY
|
||||
- STRIPE_PRICE_ID_ULTIMATE_YEARLY
|
||||
- STRIPE_EXCLUDED_PLANS
|
||||
- PADDLE_VENDOR_ID
|
||||
- PADDLE_WEBHOOK_SECRET
|
||||
- PADDLE_VENDOR_AUTH_CODE
|
||||
|
||||
52
package-lock.json
generated
52
package-lock.json
generated
@@ -7,7 +7,7 @@
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"alpinejs": "3.13.1",
|
||||
"daisyui": "3.9.2",
|
||||
"daisyui": "4.0.3",
|
||||
"tailwindcss-scrollbar": "0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -16,7 +16,7 @@
|
||||
"axios": "1.5.1",
|
||||
"laravel-vite-plugin": "0.8.1",
|
||||
"postcss": "8.4.31",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tailwindcss": "3.3.5",
|
||||
"vite": "4.4.11",
|
||||
"vue": "3.3.4"
|
||||
}
|
||||
@@ -896,11 +896,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/colord": {
|
||||
"version": "2.9.3",
|
||||
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
|
||||
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -952,16 +947,23 @@
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/culori": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/culori/-/culori-3.2.0.tgz",
|
||||
"integrity": "sha512-HIEbTSP7vs1mPq/2P9In6QyFE0Tkpevh0k9a+FkjhD+cwsYm9WRSbn4uMdW9O0yXlNYC3ppxL3gWWPOcvEl57w==",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.9.2.tgz",
|
||||
"integrity": "sha512-yJZ1QjHUaL+r9BkquTdzNHb7KIgAJVFh0zbOXql2Wu0r7zx5qZNLxclhjN0WLoIpY+o2h/8lqXg7ijj8oTigOw==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.0.3.tgz",
|
||||
"integrity": "sha512-mG6PsdIA6MEHzdJwBlJxc1rqsIAAlcfhj2O8g0ol1uWg5y6C5zTcqfG8vKBqK4y2YfCxGIVgMsMWRTSm32N1Ow==",
|
||||
"dependencies": {
|
||||
"colord": "^2.9",
|
||||
"css-selector-tokenizer": "^0.8",
|
||||
"postcss": "^8",
|
||||
"postcss-js": "^4",
|
||||
"tailwindcss": "^3.1"
|
||||
"culori": "^3",
|
||||
"picocolors": "^1",
|
||||
"postcss-js": "^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
@@ -1049,9 +1051,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||
"integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
"@nodelib/fs.walk": "^1.2.3",
|
||||
@@ -1281,9 +1283,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz",
|
||||
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
|
||||
"integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -1806,19 +1808,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz",
|
||||
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==",
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
|
||||
"integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.2.12",
|
||||
"fast-glob": "^3.3.0",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.18.2",
|
||||
"jiti": "^1.19.1",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"normalize-path": "^3.0.0",
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
"axios": "1.5.1",
|
||||
"laravel-vite-plugin": "0.8.1",
|
||||
"postcss": "8.4.31",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tailwindcss": "3.3.5",
|
||||
"vite": "4.4.11",
|
||||
"vue": "3.3.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"alpinejs": "3.13.1",
|
||||
"daisyui": "3.9.2",
|
||||
"daisyui": "4.0.3",
|
||||
"tailwindcss-scrollbar": "0.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ a {
|
||||
@apply text-white;
|
||||
}
|
||||
.box {
|
||||
@apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
|
||||
@apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
|
||||
}
|
||||
.box-without-bg {
|
||||
@apply flex p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem];
|
||||
@apply flex p-2 transition-colors h-16 min-h-full hover:text-white hover:no-underline min-h-[4rem];
|
||||
}
|
||||
.description {
|
||||
@apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white;
|
||||
@@ -124,3 +124,6 @@ tr td:first-child {
|
||||
.fullscreen {
|
||||
@apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4 ;
|
||||
}
|
||||
input.input-sm {
|
||||
@apply pr-10;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="relative" x-data>
|
||||
@if ($allowToPeak)
|
||||
<div x-on:click="changePasswordFieldType"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-2 cursor-pointer hover:text-white">
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
href="{{ route('project.service', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
<x-services.links :service="$service" />
|
||||
<x-services.links />
|
||||
<div class="flex-1"></div>
|
||||
@if (serviceStatus($service) === 'degraded')
|
||||
<button wire:click='deploy' onclick="startService.showModal()"
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
<div class="fixed z-50 top-[4.5rem] left-4" id="vue">
|
||||
<magic-bar></magic-bar>
|
||||
</div>
|
||||
<main class="main max-w-screen-2xl">
|
||||
<livewire:sponsorship />
|
||||
<main class="pb-10 main max-w-screen-2xl">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
@endsection
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
services, called resources. Any CPU intensive process will use the server's CPU where you
|
||||
are deploying your resources.</p>
|
||||
<p>Localhost is the server where Coolify is running on. It is not recommended to use one server
|
||||
for everyting.</p>
|
||||
for everything.</p>
|
||||
<p>Remote Server is a server reachable through SSH. It can be hosted at home, or from any cloud
|
||||
provider.</p>
|
||||
</x-slot:explanation>
|
||||
|
||||
@@ -69,10 +69,12 @@
|
||||
<div class="flex flex-col gap-2 xl:flex-row">
|
||||
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
|
||||
helper="Directory to use as root. Useful for monorepos." />
|
||||
@if ($application->build_pack === 'dockerfile')
|
||||
@if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
|
||||
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
|
||||
label="Dockerfile Location"
|
||||
helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" />
|
||||
@endif
|
||||
@if ($application->build_pack === 'dockerfile')
|
||||
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
|
||||
helper="Useful if you have multi-staged dockerfile." />
|
||||
@endif
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
@endif
|
||||
</div>
|
||||
@if ($application->previews->count() > 0)
|
||||
<h4 class="py-4">Deployed Previews</h4>
|
||||
<div class="pb-4">Previews</div>
|
||||
<div class="flex gap-6 ">
|
||||
@foreach ($application->previews as $preview)
|
||||
<div class="flex flex-col p-4 bg-coolgray-200">
|
||||
@@ -71,19 +71,19 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 pt-6">
|
||||
<x-forms.button wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
|
||||
<x-forms.button class="bg-coolgray-500" wire:click="deploy({{ data_get($preview, 'pull_request_id') }})">
|
||||
@if (data_get($preview, 'status') === 'exited')
|
||||
Deploy
|
||||
@else
|
||||
Redeploy
|
||||
@endif
|
||||
</x-forms.button>
|
||||
<x-forms.button wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove
|
||||
<x-forms.button class="bg-coolgray-500" wire:click="stop({{ data_get($preview, 'pull_request_id') }})">Remove
|
||||
Preview
|
||||
</x-forms.button>
|
||||
<a
|
||||
href="{{ route('project.application.deployments', [...$parameters, 'pull_request_id' => data_get($preview, 'pull_request_id')]) }}">
|
||||
<x-forms.button>
|
||||
<x-forms.button class="bg-coolgray-500">
|
||||
Get Deployment Logs
|
||||
</x-forms.button>
|
||||
</a>
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
</button>
|
||||
@endif
|
||||
@empty
|
||||
<div>No service found.</div>
|
||||
<div>No service found. Please try to reload the list!</div>
|
||||
@endforelse
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -16,19 +16,21 @@
|
||||
<x-forms.input label="Description" id="application.description"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if ($application->required_fqdn)
|
||||
<x-forms.input required placeholder="https://app.coolify.io" label="Domains"
|
||||
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@else
|
||||
<x-forms.input placeholder="https://app.coolify.io" label="Domains"
|
||||
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@if (!$application->serviceType()?->contains(str($application->image)->before(':')))
|
||||
@if ($application->required_fqdn)
|
||||
<x-forms.input required placeholder="https://app.coolify.io" label="Domains"
|
||||
id="application.fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@else
|
||||
<x-forms.input placeholder="https://app.coolify.io" label="Domains" id="application.fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@endif
|
||||
@endif
|
||||
<x-forms.input required
|
||||
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||
label="Image" id="application.image"></x-forms.input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="pt-2">Advanced</h3>
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox instantSave label="Exclude from service status"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="w-full pl-8">
|
||||
<div x-cloak x-show="activeTab === 'service-stack'">
|
||||
<livewire:project.service.stack-form :service="$service" />
|
||||
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1">
|
||||
@foreach ($applications as $application)
|
||||
<div @class([
|
||||
'border-l border-dashed border-red-500' => Str::of(
|
||||
@@ -58,7 +58,7 @@
|
||||
@endif
|
||||
<div class="text-xs">{{ $application->status }}</div>
|
||||
</a>
|
||||
<a class="flex gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
|
||||
<a class="flex items-center gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
|
||||
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span
|
||||
class="hover:text-warning">Logs</span></a>
|
||||
</div>
|
||||
@@ -88,7 +88,7 @@
|
||||
@endif
|
||||
<div class="text-xs">{{ $database->status }}</div>
|
||||
</a>
|
||||
<a class="flex gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
|
||||
<a class="flex items-center gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
|
||||
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span
|
||||
class="hover:text-warning">Logs</span></a>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,20 @@
|
||||
File</x-forms.button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input id="service.name" required label="Service Name"
|
||||
placeholder="My super wordpress site" />
|
||||
<x-forms.input id="service.name" required label="Service Name" placeholder="My super wordpress site" />
|
||||
<x-forms.input id="service.description" label="Description" />
|
||||
</div>
|
||||
@if ($fields)
|
||||
<div>
|
||||
<h3>Service Specific Configuration</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
@foreach ($fields as $serviceName => $fields)
|
||||
<x-forms.input type="{{ data_get($fields, 'isPassword') ? 'password' : 'text' }}" required
|
||||
helper="Variable name: {{ $serviceName }}"
|
||||
label="{{ data_get($fields, 'serviceName') }} {{ data_get($fields, 'name') }}"
|
||||
id="fields.{{ $serviceName }}.value"></x-forms.input>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
@@ -1,10 +1,41 @@
|
||||
<div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Webhooks</h2>
|
||||
<x-helper
|
||||
helper="For more details goto our <a class='text-white underline' href='https://coolify.io/docs/api-endpoints' target='_blank'>docs</a>." />
|
||||
</div>
|
||||
<div>
|
||||
<x-forms.input readonly label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input>
|
||||
<x-forms.input readonly
|
||||
helper="See details in our <a target='_blank' class='text-white underline' href='https://coolify.io/docs/api-authentication'>documentation</a>."
|
||||
label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Manual Git Webhooks</h3>
|
||||
@if ($githubManualWebhook && $gitlabManualWebhook)
|
||||
<form wire:submit.prevent='saveSecret' class="flex flex-col gap-2">
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.input helper="Content Type in GitHub configuration could be json or form-urlencoded."
|
||||
readonly label="GitHub" id="githubManualWebhook"></x-forms.input>
|
||||
<x-forms.input type="password"
|
||||
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitHub."
|
||||
label="GitHub Webhook Secret" id="resource.manual_webhook_secret_github"></x-forms.input>
|
||||
|
||||
</div>
|
||||
<a target="_blank" class="flex hover:no-underline" href="{{ $resource?->gitWebhook }}">
|
||||
<x-forms.button>Webhook Configuration on GitHub
|
||||
<x-external-link />
|
||||
</x-forms.button>
|
||||
</a>
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input readonly label="GitLab" id="gitlabManualWebhook"></x-forms.input>
|
||||
<x-forms.input type="password"
|
||||
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab."
|
||||
label="GitLab Webhook Secret" id="resource.manual_webhook_secret_gitlab"></x-forms.input>
|
||||
</div>
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
</form>
|
||||
@else
|
||||
You are using an official Git App. You do not need manual webhooks.
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
14
resources/views/livewire/sponsorship.blade.php
Normal file
14
resources/views/livewire/sponsorship.blade.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<div>
|
||||
@if (data_get(auth()->user(), 'is_notification_sponsorship_enabled'))
|
||||
<div class="toast">
|
||||
<div class="flex flex-col text-white rounded alert bg-coolgray-200">
|
||||
<span>Love Coolify as we do? <a href="https://coolify.io/sponsorships"
|
||||
class="underline text-warning">Please
|
||||
consider donating!</a>💜</span>
|
||||
<span>It enables us to keep creating features without paywalls, ensuring our work remains free and
|
||||
open.</span>
|
||||
<x-forms.button class="bg-coolgray-400" wire:click='disable'>Disable This Popup</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -63,6 +63,244 @@ Route::get('/source/github/install', function () {
|
||||
return handleError($e);
|
||||
}
|
||||
});
|
||||
Route::post('/source/gitlab/events/manual', function () {
|
||||
try {
|
||||
$payload = request()->collect();
|
||||
$headers = request()->headers->all();
|
||||
ray($payload, $headers);
|
||||
$x_gitlab_token = data_get($headers, 'x-gitlab-token.0');
|
||||
$x_gitlab_event = data_get($payload, 'object_kind');
|
||||
if ($x_gitlab_event === 'push') {
|
||||
$branch = data_get($payload, 'ref');
|
||||
$full_name = data_get($payload, 'project.path_with_namespace');
|
||||
if (Str::isMatch('/refs\/heads\/*/', $branch)) {
|
||||
$branch = Str::after($branch, 'refs/heads/');
|
||||
}
|
||||
if (!$branch) {
|
||||
return response('Nothing to do. No branch found in the request.');
|
||||
}
|
||||
ray('Manual Webhook GitLab Push Event with branch: ' . $branch);
|
||||
}
|
||||
if ($x_gitlab_event === 'merge_request') {
|
||||
$action = data_get($payload, 'object_attributes.action');
|
||||
ray($action);
|
||||
$branch = data_get($payload, 'object_attributes.source_branch');
|
||||
$base_branch = data_get($payload, 'object_attributes.target_branch');
|
||||
$full_name = data_get($payload, 'project.path_with_namespace');
|
||||
$pull_request_id = data_get($payload, 'object_attributes.iid');
|
||||
$pull_request_html_url = data_get($payload, 'object_attributes.url');
|
||||
if (!$branch) {
|
||||
return response('Nothing to do. No branch found in the request.');
|
||||
}
|
||||
ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
|
||||
}
|
||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||
if ($x_gitlab_event === 'push') {
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
|
||||
}
|
||||
}
|
||||
if ($x_gitlab_event === 'merge_request') {
|
||||
$applications = $applications->where('git_branch', $base_branch)->get();
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found with branch '$base_branch'.");
|
||||
}
|
||||
}
|
||||
foreach ($applications as $application) {
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
|
||||
if ($webhook_secret !== $x_gitlab_token) {
|
||||
ray('Invalid signature');
|
||||
continue;
|
||||
}
|
||||
$isFunctional = $application->destination->server->isFunctional();
|
||||
if (!$isFunctional) {
|
||||
ray('Server is not functional: ' . $application->destination->server->name);
|
||||
continue;
|
||||
}
|
||||
if ($x_gitlab_event === 'push') {
|
||||
if ($application->isDeployable()) {
|
||||
ray('Deploying ' . $application->name . ' with branch ' . $branch);
|
||||
$deployment_uuid = new Cuid2(7);
|
||||
queue_application_deployment(
|
||||
application_id: $application->id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
is_webhook: true
|
||||
);
|
||||
} else {
|
||||
ray('Deployments disabled for ' . $application->name);
|
||||
}
|
||||
}
|
||||
if ($x_gitlab_event === 'merge_request') {
|
||||
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
|
||||
if ($application->isPRDeployable()) {
|
||||
$deployment_uuid = new Cuid2(7);
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (!$found) {
|
||||
ApplicationPreview::create([
|
||||
'git_type' => 'gitlab',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
}
|
||||
queue_application_deployment(
|
||||
application_id: $application->id,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
is_webhook: true,
|
||||
git_type: 'gitlab'
|
||||
);
|
||||
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
|
||||
return response('Preview Deployment queued.');
|
||||
} else {
|
||||
ray('Preview deployments disabled for ' . $application->name);
|
||||
return response('Nothing to do. Preview Deployments disabled.');
|
||||
}
|
||||
}
|
||||
if ($action === 'closed') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
// ray('Stopping container: ' . $container_name);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
return response('Preview Deployment closed.');
|
||||
}
|
||||
return response('Nothing to do. No Preview Deployment found');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
ray($e->getMessage());
|
||||
return handleError($e);
|
||||
}
|
||||
});
|
||||
Route::post('/source/github/events/manual', function () {
|
||||
try {
|
||||
$x_github_event = Str::lower(request()->header('X-GitHub-Event'));
|
||||
$x_hub_signature_256 = Str::after(request()->header('X-Hub-Signature-256'), 'sha256=');
|
||||
$content_type = request()->header('Content-Type');
|
||||
$payload = request()->collect();
|
||||
if ($x_github_event === 'ping') {
|
||||
// Just pong
|
||||
return response('pong');
|
||||
}
|
||||
|
||||
if ($content_type !== 'application/json') {
|
||||
$payload = json_decode(data_get($payload, 'payload'), true);
|
||||
}
|
||||
if ($x_github_event === 'push') {
|
||||
$branch = data_get($payload, 'ref');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
if (Str::isMatch('/refs\/heads\/*/', $branch)) {
|
||||
$branch = Str::after($branch, 'refs/heads/');
|
||||
}
|
||||
ray('Manual Webhook GitHub Push Event with branch: ' . $branch);
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
$action = data_get($payload, 'action');
|
||||
$full_name = data_get($payload, 'repository.full_name');
|
||||
$pull_request_id = data_get($payload, 'number');
|
||||
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
|
||||
$branch = data_get($payload, 'pull_request.head.ref');
|
||||
$base_branch = data_get($payload, 'pull_request.base.ref');
|
||||
ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
|
||||
}
|
||||
if (!$branch) {
|
||||
return response('Nothing to do. No branch found in the request.');
|
||||
}
|
||||
$applications = Application::where('git_repository', 'like', "%$full_name%");
|
||||
if ($x_github_event === 'push') {
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
|
||||
}
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
$applications = $applications->where('git_branch', $base_branch)->get();
|
||||
if ($applications->isEmpty()) {
|
||||
return response("Nothing to do. No applications found with branch '$base_branch'.");
|
||||
}
|
||||
}
|
||||
ray($applications);
|
||||
foreach ($applications as $application) {
|
||||
ray($application);
|
||||
$webhook_secret = data_get($application, 'manual_webhook_secret_github');
|
||||
ray($webhook_secret);
|
||||
$hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
|
||||
ray($hmac, $x_hub_signature_256);
|
||||
if (!hash_equals($x_hub_signature_256, $hmac)) {
|
||||
ray('Invalid signature');
|
||||
continue;
|
||||
}
|
||||
$isFunctional = $application->destination->server->isFunctional();
|
||||
if (!$isFunctional) {
|
||||
ray('Server is not functional: ' . $application->destination->server->name);
|
||||
continue;
|
||||
}
|
||||
if ($x_github_event === 'push') {
|
||||
if ($application->isDeployable()) {
|
||||
ray('Deploying ' . $application->name . ' with branch ' . $branch);
|
||||
$deployment_uuid = new Cuid2(7);
|
||||
queue_application_deployment(
|
||||
application_id: $application->id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
is_webhook: true
|
||||
);
|
||||
} else {
|
||||
ray('Deployments disabled for ' . $application->name);
|
||||
}
|
||||
}
|
||||
if ($x_github_event === 'pull_request') {
|
||||
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
|
||||
if ($application->isPRDeployable()) {
|
||||
$deployment_uuid = new Cuid2(7);
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (!$found) {
|
||||
ApplicationPreview::create([
|
||||
'git_type' => 'github',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
}
|
||||
queue_application_deployment(
|
||||
application_id: $application->id,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
is_webhook: true,
|
||||
git_type: 'github'
|
||||
);
|
||||
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
|
||||
return response('Preview Deployment queued.');
|
||||
} else {
|
||||
ray('Preview deployments disabled for ' . $application->name);
|
||||
return response('Nothing to do. Preview Deployments disabled.');
|
||||
}
|
||||
}
|
||||
if ($action === 'closed') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
$container_name = generateApplicationContainerName($application, $pull_request_id);
|
||||
// ray('Stopping container: ' . $container_name);
|
||||
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
|
||||
return response('Preview Deployment closed.');
|
||||
}
|
||||
return response('Nothing to do. No Preview Deployment found');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
ray($e->getMessage());
|
||||
return handleError($e);
|
||||
}
|
||||
});
|
||||
Route::post('/source/github/events', function () {
|
||||
try {
|
||||
$id = null;
|
||||
@@ -150,6 +388,7 @@ Route::post('/source/github/events', function () {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (!$found) {
|
||||
ApplicationPreview::create([
|
||||
'git_type' => 'github',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
@@ -160,7 +399,8 @@ Route::post('/source/github/events', function () {
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
force_rebuild: false,
|
||||
is_webhook: true
|
||||
is_webhook: true,
|
||||
git_type: 'github'
|
||||
);
|
||||
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
|
||||
return response('Preview Deployment queued.');
|
||||
@@ -169,7 +409,7 @@ Route::post('/source/github/events', function () {
|
||||
return response('Nothing to do. Preview Deployments disabled.');
|
||||
}
|
||||
}
|
||||
if ($action === 'closed') {
|
||||
if ($action === 'closed' || $action === 'close') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
$found->delete();
|
||||
|
||||
@@ -37,7 +37,7 @@ services:
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- directus-redis-data:/data
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# documentation: https://fider.io/doc
|
||||
# documentation: https://fider.io/docs
|
||||
# slogan: Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services.
|
||||
# tags: feedback, user-feedback
|
||||
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
# tags: backend, api, realtime, websocket, mqtt, rest, sdk, iot, geofencing, low-code
|
||||
|
||||
services:
|
||||
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- elastic-redis-data:/data
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
retries: 30
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
|
||||
elasticsearch:
|
||||
image: kuzzleio/elasticsearch:7
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# ignore: true
|
||||
# documentation: https://docs.min.io/docs/minio-docker-quickstart-guide.html
|
||||
# slogan: MinIO is a high performance object storage server compatible with Amazon S3 APIs.
|
||||
# tags: object, storage, server, s3, api
|
||||
@@ -8,11 +7,8 @@ services:
|
||||
image: quay.io/minio/minio:latest
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
- SERVICE_FQDN_MINIO_9000
|
||||
- SERVICE_FQDN_CONSOLE_9001
|
||||
- MINIO_DOMAIN=$SERVICE_URL_MINIO_9000
|
||||
- MINIO_SERVER_URL=$SERVICE_FQDN_MINIO_9000
|
||||
- MINIO_BROWSER_REDIRECT_URL=$SERVICE_FQDN_CONSOLE_9001
|
||||
- MINIO_SERVER_URL=$MINIO_SERVER_URL
|
||||
- MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL
|
||||
- MINIO_ROOT_USER=$SERVICE_USER_MINIO
|
||||
- MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO
|
||||
volumes:
|
||||
|
||||
50
templates/compose/weblate.yaml
Normal file
50
templates/compose/weblate.yaml
Normal file
@@ -0,0 +1,50 @@
|
||||
# documentation: https://docs.weblate.org/en/latest/admin/install/docker.html
|
||||
# slogan: Weblate is a libre software web-based continuous localization system.
|
||||
# tags: localization, translation, web, web-based, continuous, libre, software
|
||||
|
||||
services:
|
||||
weblate:
|
||||
image: weblate/weblate:latest
|
||||
environment:
|
||||
- SERVICE_FQDN_WEBLATE
|
||||
- WEBLATE_SITE_DOMAIN=$SERVICE_URL_WEBLATE
|
||||
- WEBLATE_ADMIN_NAME=${WEBLATE_ADMIN_NAME:-Admin}
|
||||
- WEBLATE_ADMIN_EMAIL=${WEBLATE_ADMIN_EMAIL:-admin@example.com}
|
||||
- WEBLATE_ADMIN_PASSWORD=$SERVICE_PASSWORD_WEBLATE
|
||||
- DEFAULT_FROM_EMAIL=${WEBLATE_ADMIN_EMAIL:-admin@example.com}
|
||||
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
|
||||
- POSTGRES_USER=$SERVICE_USER_POSTGRES
|
||||
- POSTGRES_DATABASE=${POSTGRES_DB:-weblate}
|
||||
- POSTGRES_HOST=postgresql
|
||||
- POSTGRES_PORT=5432
|
||||
- REDIS_HOST=redis
|
||||
volumes:
|
||||
- weblate-data:/app/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080"]
|
||||
interval: 2s
|
||||
timeout: 10s
|
||||
retries: 15
|
||||
postgresql:
|
||||
image: postgres:15-alpine
|
||||
volumes:
|
||||
- postgresql-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=$SERVICE_USER_POSTGRES
|
||||
- POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES
|
||||
- POSTGRES_DB=${POSTGRES_DB:-weblate}
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
command: redis-server --appendonly yes
|
||||
volumes:
|
||||
- weblate-redis-data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
@@ -68,7 +68,7 @@
|
||||
"directus-with-postgresql": {
|
||||
"documentation": "https:\/\/docs.directus.io\/self-hosted\/quickstart.html",
|
||||
"slogan": "Directus is an open-source tool that wraps custom SQL databases with a dynamic API, and provides an intuitive admin app for managing its content.",
|
||||
"compose": "c2VydmljZXM6CiAgZGlyZWN0dXM6CiAgICBpbWFnZTogJ2RpcmVjdHVzL2RpcmVjdHVzOjEwLjcnCiAgICB2b2x1bWVzOgogICAgICAtICdkaXJlY3R1cy11cGxvYWRzOi9kaXJlY3R1cy91cGxvYWRzJwogICAgICAtICdkaXJlY3R1cy1leHRlbnNpb25zOi9kaXJlY3R1cy9leHRlbnNpb25zJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0RJUkVDVFVTCiAgICAgIC0gS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9LRVkKICAgICAgLSBTRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVAogICAgICAtICdBRE1JTl9FTUFJTD0ke0FETUlOX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIC0gQURNSU5fUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQURNSU4KICAgICAgLSBEQl9DTElFTlQ9cG9zdGdyZXMKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1JUPTU0MzIKICAgICAgLSAnREJfREFUQUJBU0U9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1kaXJlY3R1c30nCiAgICAgIC0gREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUwKICAgICAgLSBEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFdFQlNPQ0tFVFNfRU5BQkxFRD10cnVlCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnZGlyZWN0dXMtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZGlyZWN0dXN9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo2LWFscGluZScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1hcHBlbmRvbmx5IHllcycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RpcmVjdHVzLXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
|
||||
"compose": "c2VydmljZXM6CiAgZGlyZWN0dXM6CiAgICBpbWFnZTogJ2RpcmVjdHVzL2RpcmVjdHVzOjEwLjcnCiAgICB2b2x1bWVzOgogICAgICAtICdkaXJlY3R1cy11cGxvYWRzOi9kaXJlY3R1cy91cGxvYWRzJwogICAgICAtICdkaXJlY3R1cy1leHRlbnNpb25zOi9kaXJlY3R1cy9leHRlbnNpb25zJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0RJUkVDVFVTCiAgICAgIC0gS0VZPSRTRVJWSUNFX0JBU0U2NF82NF9LRVkKICAgICAgLSBTRUNSRVQ9JFNFUlZJQ0VfQkFTRTY0XzY0X1NFQ1JFVAogICAgICAtICdBRE1JTl9FTUFJTD0ke0FETUlOX0VNQUlMOi1hZG1pbkBleGFtcGxlLmNvbX0nCiAgICAgIC0gQURNSU5fUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfQURNSU4KICAgICAgLSBEQl9DTElFTlQ9cG9zdGdyZXMKICAgICAgLSBEQl9IT1NUPXBvc3RncmVzcWwKICAgICAgLSBEQl9QT1JUPTU0MzIKICAgICAgLSAnREJfREFUQUJBU0U9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1kaXJlY3R1c30nCiAgICAgIC0gREJfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTUUwKICAgICAgLSBEQl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtIFdFQlNPQ0tFVFNfRU5BQkxFRD10cnVlCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTUtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnZGlyZWN0dXMtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZGlyZWN0dXN9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LWFscGluZScKICAgIGNvbW1hbmQ6ICdyZWRpcy1zZXJ2ZXIgLS1hcHBlbmRvbmx5IHllcycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2RpcmVjdHVzLXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
|
||||
"tags": [
|
||||
"directus",
|
||||
"cms",
|
||||
@@ -132,7 +132,7 @@
|
||||
]
|
||||
},
|
||||
"fider": {
|
||||
"documentation": "https:\/\/fider.io\/doc",
|
||||
"documentation": "https:\/\/fider.io\/docs",
|
||||
"slogan": "Fider is an open-source feedback platform for collecting and managing user feedback, helping you prioritize improvements to your products and services.",
|
||||
"compose": "c2VydmljZXM6CiAgZmlkZXI6CiAgICBpbWFnZTogJ2dldGZpZGVyL2ZpZGVyOnN0YWJsZScKICAgIGVudmlyb25tZW50OgogICAgICBCQVNFX1VSTDogJFNFUlZJQ0VfRlFETl9GSURFUgogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfTVlTUUw6JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUxAZGF0YWJhc2U6NTQzMi9maWRlcj9zc2xtb2RlPWRpc2FibGUnCiAgICAgIEpXVF9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0ZJREVSCiAgICAgIEVNQUlMX05PUkVQTFk6ICcke0VNQUlMX05PUkVQTFk6LW5vcmVwbHlAZXhhbXBsZS5jb219JwogICAgICBFTUFJTF9NQUlMR1VOX0FQSTogJEVNQUlMX01BSUxHVU5fQVBJCiAgICAgIEVNQUlMX01BSUxHVU5fRE9NQUlOOiAkRU1BSUxfTUFJTEdVTl9ET01BSU4KICAgICAgRU1BSUxfTUFJTEdVTl9SRUdJT046ICRFTUFJTF9NQUlMR1VOX1JFR0lPTgogICAgICBFTUFJTF9TTVRQX0hPU1Q6ICcke0VNQUlMX1NNVFBfSE9TVDotc210cC5tYWlsZ3VuLmNvbX0nCiAgICAgIEVNQUlMX1NNVFBfUE9SVDogJyR7RU1BSUxfU01UUF9QT1JUOi01ODd9JwogICAgICBFTUFJTF9TTVRQX1VTRVJOQU1FOiAnJHtFTUFJTF9TTVRQX1VTRVJOQU1FOi1wb3N0bWFzdGVyQG1haWxndW4uY29tfScKICAgICAgRU1BSUxfU01UUF9QQVNTV09SRDogJEVNQUlMX1NNVFBfUEFTU1dPUkQKICAgICAgRU1BSUxfU01UUF9FTkFCTEVfU1RBUlRUTFM6ICRFTUFJTF9TTVRQX0VOQUJMRV9TVEFSVFRMUwogICAgICBFTUFJTF9BV1NTRVNfUkVHSU9OOiAkRU1BSUxfQVdTU0VTX1JFR0lPTgogICAgICBFTUFJTF9BV1NTRVNfQUNDRVNTX0tFWV9JRDogJEVNQUlMX0FXU1NFU19BQ0NFU1NfS0VZX0lECiAgICAgIEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWTogJEVNQUlMX0FXU1NFU19TRUNSRVRfQUNDRVNTX0tFWQogIGRhdGFiYXNlOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnX2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotZmlkZXJ9Jwo=",
|
||||
"tags": [
|
||||
@@ -275,7 +275,7 @@
|
||||
"kuzzle": {
|
||||
"documentation": "https:\/\/docs.kuzzle.io\/",
|
||||
"slogan": "Kuzzle is a generic backend offering the basic building blocks common to every application.",
|
||||
"compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjYtYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogMXMKICAgICAgdGltZW91dDogM3MKICAgICAgcmV0cmllczogMzAKICBlbGFzdGljc2VhcmNoOgogICAgaW1hZ2U6ICdrdXp6bGVpby9lbGFzdGljc2VhcmNoOjcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6OTIwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDJzCiAgICAgIHJldHJpZXM6IDEwCiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6IDY1NTM2CiAga3V6emxlOgogICAgaW1hZ2U6ICdrdXp6bGVpby9rdXp6bGU6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0tVWlpMRV83NTEyCiAgICAgIC0gJ2t1enpsZV9zZXJ2aWNlc19fc3RvcmFnZUVuZ2luZV9fY2xpZW50X19ub2RlPWh0dHA6Ly9lbGFzdGljc2VhcmNoOjkyMDAnCiAgICAgIC0ga3V6emxlX3NlcnZpY2VzX19zdG9yYWdlRW5naW5lX19jb21tb25NYXBwaW5nX19keW5hbWljPXRydWUKICAgICAgLSBrdXp6bGVfc2VydmljZXNfX2ludGVybmFsQ2FjaGVfX25vZGVfX2hvc3Q9cmVkaXMKICAgICAgLSBrdXp6bGVfc2VydmljZXNfX21lbW9yeVN0b3JhZ2VfX25vZGVfX2hvc3Q9cmVkaXMKICAgICAgLSBrdXp6bGVfc2VydmVyX19wcm90b2NvbHNfX21xdHRfX2VuYWJsZWQ9dHJ1ZQogICAgICAtIGt1enpsZV9zZXJ2ZXJfX3Byb3RvY29sc19fbXF0dF9fZGV2ZWxvcG1lbnRNb2RlPWZhbHNlCiAgICAgIC0ga3V6emxlX2xpbWl0c19fbG9naW5zUGVyU2Vjb25kPTUwCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdERUJVRz0ke0RFQlVHOi1rdXp6bGU6Y2x1c3RlcjpzeW5jfScKICAgICAgLSAnREVCVUdfREVQVEg9JHtERUJVR19ERVBUSDotMH0nCiAgICAgIC0gJ0RFQlVHX01BWF9BUlJBWV9MRU5HVEg9JHtERUJVR19NQVhfQVJSQVk6LTEwMH0nCiAgICAgIC0gJ0RFQlVHX0VYUEFORD0ke0RFQlVHX0VYUEFORDotb2ZmfScKICAgICAgLSAnREVCVUdfU0hPV19ISURERU49eyRERUJVR19TSE9XX0hJRERFTjotb259JwogICAgICAtICdERUJVR19DT0xPUlM9JHtERUJVR19DT0xPUlM6LW9ufScKICAgIGNhcF9hZGQ6CiAgICAgIC0gU1lTX1BUUkFDRQogICAgdWxpbWl0czoKICAgICAgbm9maWxlOiA2NTUzNgogICAgc3lzY3RsczoKICAgICAgLSBuZXQuY29yZS5zb21heGNvbm49ODE5MgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0Ojc1MTIvX2hlYWx0aGNoZWNrJwogICAgICB0aW1lb3V0OiAxcwogICAgICBpbnRlcnZhbDogMnMKICAgICAgcmV0cmllczogMzAKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIGVsYXN0aWNzZWFyY2g6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkK",
|
||||
"compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjctYWxwaW5lJwogICAgY29tbWFuZDogJ3JlZGlzLXNlcnZlciAtLWFwcGVuZG9ubHkgeWVzJwogICAgdm9sdW1lczoKICAgICAgLSAnZWxhc3RpYy1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgZWxhc3RpY3NlYXJjaDoKICAgIGltYWdlOiAna3V6emxlaW8vZWxhc3RpY3NlYXJjaDo3JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjkyMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAycwogICAgICByZXRyaWVzOiAxMAogICAgdWxpbWl0czoKICAgICAgbm9maWxlOiA2NTUzNgogIGt1enpsZToKICAgIGltYWdlOiAna3V6emxlaW8va3V6emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LVVpaTEVfNzUxMgogICAgICAtICdrdXp6bGVfc2VydmljZXNfX3N0b3JhZ2VFbmdpbmVfX2NsaWVudF9fbm9kZT1odHRwOi8vZWxhc3RpY3NlYXJjaDo5MjAwJwogICAgICAtIGt1enpsZV9zZXJ2aWNlc19fc3RvcmFnZUVuZ2luZV9fY29tbW9uTWFwcGluZ19fZHluYW1pYz10cnVlCiAgICAgIC0ga3V6emxlX3NlcnZpY2VzX19pbnRlcm5hbENhY2hlX19ub2RlX19ob3N0PXJlZGlzCiAgICAgIC0ga3V6emxlX3NlcnZpY2VzX19tZW1vcnlTdG9yYWdlX19ub2RlX19ob3N0PXJlZGlzCiAgICAgIC0ga3V6emxlX3NlcnZlcl9fcHJvdG9jb2xzX19tcXR0X19lbmFibGVkPXRydWUKICAgICAgLSBrdXp6bGVfc2VydmVyX19wcm90b2NvbHNfX21xdHRfX2RldmVsb3BtZW50TW9kZT1mYWxzZQogICAgICAtIGt1enpsZV9saW1pdHNfX2xvZ2luc1BlclNlY29uZD01MAogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnREVCVUc9JHtERUJVRzota3V6emxlOmNsdXN0ZXI6c3luY30nCiAgICAgIC0gJ0RFQlVHX0RFUFRIPSR7REVCVUdfREVQVEg6LTB9JwogICAgICAtICdERUJVR19NQVhfQVJSQVlfTEVOR1RIPSR7REVCVUdfTUFYX0FSUkFZOi0xMDB9JwogICAgICAtICdERUJVR19FWFBBTkQ9JHtERUJVR19FWFBBTkQ6LW9mZn0nCiAgICAgIC0gJ0RFQlVHX1NIT1dfSElEREVOPXskREVCVUdfU0hPV19ISURERU46LW9ufScKICAgICAgLSAnREVCVUdfQ09MT1JTPSR7REVCVUdfQ09MT1JTOi1vbn0nCiAgICBjYXBfYWRkOgogICAgICAtIFNZU19QVFJBQ0UKICAgIHVsaW1pdHM6CiAgICAgIG5vZmlsZTogNjU1MzYKICAgIHN5c2N0bHM6CiAgICAgIC0gbmV0LmNvcmUuc29tYXhjb25uPTgxOTIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo3NTEyL19oZWFsdGhjaGVjaycKICAgICAgdGltZW91dDogMXMKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHJldHJpZXM6IDMwCiAgICBkZXBlbmRzX29uOgogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBlbGFzdGljc2VhcmNoOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5Cg==",
|
||||
"tags": [
|
||||
"backend",
|
||||
"api",
|
||||
@@ -300,6 +300,18 @@
|
||||
"playlist"
|
||||
]
|
||||
},
|
||||
"minio": {
|
||||
"documentation": "https:\/\/docs.min.io\/docs\/minio-docker-quickstart-guide.html",
|
||||
"slogan": "MinIO is a high performance object storage server compatible with Amazon S3 APIs.",
|
||||
"compose": "c2VydmljZXM6CiAgbWluaW86CiAgICBpbWFnZTogJ3F1YXkuaW8vbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZGF0YSAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTUlOSU9fU0VSVkVSX1VSTD0kTUlOSU9fU0VSVkVSX1VSTAogICAgICAtIE1JTklPX0JST1dTRVJfUkVESVJFQ1RfVVJMPSRNSU5JT19CUk9XU0VSX1JFRElSRUNUX1VSTAogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScK",
|
||||
"tags": [
|
||||
"object",
|
||||
"storage",
|
||||
"server",
|
||||
"s3",
|
||||
"api"
|
||||
]
|
||||
},
|
||||
"moodle": {
|
||||
"documentation": "https:\/\/moodle.org",
|
||||
"slogan": "Moodle is the world\u2019s most customisable and trusted eLearning solution that empowers educators to improve our world.",
|
||||
@@ -462,6 +474,20 @@
|
||||
"security"
|
||||
]
|
||||
},
|
||||
"weblate": {
|
||||
"documentation": "https:\/\/docs.weblate.org\/en\/latest\/admin\/install\/docker.html",
|
||||
"slogan": "Weblate is a libre software web-based continuous localization system.",
|
||||
"compose": "c2VydmljZXM6CiAgd2VibGF0ZToKICAgIGltYWdlOiAnd2VibGF0ZS93ZWJsYXRlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XRUJMQVRFCiAgICAgIC0gV0VCTEFURV9TSVRFX0RPTUFJTj0kU0VSVklDRV9VUkxfV0VCTEFURQogICAgICAtICdXRUJMQVRFX0FETUlOX05BTUU9JHtXRUJMQVRFX0FETUlOX05BTUU6LUFkbWlufScKICAgICAgLSAnV0VCTEFURV9BRE1JTl9FTUFJTD0ke1dFQkxBVEVfQURNSU5fRU1BSUw6LWFkbWluQGV4YW1wbGUuY29tfScKICAgICAgLSBXRUJMQVRFX0FETUlOX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1dFQkxBVEUKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7V0VCTEFURV9BRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXdlYmxhdGV9JwogICAgICAtIFBPU1RHUkVTX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFJFRElTX0hPU1Q9cmVkaXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3dlYmxhdGUtZGF0YTovYXBwL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi13ZWJsYXRlfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICB2b2x1bWVzOgogICAgICAtICd3ZWJsYXRlLXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK",
|
||||
"tags": [
|
||||
"localization",
|
||||
"translation",
|
||||
"web",
|
||||
"web-based",
|
||||
"continuous",
|
||||
"libre",
|
||||
"software"
|
||||
]
|
||||
},
|
||||
"whoogle": {
|
||||
"documentation": "https:\/\/github.com\/benbusby\/whoogle-search#install",
|
||||
"slogan": "Whoogle is a self-hosted, privacy-focused search engine front-end for accessing Google search results without tracking and data collection.",
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"version": "3.12.36"
|
||||
},
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.122"
|
||||
"version": "4.0.0-beta.132"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user