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
16 changes: 15 additions & 1 deletion app/Commands/DatabaseClusterCreate.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,21 @@ class DatabaseClusterCreate extends BaseCommand
{--engine-version= : Database engine version (e.g. 18, 8.4). Default: newest available}
{--region= : Database region}
{--cluster= : Dedicated cluster ID to create the database cluster on}
{--create-default-database= : Whether to create a default database (true or false; Neon clusters cannot skip it). Default: true}';
{--create-default-database= : Whether to create a default database (true or false; Neon clusters cannot skip it). Default: true}
{--preset= : Preset to start from, as listed by the API for the type and region (e.g. dev, prod; case-insensitive). Default: the first preset offered}
{--size= : Instance size (Laravel MySQL, AWS RDS)}
{--storage= : Storage in GB (Laravel MySQL, AWS RDS)}
{--storage-autoscale-max-gb= : Largest storage in GB the cluster may grow to on its own, or "null" to turn it off (Laravel MySQL)}
{--is-public= : Whether publicly accessible (Laravel MySQL, AWS RDS)}
{--uses-scheduled-snapshots= : Whether scheduled backups are enabled (Laravel MySQL)}
{--retention-days= : Days to retain backups}
{--maintenance-window= : UTC maintenance window, e.g. sun:06:00-sun:06:30 (Laravel MySQL, AWS RDS)}
{--suspend-seconds= : Seconds of inactivity before scale to zero, 0 for never (Laravel MySQL, Neon)}
{--cu-min= : Minimum compute units (Neon)}
{--cu-max= : Maximum compute units (Neon)}
{--uses-pitr= : Whether point-in-time recovery is enabled (AWS RDS)}
{--deployment-option= : single-az, single-az-with-read-replicas, or multi-az (AWS RDS)}
{--read-replicas= : Number of read replicas (AWS RDS)}';

protected $description = 'Create a new database cluster';

Expand Down
27 changes: 3 additions & 24 deletions app/Commands/DatabaseClusterUpdate.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Commands;

use App\Client\Requests\UpdateDatabaseClusterRequestData;
use App\Concerns\CoercesDatabaseClusterConfig;
use App\Dto\DatabaseCluster;
use App\Dto\DatabaseType;
use App\Exceptions\CommandExitException;
Expand All @@ -17,6 +18,8 @@

class DatabaseClusterUpdate extends BaseCommand
{
use CoercesDatabaseClusterConfig;

protected ?string $jsonDataClass = DatabaseCluster::class;

protected $signature = 'database-cluster:update
Expand Down Expand Up @@ -217,28 +220,4 @@ protected function collectDataAndUpdate(DatabaseCluster $database, DatabaseType

return $this->updateCluster($database, $type);
}

protected function coerceConfigValue(string $key, mixed $value, DatabaseType $type): mixed
{
$schemaField = collect($type->configSchema)->firstWhere('name', $key);

if (! $schemaField) {
return $value;
}

$schema = is_array($schemaField) ? $schemaField : $schemaField->toArray();
$fieldType = $schema['type'] ?? 'string';

// Nullable fields such as storage_autoscale_max_gb use null to turn the setting off.
if (is_string($value) && strtolower($value) === 'null' && in_array($fieldType, ['integer', 'number'], true)) {
return null;
}

return match ($fieldType) {
'integer' => (int) $value,
'number' => (float) $value,
'boolean' => filter_var($value, FILTER_VALIDATE_BOOLEAN),
default => $value,
};
}
}
45 changes: 45 additions & 0 deletions app/Concerns/CoercesDatabaseClusterConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Concerns;

use App\Dto\DatabaseType;
use RuntimeException;

trait CoercesDatabaseClusterConfig
{
protected function coerceConfigValue(string $key, mixed $value, DatabaseType $type): mixed
{
$schemaField = collect($type->configSchema)->firstWhere('name', $key);

if (! $schemaField) {
return $value;
}

$schema = is_array($schemaField) ? $schemaField : $schemaField->toArray();
$fieldType = $schema['type'] ?? 'string';

// Nullable fields such as storage_autoscale_max_gb use null to turn the setting off.
if (is_string($value) && strtolower($value) === 'null' && in_array($fieldType, ['integer', 'number'], true)) {
return null;
}

[$filter, $expected] = match ($fieldType) {
'integer' => [FILTER_VALIDATE_INT, 'a whole number'],
'number' => [FILTER_VALIDATE_FLOAT, 'a number'],
'boolean' => [FILTER_VALIDATE_BOOLEAN, 'true or false'],
default => [null, null],
};

if ($filter === null) {
return $value;
}

$parsed = filter_var($value, $filter, FILTER_NULL_ON_FAILURE);

if ($parsed === null) {
throw new RuntimeException(sprintf('Invalid --%s value "%s". Must be %s.', str_replace('_', '-', $key), $value, $expected));
}

return $parsed;
}
}
77 changes: 75 additions & 2 deletions app/Concerns/CreatesDatabaseCluster.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@

