Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 49 additions & 45 deletions ProcessMaker/Console/Commands/TenantsCreate.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use ProcessMaker\Multitenancy\Tenant;

class TenantsCreate extends Command
Expand All @@ -17,7 +18,17 @@ class TenantsCreate extends Command
*
* @var string
*/
protected $signature = 'tenants:create {--name=} {--url=} {--database=} {--username=} {--password=} {--storage-folder=} {--lang-folder=} {--app-key=}';
protected $signature = 'tenants:create
{--name=}
{--url=}
{--database=}
{--username=}
{--password=}
{--storage-folder=}
{--lang-folder=}
{--app-key=}
{--skip-initialize-folders}
{--skip-setup-notifications}';

/**
* The console command description.
Expand All @@ -31,6 +42,17 @@ class TenantsCreate extends Command
*/
public function handle()
{
$infoCallback = function ($type, $message) {
if ($type === 'out') {
$this->info($message);
} else {
$this->error($message);
}
};

// Check if rsync exists
Process::run('which rsync')->throw();

$requiredOptions = ['name', 'database', 'url'];

foreach ($requiredOptions as $option) {
Expand Down Expand Up @@ -82,21 +104,8 @@ public function handle()
if ($storageFolderOption) {
if (File::isDirectory($storageFolderOption)) {
$this->info('Moving storage folder to ' . $tenantStoragePath);
$subfoldersToExclude = '/^(tenant_\d+|logs|transitions)$/i';
foreach (File::directories($storageFolderOption) as $subfolder) {
if (preg_match($subfoldersToExclude, basename($subfolder))) {
$this->info('Skipping ' . $subfolder);
continue;
}
$this->info('Moving ' . $subfolder . ' to ' . $tenantStoragePath);
rename($subfolder, $tenantStoragePath . '/' . basename($subfolder));
}

// Move all files from the root storage folder to the tenant storage folder
foreach (File::files($storageFolderOption) as $file) {
$this->info('Moving ' . $file . ' to ' . $tenantStoragePath);
rename($file, $tenantStoragePath . '/' . basename($file));
}
$cmd = "rsync -avz --exclude='tenant_*' --exclude='logs' --exclude='transitions' " . $storageFolderOption . '/ ' . $tenantStoragePath;
Process::run($cmd, $infoCallback)->throw();
} else {
$this->error('Storage folder does not exist: ' . $storageFolderOption);

Expand All @@ -110,13 +119,14 @@ public function handle()
$tenantLangPath = resource_path('lang/tenant_' . $tenant->id);
if ($langFolderOption) {
if (File::isDirectory($langFolderOption)) {
$this->info('Moving lang folder to ' . $tenantLangPath);
$this->info('Copying lang folder to ' . $tenantLangPath);
if (File::isDirectory($tenantLangPath)) {
$this->error('Tenant lang path already exists: ' . $tenantLangPath);

return 1;
} else {
rename($langFolderOption, $tenantLangPath);
$cmd = "rsync -avz --exclude='tenant_*' " . $langFolderOption . '/ ' . $tenantLangPath;
Process::run($cmd, $infoCallback)->throw();
}
} else {
$this->error('Lang folder does not exist: ' . $langFolderOption);
Expand All @@ -130,16 +140,13 @@ public function handle()
}
}

$cmd = sprintf(
"rsync -azv --ignore-existing --exclude='tenant_*' %s %s",
escapeshellarg($sourceLangPath . '/'),
escapeshellarg($tenantLangPath)
);
exec($cmd, $output, $returnVar);
if ($returnVar !== 0) {
$this->error('Failed to rsync lang folder to tenant: ' . implode(PHP_EOL, $output));

return 1;
if (!$this->option('skip-initialize-folders')) {
$cmd = sprintf(
"rsync -azv --ignore-existing --exclude='tenant_*' %s %s",
escapeshellarg($sourceLangPath . '/'),
escapeshellarg($tenantLangPath)
);
Process::run($cmd, $infoCallback)->throw();
}

$subfolders = [
Expand All @@ -163,9 +170,11 @@ public function handle()
'api-docs',
];

foreach ($subfolders as $subfolder) {
if (!File::isDirectory($tenantStoragePath . '/' . $subfolder)) {
mkdir($tenantStoragePath . '/' . $subfolder, 0755, true);
if (!$this->option('skip-initialize-folders')) {
foreach ($subfolders as $subfolder) {
if (!File::isDirectory($tenantStoragePath . '/' . $subfolder)) {
mkdir($tenantStoragePath . '/' . $subfolder, 0755, true);
}
}
}

Expand All @@ -183,20 +192,15 @@ public function handle()
// Setup database
DB::connection('landlord')->statement("CREATE DATABASE IF NOT EXISTS `{$this->option('database')}`");

// Hold off on this for now.
// $this->tenantArtisan('tenant:storage-link', $tenant->id);

// Must be run after migrations so skip it. (provider somewhere complains about a table missing)
// $this->tenantArtisan('passport:keys --force', $tenant->id);

$this->info("Empty tenant created.\n");
$this->info("With the tenant set (using TENANT={$tenant->id} env prefix) you must now:");
$this->line('- Run migrations');
$this->line('- Seed the database');
$this->line('- Run the install command for each package');
$this->line('- Run artisan upgrade');
$this->line('- Generate passport keys with artisan passport:keys');
$this->info("For example, `TENANT={$tenant->id} php artisan migrate:fresh --seed`");
$this->info("Empty tenant created. ID: {$tenant->id}");
if (!$this->option('skip-setup-notifications')) {
$this->info("You will need to do the following using TENANT={$tenant->id} env prefix");
$this->line('- Run migrations and seed the database');
$this->line('- Run the install command for each package');
$this->line('- Run artisan upgrade');
$this->line('- Install passport by calling passport:install');
$this->info("For example, `TENANT={$tenant->id} php artisan migrate:fresh --seed`");
}
}

private function tenantArtisan($command, $tenantId)
Expand Down
53 changes: 6 additions & 47 deletions ProcessMaker/Console/Commands/TenantsEnable.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,6 @@ public function handle()

return 1;
}

// Check if rsync exists
if (!exec('which rsync')) {
$this->error('rsync is not installed');

return 1;
}
}

// Get the landlord database name
Expand Down Expand Up @@ -97,35 +90,17 @@ public function handle()
* Begin migration if the --migrate option is provided.
*/

// First, copy the existing storage folder to a temp location
$tempStorageFolder = base_path('storage-temp');
exec("rsync -avz --exclude='tenant_*' " . base_path('storage') . '/ ' . $tempStorageFolder, $output, $returnVar);
if ($returnVar !== 0) {
$this->error('Failed to copy storage folder to temp location');
$this->error(implode("\n", $output));

return 1;
}
$this->info(implode("\n", $output));

// Next, do the same thing for the lang folder
$tempLangFolder = base_path('lang-temp');
exec('rsync -avz ' . resource_path('lang') . '/ ' . $tempLangFolder, $output, $returnVar);
if ($returnVar !== 0) {
$this->error('Failed to copy lang folder to temp location');
$this->error(implode("\n", $output));
}
$this->info(implode("\n", $output));

// Now, create the tenant. The folder will be moved to the new tenant after the creation
// and the $tempStorageFolder will no longer exist.
// Now, create the tenant.
// The contents of lang-folder and storage-folder will be copied to the new tenant.
$exitCode = Artisan::call('tenants:create', [
'--database' => config('database.connections.processmaker.database'),
'--url' => config('app.url'),
'--storage-folder' => $tempStorageFolder,
'--lang-folder' => $tempLangFolder,
'--storage-folder' => storage_path(),
'--lang-folder' => lang_path(),
'--name' => config('app.name'),
'--app-key' => config('app.key'),
'--skip-setup-notifications' => true,
'--skip-initialize-folders' => true,
], $this->output);

if ($exitCode !== 0) {
Expand All @@ -134,22 +109,6 @@ public function handle()
return 1;
}

// Remove temp storage folder
exec('rm -rf ' . $tempStorageFolder, $output, $returnVar);
if ($returnVar !== 0) {
$this->error('Failed to remove temp storage folder');
$this->error(implode("\n", $output));
}
$this->info(implode("\n", $output));

// Remove temp lang folder
exec('rm -rf ' . $tempLangFolder, $output, $returnVar);
if ($returnVar !== 0) {
$this->error('Failed to remove temp lang folder');
$this->error(implode("\n", $output));
}
$this->info(implode("\n", $output));

// Add or update the MULTITENANCY env var
$this->addOrUpdateEnvVar('MULTITENANCY', 'true');

Expand Down
86 changes: 55 additions & 31 deletions ProcessMaker/Console/Commands/TenantsTransition.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ class TenantsTransition extends Command
*
* @var string
*/
protected $description = 'Transition clients to tenants from storage/transitions folder';
protected $description = 'Transition clients to tenants from .env files in storage/transitions folder';

/**
* The success messages.
*
* @var array
*/
protected $success = [];

/**
* Execute the console command.
Expand All @@ -44,59 +51,68 @@ public function handle()
return 1;
}

$clientFolders = File::directories($transitionsPath);
$envFiles = File::files($transitionsPath, true);
$envFiles = array_filter($envFiles, function ($file) {
return str_starts_with(basename($file), '.env');
});

if (empty($clientFolders)) {
$this->error('No client folders found in storage/transitions.');
if (empty($envFiles)) {
$this->error('No .env files found in storage/transitions.');

return 1;
}

foreach ($clientFolders as $clientFolder) {
$this->processClientFolder($clientFolder);
foreach ($envFiles as $envFile) {
$exitCode = $this->processEnvFile($envFile);
if ($exitCode !== 0) {
$this->outputSuccessMessages();

return $exitCode;
}
}

$this->info('All clients have been transitioned to tenants.');
$this->outputSuccessMessages();

return 0;
}

private function outputSuccessMessages()
{
foreach ($this->success as $message) {
$this->info($message);
}
}

/**
* Process a single client folder
* Process a single .env file
*
* @param string $clientFolder
* @param string $envFilePath
* @return void
*/
private function processClientFolder(string $clientFolder)
private function processEnvFile(string $envFilePath)
{
$clientName = basename($clientFolder);
$this->info("Processing client: {$clientName}");

$envFile = $clientFolder . '/.env';
if (!File::exists($envFile)) {
$this->error("No .env file found in {$clientName}");

return;
}
$fileName = basename($envFilePath);
$this->info("Processing .env file: {$fileName}");

// Read the .env file
$envContents = File::get($envFile);
$envContents = File::get($envFilePath);
$envVars = $this->parseEnvFile($envContents);

// Required environment variables
$requiredVars = ['APP_NAME', 'APP_URL', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD'];
foreach ($requiredVars as $var) {
if (!isset($envVars[$var])) {
$this->error("Missing required environment variable: {$var} in {$clientName}");
$this->error("Missing required environment variable: {$var} in {$fileName}");

return;
return 1;
}
}

$appName = $envVars['PROCESS_INTELLIGENCE_COMPANY_NAME'];
$appName = $envVars['PROCESS_INTELLIGENCE_COMPANY_NAME'] ?? null;
if (!$appName) {
// Get the app name from the folder name
$appName = basename($clientFolder);
// Get the app name from the .env file suffix (e.g., .env.my-app -> my-app)
$appName = str_replace('.env.', '', $fileName);
}

// Create the tenant
Expand All @@ -105,31 +121,39 @@ private function processClientFolder(string $clientFolder)
$this->info("Creating tenant for domain: {$domain}");

// Call tenants:create command
// NOTE: Storage and lang folders will be moved manually after the tenant is created.
$command = [
'--name' => $appName,
'--url' => $envVars['APP_URL'],
'--database' => $envVars['DB_DATABASE'],
'--username' => $envVars['DB_USERNAME'],
'--password' => $envVars['DB_PASSWORD'],
'--storage-folder' => $clientFolder . '/storage',
'--lang-folder' => $clientFolder . '/lang',
'--app-key' => $envVars['APP_KEY'],
'--skip-setup-notifications' => true,
'--skip-initialize-folders' => true,
];

Artisan::call('tenants:create', $command);
$exitCode = Artisan::call('tenants:create', $command, $this->output);
if ($exitCode !== 0) {
$this->error("Failed to create tenant for domain: {$domain}");

return 1;
}

// Find the newly created tenant
$tenant = Tenant::where('domain', $domain)->first();
if (!$tenant) {
$this->error("Failed to find tenant after creation for domain: {$domain}");

return;
return 1;
}

// Delete the client folder
File::deleteDirectory($clientFolder);
// Delete the .env file
File::delete($envFilePath);

$this->info("Successfully transitioned client {$clientName} to tenant.");
$this->success[] = "Success: {$fileName} -> Tenant ID: {$tenant->id}";

return 0;
}

/**
Expand Down
Loading
Loading