Merge pull request #1284 from coollabsio/next

v4.0.0-beta.63
This commit is contained in:
Andras Bacsai 2023-10-04 11:01:05 +02:00 committed by GitHub
commit 93ec785f4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 271 additions and 170 deletions

View File

@ -50,8 +50,9 @@ class General extends Component
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}";
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}

View File

@ -29,7 +29,10 @@ class Index extends Component
$this->parameters = get_route_parameters();
$this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->refreshStack();
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
ray($this->applications);
ray($this->databases);
}
public function saveCompose($raw)
{

View File

@ -0,0 +1,35 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\LocalPersistentVolume;
use Livewire\Component;
class Storage extends Component
{
protected $listeners = ['addNewVolume'];
public $resource;
public function render()
{
return view('livewire.project.service.storage');
}
public function addNewVolume($data)
{
try {
LocalPersistentVolume::create([
'name' => $data['name'],
'mount_path' => $data['mount_path'],
'host_path' => $data['host_path'],
'resource_id' => $this->resource->id,
'resource_type' => $this->resource->getMorphClass(),
]);
$this->resource->refresh();
$this->emit('success', 'Storage added successfully');
$this->emit('clearAddStorage');
$this->emit('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@ -33,7 +33,7 @@ class Add extends Component
{
$this->validate();
$name = $this->uuid . '-' . $this->name;
$this->emitUp('submit', [
$this->emit('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path,
'host_path' => $this->host_path,

View File

@ -7,30 +7,11 @@ use Livewire\Component;
class All extends Component
{
public bool $isHeaderVisible = true;
public $resource;
protected $listeners = ['refreshStorages', 'submit'];
protected $listeners = ['refreshStorages'];
public function refreshStorages()
{
$this->resource->refresh();
}
public function submit($data)
{
try {
LocalPersistentVolume::create([
'name' => $data['name'],
'mount_path' => $data['mount_path'],
'host_path' => $data['host_path'],
'resource_id' => $this->resource->id,
'resource_type' => $this->resource->getMorphClass(),
]);
$this->resource->refresh();
$this->emit('success', 'Storage added successfully');
$this->emit('clearAddStorage');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@ -44,6 +44,10 @@ class Application extends BaseModel
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function type()
{

View File

@ -31,23 +31,18 @@ class LocalFileVolume extends BaseModel
}
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
ray($isFile);
if ($isFile == 'OK' && $fileVolume->is_directory) {
throw new \Exception("File $path is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.");
} else if ($isDir == 'OK' && !$fileVolume->is_directory) {
throw new \Exception("File $path is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory.");
}
if (($isFile == 'NOK' && !$fileVolume->is_directory) || $isFile == 'OK') {
$rootDir = Str::of($path)->dirname();
$commands->push("mkdir -p $rootDir > /dev/null 2>&1 || true");
$commands->push("touch $path > /dev/null 2>&1 || true");
if ($content) {
$content = base64_encode($content);
$commands->push("echo '$content' | base64 -d > $path");
}
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
if (!$fileVolume->is_directory && $isDir == 'NOK') {
$content = base64_encode($content);
$commands->push("echo '$content' | base64 -d > $path");
} else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true");
}
ray($commands->toArray());
return instant_remote_process($commands, $server);
}
}

View File

@ -5,6 +5,7 @@ namespace App\Models;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
@ -120,7 +121,20 @@ class Server extends BaseModel
{
return $this->hasMany(Service::class);
}
public function getIp(): Attribute
{
return Attribute::make(
get: function () {
if (isDev()) {
return '127.0.0.1';
}
if ($this->ip === 'host.docker.internal') {
return base_ip();
}
return $this->ip;
}
);
}
public function previews()
{
return $this->destinations()->map(function ($standaloneDocker) {

View File

@ -3,7 +3,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Symfony\Component\Yaml\Yaml;
class ServiceDatabase extends BaseModel
{

View File

@ -76,6 +76,11 @@ class StandalonePostgresql extends BaseModel
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();

View File

@ -85,28 +85,39 @@ 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
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
if ($isFile === 'NOK' &&!$fileVolume->is_directory && $isInit) {
$fileVolume->saveStorageOnServer($oneService);
continue;
}
if ($isFile == 'OK' && !$fileVolume->is_directory) {
if ($isFile == 'OK') {
// If its a file & exists
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
if (base64_encode($filesystemContent) != base64_encode($content)) {
$fileVolume->content = $filesystemContent;
$fileVolume->save();
}
} else {
if ($isDir == 'OK') {
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
} else {
$fileVolume->content = null;
$fileVolume->is_directory = false;
$fileVolume->save();
}
$fileVolume->content = $filesystemContent;
$fileVolume->is_directory = false;
$fileVolume->save();
} else if ($isDir == 'OK') {
// If its a directory & exists
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
} else if ($isFile == 'NOK' && $isDir == 'NOK' && !$fileVolume->is_directory && $isInit && $content) {
// Does not exists (no dir or file), not flagged as directory, is init, has content
$fileVolume->content = $content;
$fileVolume->is_directory = false;
$fileVolume->save();
$content = base64_encode($content);
$dir = Str::of($fileLocation)->dirname();
instant_remote_process([
"mkdir -p $dir",
"echo '$content' | base64 -d > $fileLocation"
], $server);
} else if ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) {
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
instant_remote_process(["mkdir -p $fileLocation"], $server);
}
}
} catch (\Throwable $e) {

View File

@ -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.62',
'release' => '4.0.0-beta.63',
// When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'),

View File

@ -1,3 +1,3 @@
<?php
return '4.0.0-beta.62';
return '4.0.0-beta.63';

View File

@ -34,14 +34,14 @@ services:
POSTGRES_DB: "${DB_DATABASE:-coolify}"
POSTGRES_HOST_AUTH_METHOD: "trust"
volumes:
- ./_data/coolify/_volumes/database/:/var/lib/postgresql/data
- /data/coolify/_volumes/database/:/var/lib/postgresql/data
redis:
ports:
- "${FORWARD_REDIS_PORT:-6379}:6379"
env_file:
- .env
volumes:
- ./_data/coolify/_volumes/redis/:/data
- /data/coolify/_volumes/redis/:/data
vite:
image: node:19
working_dir: /var/www/html
@ -56,7 +56,7 @@ services:
volumes:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock
- ./_data/coolify/:/data/coolify
- /data/coolify/:/data/coolify
mailpit:
image: "axllent/mailpit:latest"
container_name: coolify-mail
@ -76,6 +76,6 @@ services:
MINIO_ACCESS_KEY: "${MINIO_ACCESS_KEY:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_SECRET_KEY:-minioadmin}"
volumes:
- ./_data/coolify/_volumes/minio/:/data
- /data/coolify/_volumes/minio/:/data
networks:
- coolify

View File

@ -26,27 +26,29 @@
<x-forms.input label="Image" id="database.image" required
helper="For all available images, check here:<br><br><a target='_blank' href='https://hub.docker.com/_/postgres'>https://hub.docker.com/_/postgres</a>" />
</div>
<div class="flex gap-2">
@if ($database->started_at)
<x-forms.input label="Username" id="database.postgres_username" placeholder="If empty: postgres"
readonly helper="You can only modify it before the initial start." />
<x-forms.input label="Password" id="database.postgres_password" type="password" required readonly
helper="You can only modify it before the initial start." />
<x-forms.input label="Database" id="database.postgres_db"
@if ($database->started_at)
<div class="flex gap-2">
<x-forms.input label="Initial Username" id="database.postgres_username" placeholder="If empty: postgres"
readonly helper="You can only change this in the database." />
<x-forms.input label="Initial Password" id="database.postgres_password" type="password" required readonly
helper="You can only change this in the database." />
<x-forms.input label="Initial Database" id="database.postgres_db"
placeholder="If empty, it will be the same as Username." readonly
helper="You can only modify it before the initial start." />
@else
<x-forms.input label="Username" id="database.postgres_user" placeholder="If empty: postgres"
helper="You can only modify it before the initial start." />
<x-forms.input label="Password" id="database.postgres_password" type="password" required
helper="You can only modify it before the initial start." />
helper="You can only change this in the database." />
</div>
@else
<div class="pt-8 text-warning">Please verify these values. You can only modify them before the initial start. After that, you need to modify it in the database.
</div>
<div class="flex gap-2 pb-8">
<x-forms.input label="Username" id="database.postgres_user" placeholder="If empty: postgres" />
<x-forms.input label="Password" id="database.postgres_password" type="password" required />
<x-forms.input label="Database" id="database.postgres_db"
placeholder="If empty, it will be the same as Username."
helper="You can only modify it before the initial start." />
@endif
</div>
placeholder="If empty, it will be the same as Username." />
</div>
@endif
<div class="flex gap-2">
<x-forms.input label="Initial Arguments" id="database.postgres_initdb_args"
<x-forms.input label="Initial Database Arguments" id="database.postgres_initdb_args"
placeholder="If empty, use default. See in docker docs." />
<x-forms.input label="Host Auth Method" id="database.postgres_host_auth_method"
placeholder="If empty, use default. See in docker docs." />
@ -55,11 +57,12 @@
<h3 class="py-2">Network</h3>
<div class="flex items-end gap-2">
<x-forms.input placeholder="3000:5432" id="database.ports_mappings" label="Ports Mappings"
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{$database->is_public}}" id="database.public_port" label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" />
helper="A comma separated list of ports you would like to map to the host system.<br><span class='inline-block font-bold text-warning'>Example</span>3000:5432,3002:5433" />
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
label="Public Port" />
<x-forms.checkbox instantSave id="database.is_public" label="Accessible over the internet" />
</div>
<x-forms.input label="Postgres URL" readonly wire:model="db_url" />
</div>
</form>
<div class="pb-16">

View File

@ -1,4 +1,4 @@
<div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.10000ms="checkStatus">
<div x-data="{ raw: true, activeTab: window.location.hash ? window.location.hash.substring(1) : 'service-stack' }" wire:poll.2000ms="checkStatus">
<livewire:project.service.navbar :service="$service" :parameters="$parameters" :query="$query" />
<livewire:project.service.compose-modal :raw="$service->docker_compose_raw" :actual="$service->docker_compose" />
<div class="flex h-full pt-6">
@ -102,42 +102,17 @@
</div>
<div x-cloak x-show="activeTab === 'storages'">
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="text-warning">Please modify storage layout in your Docker Compose file.</span>
@foreach ($applications as $application)
@if ($loop->first)
<livewire:project.shared.storages.all :resource="$application" />
@else
<livewire:project.shared.storages.all :resource="$application" :isHeaderVisible="false" />
@endif
@if ($application->fileStorages()->get()->count() > 0)
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($application->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
<livewire:project.service.storage wire:key="application-{{ $application->id }}"
:resource="$application" />
@endforeach
@foreach ($databases as $database)
@if ($loop->first)
<h3 class="pt-4">{{ Str::headline($database->name) }}</h3>
@if ($applications->count() > 0)
<livewire:project.shared.storages.all :resource="$database" :isHeaderVisible="false" />
@else
<livewire:project.shared.storages.all :resource="$database" />
@endif
@if ($database->fileStorages()->get()->count() > 0)
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($database->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
@else
<livewire:project.shared.storages.all :resource="$database" :isHeaderVisible="false" />
@endif
<livewire:project.service.storage wire:key="database-{{ $database->id }}" :resource="$database" />
@endforeach
</div>

View File

@ -24,15 +24,13 @@
<livewire:project.service.application :application="$serviceApplication" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$serviceApplication" />
@if ($serviceApplication->fileStorages()->get()->count() > 0)
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($serviceApplication->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceApplication->id }}"
:resource="$serviceApplication" />
</div>
@endisset
@isset($serviceDatabase)
@ -40,15 +38,13 @@
<livewire:project.service.database :database="$serviceDatabase" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$serviceDatabase" />
@if ($serviceDatabase->fileStorages()->get()->count() > 0)
<h5 class="py-4">Mounted Files/Dirs (binds)</h5>
<div class="flex flex-col gap-4">
@foreach ($serviceDatabase->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="{{ $loop->index }}" />
@endforeach
</div>
@endif
<div class="flex items-center gap-2">
<h2>Storages</h2>
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
<span class="text-warning">Please modify storage layout in your Docker Compose file.</span>
<livewire:project.service.storage wire:key="application-{{ $serviceDatabase->id }}"
:resource="$serviceDatabase" />
</div>
@endisset
</div>

View File

@ -0,0 +1,49 @@
<div>
@if (
$resource->getMorphClass() == 'App\Models\Application' ||
$resource->getMorphClass() == 'App\Models\StandalonePostgresql')
<div class="flex items-center gap-2">
<h2>Storages</h2>
<x-helper
helper="For Preview Deployments, storage has a <span class='text-helper'>-pr-#PRNumber</span> in their
volume
name, example: <span class='text-helper'>-pr-1</span>" />
<x-forms.button class="btn" onclick="newStorage.showModal()">+ Add</x-forms.button>
<livewire:project.shared.storages.add :uuid="$resource->uuid" />
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
@if (
$resource->persistentStorages()->get()->count() === 0 &&
$resource->fileStorages()->get()->count() == 0)
<div>No storage found.</div>
@else
@if ($resource->persistentStorages()->get()->count() > 0)
<livewire:project.shared.storages.all :resource="$resource" />
@endif
@if ($resource->fileStorages()->get()->count() > 0)
<div class="flex flex-col gap-4 pt-4">
@foreach ($resource->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage"
wire:key="resource-{{ $resource->uuid }}" />
@endforeach
</div>
@endif
@endif
@else
@if (
$resource->persistentStorages()->get()->count() > 0 ||
$resource->fileStorages()->get()->count() > 0)
<h3 class="pt-4">{{ Str::headline($resource->name) }} </h3>
@endif
@if ($resource->persistentStorages()->get()->count() > 0)
<livewire:project.shared.storages.all :resource="$resource" />
@endif
@if ($resource->fileStorages()->get()->count() > 0)
<div class="flex flex-col gap-4 pt-4">
@foreach ($resource->fileStorages()->get()->sort() as $fileStorage)
<livewire:project.service.file-storage :fileStorage="$fileStorage" wire:key="resource-{{ $resource->uuid }}" />
@endforeach
</div>
@endif
@endif
</div>

View File

@ -29,13 +29,8 @@
</a>
</div>
<div class="flex-1 pl-8">
@if (serviceStatus($resource) === 'running')
<livewire:project.shared.get-logs :server="$server" :container="$container" />
@else
Service is not running.
@endif
<livewire:project.shared.get-logs :server="$server" :container="$container" />
</div>
</div>
@endif
</x-layout>

View File

@ -1,25 +1,4 @@
<div>
@if ($isHeaderVisible)
<div>
<div class="flex items-center gap-2">
<h2>Storages</h2>
@if ($resource->type() !== 'service')
<x-helper
helper="For Preview Deployments, storage has a <span class='text-helper'>-pr-#PRNumber</span> in their
volume
name, example: <span class='text-helper'>-pr-1</span>" />
<x-forms.button class="btn" onclick="newStorage.showModal()">+ Add</x-forms.button>
<livewire:project.shared.storages.add :uuid="$resource->uuid" />
@endif
</div>
<div class="pb-4">Persistent storage to preserve data between deployments.</div>
@if ($resource->type() === 'service')
<span class="text-warning">Please modify storage layout in your <a class="underline"
href="{{ Str::of(url()->current())->beforeLast('/') }}">Docker Compose</a> file.</span>
<h2 class="pt-4">{{ Str::headline($resource->name) }} </h2>
@endif
</div>
@endif
<div class="flex flex-col gap-4">
@foreach ($resource->persistentStorages as $storage)
@if ($resource->type() === 'service')

View File

@ -14,8 +14,7 @@
@click.prevent="activeTab = 'source'; window.location.hash = 'source'" href="#">Source</a>
@endif
<a :class="activeTab === 'server' && 'text-white'"
@click.prevent="activeTab = 'server'; window.location.hash = 'server'"
href="#">Server
@click.prevent="activeTab = 'server'; window.location.hash = 'server'" href="#">Server
</a>
<a :class="activeTab === 'storages' && 'text-white'"
@click.prevent="activeTab = 'storages'; window.location.hash = 'storages'" href="#">Storages
@ -27,8 +26,8 @@
</a>
@endif
<a :class="activeTab === 'health' && 'text-white'"
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks
</a>
@click.prevent="activeTab = 'health'; window.location.hash = 'health'" href="#">Health Checks
</a>
<a :class="activeTab === 'rollback' && 'text-white'"
@click.prevent="activeTab = 'rollback'; window.location.hash = 'rollback'" href="#">Rollback
</a>
@ -56,7 +55,7 @@
<livewire:project.shared.destination :destination="$application->destination" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$application" />
<livewire:project.service.storage :resource="$application" />
</div>
<div x-cloak x-show="activeTab === 'previews'">
<livewire:project.application.previews :application="$application" />

View File

@ -47,7 +47,7 @@
<livewire:project.shared.destination :destination="$database->destination" />
</div>
<div x-cloak x-show="activeTab === 'storages'">
<livewire:project.shared.storages.all :resource="$database" />
<livewire:project.service.storage :resource="$database" />
</div>
<div x-cloak x-show="activeTab === 'resource-limits'">
<livewire:project.shared.resource-limits :resource="$database" />

57
scripts/sync_volume.sh Normal file
View File

@ -0,0 +1,57 @@
#!/bin/bash
# Sync docker volumes between two servers
VERSION="1.0.0"
SOURCE=$1
DESTINATION=$2
set -e
if [ -z "$SOURCE" ]; then
echo "Source server is not specified."
exit 1
fi
if [ -z "$DESTINATION" ]; then
echo "Destination server is not specified."
exit 1
fi
SOURCE_USER=$(echo $SOURCE | cut -d@ -f1)
SOURCE_SERVER=$(echo $SOURCE | cut -d: -f1 | cut -d@ -f2)
SOURCE_PORT=$(echo $SOURCE | cut -d: -f2 | cut -d/ -f1)
SOURCE_VOLUME_NAME=$(echo $SOURCE | cut -d/ -f2)
if ! [[ "$SOURCE_PORT" =~ ^[0-9]+$ ]]; then
echo "Invalid source port: $SOURCE_PORT"
exit 1
fi
DESTINATION_USER=$(echo $DESTINATION | cut -d@ -f1)
DESTINATION_SERVER=$(echo $DESTINATION | cut -d: -f1 | cut -d@ -f2)
DESTINATION_PORT=$(echo $DESTINATION | cut -d: -f2 | cut -d/ -f1)
DESTINATION_VOLUME_NAME=$(echo $DESTINATION | cut -d/ -f2)
if ! [[ "$DESTINATION_PORT" =~ ^[0-9]+$ ]]; then
echo "Invalid destination port: $DESTINATION_PORT"
exit 1
fi
echo "Generating backup file to ./$SOURCE_VOLUME_NAME.tgz"
ssh -p $SOURCE_PORT $SOURCE_USER@$SOURCE_SERVER "docker run -v $SOURCE_VOLUME_NAME:/volume --rm --log-driver none loomchild/volume-backup backup -c pigz -v" >./$SOURCE_VOLUME_NAME.tgz
echo ""
if [ -f "./$SOURCE_VOLUME_NAME.tgz" ]; then
echo "Uploading backup file to $DESTINATION_SERVER:~/$DESTINATION_VOLUME_NAME.tgz"
scp -P $DESTINATION_PORT ./$SOURCE_VOLUME_NAME.tgz $DESTINATION_USER@$DESTINATION_SERVER:~/$DESTINATION_VOLUME_NAME.tgz
echo ""
echo "Restoring backup file on remote ($DESTINATION_SERVER:/~/$DESTINATION_VOLUME_NAME.tgz)"
ssh -p $DESTINATION_PORT $DESTINATION_USER@$DESTINATION_SERVER "docker run -i -v $DESTINATION_VOLUME_NAME:/volume --log-driver none --rm loomchild/volume-backup restore -c pigz -vf < ~/$DESTINATION_VOLUME_NAME.tgz"
echo ""
echo "Deleting backup file on remote ($DESTINATION_SERVER:/~/$DESTINATION_VOLUME_NAME.tgz)"
ssh -p $DESTINATION_PORT $DESTINATION_USER@$DESTINATION_SERVER "rm ~/$DESTINATION_VOLUME_NAME.tgz"
echo ""
echo "Local file ./$SOURCE_VOLUME_NAME.tgz is not deleted."
echo ""
echo "WARNING: If you are copying a database volume, you need to set the right users/passwords on the destination service's environment variables."
echo "Why? Because we are copying the volume as-is, so the database credentials will bethe same as on the source volume."
fi

View File

@ -2,6 +2,6 @@
"plausible-analytics": {
"documentation": "https://plausible.io/docs",
"slogan": "A lighweight and open-source website analytics tool.",
"compose": "dmVyc2lvbjogIjMuMyIKc2VydmljZXM6CiAgcGxhdXNpYmxlLWFuYWx5dGljczoKICAgIGltYWdlOiBwbGF1c2libGUvYW5hbHl0aWNzOnYyLjAKICAgIGNvbW1hbmQ6IHNoIC1jICJzbGVlcCAxMCAmJiAvZW50cnlwb2ludC5zaCBkYiBjcmVhdGVkYiAmJiAvZW50cnlwb2ludC5zaCBkYiBtaWdyYXRlICYmIC9lbnRyeXBvaW50LnNoIHJ1biIKICAgIGVudmlyb25tZW50OgogICAgICAtIERBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYXVzaWJsZV9kYi9wbGF1c2libGUKICAgICAgLSBCQVNFX1VSTD0kU0VSVklDRV9GUUROX1BMQVVTSUJMRQogICAgICAtIFNFQ1JFVF9LRVlfQkFTRT0kU0VSVklDRV9CQVNFNjRfNjRfUExBVVNJQkxFCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYXVzaWJsZV9kYgogICAgICAtIHBsYXVzaWJsZV9ldmVudHNfZGIKICAgICAgLSBtYWlsCgogIG1haWw6CiAgICBpbWFnZTogYnl0ZW1hcmsvc210cAoKICBwbGF1c2libGVfZGI6CiAgICBpbWFnZTogcG9zdGdyZXM6MTQtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIGRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19EQj1wbGF1c2libGUKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwoKICBwbGF1c2libGVfZXZlbnRzX2RiOgogICAgaW1hZ2U6IGNsaWNraG91c2UvY2xpY2tob3VzZS1zZXJ2ZXI6MjMuMy43LjUtYWxwaW5lCiAgICB2b2x1bWVzOgogICAgICAtIHR5cGU6IHZvbHVtZQogICAgICAgIHNvdXJjZTogZXZlbnQtZGF0YQogICAgICAgIHRhcmdldDogL3Zhci9saWIvY2xpY2tob3VzZQogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLWNvbmZpZy54bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvY2xpY2tob3VzZS1zZXJ2ZXIvY29uZmlnLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PHByb2ZpbGVzPjxkZWZhdWx0Pjxsb2dfcXVlcmllcz4wPC9sb2dfcXVlcmllcz48bG9nX3F1ZXJ5X3RocmVhZHM+MDwvbG9nX3F1ZXJ5X3RocmVhZHM+PC9kZWZhdWx0PjwvcHJvZmlsZXM+PC9jbGlja2hvdXNlPgogICAgICAtIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vY2xpY2tob3VzZS9jbGlja2hvdXNlLXVzZXItY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci91c2Vycy5kL2xvZ2dpbmcueG1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogPi0KICAgICAgICAgIDxjbGlja2hvdXNlPjxsb2dnZXI+PGxldmVsPndhcm5pbmc8L2xldmVsPjxjb25zb2xlPnRydWU8L2NvbnNvbGU+PC9sb2dnZXI+PHF1ZXJ5X3RocmVhZF9sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHF1ZXJ5X2xvZyByZW1vdmU9InJlbW92ZSIvPjx0ZXh0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48dHJhY2VfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PG1ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PGFzeW5jaHJvbm91c19tZXRyaWNfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjxzZXNzaW9uX2xvZyByZW1vdmU9InJlbW92ZSIvPjxwYXJ0X2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48L2NsaWNraG91c2U+CiAgICB1bGltaXRzOgogICAgICBub2ZpbGU6CiAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgaGFyZDogMjYyMTQ0Cg=="
"compose": "dmVyc2lvbjogJzMuMycKc2VydmljZXM6CiAgcGxhdXNpYmxlX2RiOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNC1hbHBpbmUnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJy9ldGMvZGF0YS9wbGF1c2libGUvZGItZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kUE9TVEdSRVNfUEFTU1dPUkQKICBwbGF1c2libGVfZXZlbnRzX2RiOgogICAgaW1hZ2U6ICdjbGlja2hvdXNlL2NsaWNraG91c2Utc2VydmVyOjIzLjMuNy41LWFscGluZScKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgdm9sdW1lczoKICAgICAgLSAnL2V0Yy9kYXRhL3BsYXVzaWJsZS9ldmVudC1kYXRhOi92YXIvbGliL2NsaWNraG91c2UnCiAgICAgIC0gdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogL2V0Yy9kYXRhL3BsYXVzaWJsZS9jbGlja2hvdXNlL2NsaWNraG91c2UtY29uZmlnLnhtbAogICAgICAgIHRhcmdldDogL2V0Yy9jbGlja2hvdXNlLXNlcnZlci9jb25maWcuZC9sb2dnaW5nLnhtbAogICAgICAgIHJlYWRfb25seTogdHJ1ZQogICAgICAgIGNvbnRlbnQ6ID4tCiAgICAgICAgICA8Y2xpY2tob3VzZT48cHJvZmlsZXM+PGRlZmF1bHQ+PGxvZ19xdWVyaWVzPjA8L2xvZ19xdWVyaWVzPjxsb2dfcXVlcnlfdGhyZWFkcz4wPC9sb2dfcXVlcnlfdGhyZWFkcz48L2RlZmF1bHQ+PC9wcm9maWxlcz48L2NsaWNraG91c2U+CiAgICAgIC0gdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogL2V0Yy9kYXRhL3BsYXVzaWJsZS9jbGlja2hvdXNlL2NsaWNraG91c2UtdXNlci1jb25maWcueG1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL2NsaWNraG91c2Utc2VydmVyL3VzZXJzLmQvbG9nZ2luZy54bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiA+LQogICAgICAgICAgPGNsaWNraG91c2U+PGxvZ2dlcj48bGV2ZWw+d2FybmluZzwvbGV2ZWw+PGNvbnNvbGU+dHJ1ZTwvY29uc29sZT48L2xvZ2dlcj48cXVlcnlfdGhyZWFkX2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48cXVlcnlfbG9nIHJlbW92ZT0icmVtb3ZlIi8+PHRleHRfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjx0cmFjZV9sb2cgcmVtb3ZlPSJyZW1vdmUiLz48bWV0cmljX2xvZwogICAgICAgICAgcmVtb3ZlPSJyZW1vdmUiLz48YXN5bmNocm9ub3VzX21ldHJpY19sb2cKICAgICAgICAgIHJlbW92ZT0icmVtb3ZlIi8+PHNlc3Npb25fbG9nIHJlbW92ZT0icmVtb3ZlIi8+PHBhcnRfbG9nCiAgICAgICAgICByZW1vdmU9InJlbW92ZSIvPjwvY2xpY2tob3VzZT4KICAgIHVsaW1pdHM6CiAgICAgICAgbm9maWxlOgogICAgICAgICAgc29mdDogMjYyMTQ0CiAgICAgICAgICBoYXJkOiAyNjIxNDQKICBwbGF1c2libGU6CiAgICBpbWFnZTogJ3BsYXVzaWJsZS9hbmFseXRpY3M6djIuMCcKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgY29tbWFuZDogJ3NoIC1jICJzbGVlcCAxMCAmJiAvZW50cnlwb2ludC5zaCBkYiBjcmVhdGVkYiAmJiAvZW50cnlwb2ludC5zaCBkYiBtaWdyYXRlICYmIC9lbnRyeXBvaW50LnNoIHJ1biInCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYXVzaWJsZV9kYgogICAgICAtIHBsYXVzaWJsZV9ldmVudHNfZGIKICAgIHBvcnRzOgogICAgICAtICc4MDAwOjgwMDAnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFQ1JFVF9LRVlfQkFTRQogICAgICAtIERBVEFCQVNFX1VSTD0kREFUQUJBU0VfVVJMCiAgICAgIC0gJ0NMSUNLSE9VU0VfREFUQUJBU0VfVVJMPWh0dHA6Ly9wbGF1c2libGVfZXZlbnRzX2RiOjgxMjMvcGxhdXNpYmxlX2V2ZW50c19kYicKICAgICAgLSBNQUlMRVJfQURBUFRFUj0kTUFJTEVSX0FEQVBURVIKICAgICAgLSBTRU5ER1JJRF9BUElfS0VZPSRTRU5ER1JJRF9BUElfS0VZCiAgICAgIC0gR09PR0xFX0NMSUVOVF9JRD0kR09PR0xFX0NMSUVOVF9JRAogICAgICAtIEdPT0dMRV9DTElFTlRfU0VDUkVUPSRHT09HTEVfQ0xJRU5UX1NFQ1JFVAogICAgICAtIERJU0FCTEVfUkVHSVNUUkFUSU9OPSRESVNBQkxFX1JFR0lTVFJBVElPTgogICAgICAtIEJBU0VfVVJMPSRCQVNFX1VSTAogICAgICAtIExPR19GQUlMRURfTE9HSU5fQVRURU1QVFM9JExPR19GQUlMRURfTE9HSU5fQVRURU1QVFMK"
},
}

View File

@ -4,7 +4,7 @@
"version": "3.12.36"
},
"v4": {
"version": "4.0.0-beta.62"
"version": "4.0.0-beta.63"
}
}
}