trait CreatesDatabaseCluster
{
use CoercesDatabaseClusterConfig;

protected const DATABASE_CLUSTER_CONFIG_FIELDS = [
'size',
'storage',
'storage_autoscale_max_gb',
'is_public',
'uses_scheduled_snapshots',
'retention_days',
'maintenance_window',
'suspend_seconds',
'cu_min',
'cu_max',
'uses_pitr',
'deployment_option',
'read_replicas',
];

protected ?string $databaseClusterPreset = null;

protected function createDatabaseCluster(array $defaults = []): DatabaseCluster
Expand Down Expand Up @@ -110,7 +128,10 @@ protected function createDatabaseCluster(array $defaults = []): DatabaseCluster
),
);

$config = $this->databaseClusterConfigFromPreset($selectedType) ?? $this->promptForDatabaseClusterConfig($selectedType);
$config = [
...$this->databaseClusterConfigFromPreset($selectedType) ?? $this->promptForDatabaseClusterConfig($selectedType),
...$this->databaseClusterConfigFromOptions($selectedType),
];
$clusterId = $this->hasOption('cluster') ? $this->option('cluster') : null;
$createDefaultDatabase = $this->createDefaultDatabaseOption();

Expand Down Expand Up @@ -165,6 +186,26 @@ protected function databaseClusterConfigFromPreset(DatabaseType $type): ?array
return null;
}

$requested = $this->hasOption('preset') ? $this->option('preset') : null;

if ($requested !== null) {
$preset = $presets->first(fn (DatabasePreset $preset) => strcasecmp($preset->key, $requested) === 0);

if ($preset === null) {
throw new RuntimeException(sprintf(
'Invalid --preset value "%s" for database type "%s" in region "%s". Must be one of: %s (case-insensitive)',
$requested,
$type->type,
$region,
$presets->keys()->implode(', '),
));
}

$this->databaseClusterPreset = $preset->key;

return $preset->config;
}

// The cached choice may not exist for a region picked on a later attempt.
if ($this->databaseClusterPreset !== null && $presets->has($this->databaseClusterPreset)) {
return $presets->get($this->databaseClusterPreset)->config;
Expand All @@ -187,6 +228,33 @@ protected function databaseClusterConfigFromPreset(DatabaseType $type): ?array
return $presets->get($this->databaseClusterPreset)->config;
}

/**
* Config fields passed as options, which win over the preset's values.
*/
protected function databaseClusterConfigFromOptions(DatabaseType $type): array
{
$fields = collect($type->configSchema)->map(fn ($field) => is_array($field) ? $field['name'] : $field->name);
$config = [];

foreach (self::DATABASE_CLUSTER_CONFIG_FIELDS as $name) {
$option = str_replace('_', '-', $name);
// The custom config prompts start from the flag, so their answer wins once the API has asked for a new one.
$value = $this->form()->get('config.'.$name, $this->hasOption($option) ? $this->option($option) : null);

if ($value === null) {
continue;
}

if (! $fields->contains($name)) {
throw new RuntimeException(sprintf('The --%s option does not apply to %s clusters.', $option, $type->label));
}

$config[$name] = $this->coerceConfigValue($name, $value, $type);
}

return $config;
}

protected function describeDatabasePreset(DatabaseType $type, DatabasePreset $preset): string
{
// Only some types have a hand-written summary; the rest (such as RDS) get their config listed as-is.
Expand All @@ -210,12 +278,13 @@ protected function promptForDatabaseClusterConfig(DatabaseType $type): array
$name = $schema['name'];
$fieldType = $schema['type'] ?? 'string';
$required = $schema['required'] ?? false;
$description = $schema['description'] ?? null;
$description = $schema['description'] ?? '';
$min = $schema['min'] ?? null;
$max = $schema['max'] ?? null;
$enum = $schema['enum'] ?? [];
$example = $schema['example'] ?? null;
$key = 'config.'.$name;
$optionName = str_replace('_', '-', $name);

$label = match ($name) {
'cu_min' => 'CU min',
Expand All @@ -235,6 +304,7 @@ protected function promptForDatabaseClusterConfig(DatabaseType $type): array
required: $required,
),
),
$optionName,
);
} elseif ($fieldType === 'boolean') {
$this->form()->prompt(
Expand All @@ -246,6 +316,7 @@ protected function promptForDatabaseClusterConfig(DatabaseType $type): array
hint: $description,
),
),
$optionName,
);
} elseif ($fieldType === 'integer') {
$this->form()->prompt(
Expand All @@ -260,6 +331,7 @@ protected function promptForDatabaseClusterConfig(DatabaseType $type): array
max: $max,
),
),
$optionName,
);
} else {
$this->form()->prompt(
Expand All @@ -272,6 +344,7 @@ protected function promptForDatabaseClusterConfig(DatabaseType $type): array
hint: $description,
),
),
$optionName,
);
}
}
Expand Down
Loading
Loading