Skip to content

Commit 3e53ad3

Browse files
authored
Merge pull request #188 from utopia-php/add-custom-domains-migration
Add Custom Domains migration
2 parents b07a09f + 294ff2c commit 3e53ad3

12 files changed

Lines changed: 379 additions & 0 deletions

File tree

src/Migration/Destinations/Appwrite.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
use Appwrite\Enums\Framework;
1111
use Appwrite\Enums\PasswordHash;
1212
use Appwrite\Enums\ProjectProtocolId;
13+
use Appwrite\Enums\ProxyResourceType;
1314
use Appwrite\Enums\Runtime;
1415
use Appwrite\Enums\SmtpEncryption;
16+
use Appwrite\Enums\StatusCode;
1517
use Appwrite\InputFile;
1618
use Appwrite\Services\Functions;
1719
use Appwrite\Services\Messaging;
1820
use Appwrite\Services\Project;
21+
use Appwrite\Services\Proxy;
1922
use Appwrite\Services\Sites;
2023
use Appwrite\Services\Storage;
2124
use Appwrite\Services\Teams;
@@ -51,6 +54,7 @@
5154
use Utopia\Migration\Resources\Database\Index;
5255
use Utopia\Migration\Resources\Database\Row;
5356
use Utopia\Migration\Resources\Database\Table;
57+
use Utopia\Migration\Resources\Domains\Rule;
5458
use Utopia\Migration\Resources\Functions\Deployment;
5559
use Utopia\Migration\Resources\Functions\EnvVar;
5660
use Utopia\Migration\Resources\Functions\Func;
@@ -106,6 +110,7 @@ class Appwrite extends Destination
106110
private Functions $functions;
107111
private Messaging $messaging;
108112
private Project $project;
113+
private Proxy $proxy;
109114
private Sites $sites;
110115
private Storage $storage;
111116
private Teams $teams;
@@ -191,6 +196,7 @@ public function __construct(
191196
$this->functions = new Functions($this->client);
192197
$this->messaging = new Messaging($this->client);
193198
$this->project = new Project($this->client);
199+
$this->proxy = new Proxy($this->client);
194200
$this->sites = new Sites($this->client);
195201
$this->storage = new Storage($this->client);
196202
$this->teams = new Teams($this->client);
@@ -302,6 +308,9 @@ public static function getSupportedResources(): array
302308

303309
// Backups
304310
Resource::TYPE_BACKUP_POLICY,
311+
312+
// Domains
313+
Resource::TYPE_RULE,
305314
];
306315
}
307316

@@ -461,6 +470,7 @@ protected function import(array $resources, callable $callback): void
461470
Transfer::GROUP_INTEGRATIONS => $this->importIntegrationsResource($resource),
462471
Transfer::GROUP_BACKUPS => $this->importBackupResource($resource),
463472
Transfer::GROUP_PROJECTS => $this->importProjectsResource($resource),
473+
Transfer::GROUP_DOMAINS => $this->importDomainsResource($resource),
464474
default => throw new \Exception('Invalid resource group', Exception::CODE_VALIDATION),
465475
};
466476
} catch (\Throwable $e) {
@@ -3156,6 +3166,25 @@ public function importProjectsResource(Resource $resource): Resource
31563166
return $resource;
31573167
}
31583168

3169+
public function importDomainsResource(Resource $resource): Resource
3170+
{
3171+
switch ($resource->getName()) {
3172+
case Resource::TYPE_RULE:
3173+
/** @var Rule $resource */
3174+
$success = $this->createRule($resource);
3175+
if (!$success) {
3176+
return $resource;
3177+
}
3178+
break;
3179+
}
3180+
3181+
if ($resource->getStatus() !== Resource::STATUS_SKIPPED) {
3182+
$resource->setStatus(Resource::STATUS_SUCCESS);
3183+
}
3184+
3185+
return $resource;
3186+
}
3187+
31593188
protected function createProjectVariable(ProjectVariable $resource): bool
31603189
{
31613190
$existing = $this->dbForProject->findOne('variables', [
@@ -3280,6 +3309,91 @@ protected function createSMTP(SMTP $resource): bool
32803309
return true;
32813310
}
32823311

3312+
/**
3313+
* Auto-generated rules (default `.appwrite.network` domains for functions/sites)
3314+
* are recreated automatically on the destination when the parent Function/Site
3315+
* is migrated, so only manual rules need to be imported.
3316+
*
3317+
* Function/site IDs are preserved across migration, so the source
3318+
* `deploymentResourceId` is passed through directly.
3319+
*/
3320+
protected function createRule(Rule $resource): bool
3321+
{
3322+
if ($resource->getTrigger() !== 'manual') {
3323+
$resource->setStatus(Resource::STATUS_SKIPPED, 'Auto-generated rule, recreated by parent resource migration');
3324+
return false;
3325+
}
3326+
3327+
$type = $resource->getType();
3328+
$deploymentResourceType = $resource->getDeploymentResourceType();
3329+
$branch = $resource->getDeploymentVcsProviderBranch();
3330+
3331+
try {
3332+
switch ($type) {
3333+
case 'api':
3334+
$this->proxy->createAPIRule($resource->getDomain());
3335+
break;
3336+
3337+
case 'redirect':
3338+
$statusCode = match ($resource->getRedirectStatusCode()) {
3339+
301 => StatusCode::MOVEDPERMANENTLY301(),
3340+
302 => StatusCode::FOUND302(),
3341+
307 => StatusCode::TEMPORARYREDIRECT307(),
3342+
308 => StatusCode::PERMANENTREDIRECT308(),
3343+
default => StatusCode::MOVEDPERMANENTLY301(),
3344+
};
3345+
3346+
$resourceType = $deploymentResourceType === 'site'
3347+
? ProxyResourceType::SITE()
3348+
: ProxyResourceType::FUNCTIONMODEL();
3349+
3350+
$this->proxy->createRedirectRule(
3351+
$resource->getDomain(),
3352+
$resource->getRedirectUrl(),
3353+
$statusCode,
3354+
$resource->getDeploymentResourceId(),
3355+
$resourceType,
3356+
);
3357+
break;
3358+
3359+
case 'deployment':
3360+
if ($deploymentResourceType === 'function') {
3361+
$this->proxy->createFunctionRule(
3362+
$resource->getDomain(),
3363+
$resource->getDeploymentResourceId(),
3364+
$branch !== '' ? $branch : null,
3365+
);
3366+
} elseif ($deploymentResourceType === 'site') {
3367+
$this->proxy->createSiteRule(
3368+
$resource->getDomain(),
3369+
$resource->getDeploymentResourceId(),
3370+
$branch !== '' ? $branch : null,
3371+
);
3372+
} else {
3373+
$resource->setStatus(Resource::STATUS_SKIPPED, 'Unsupported deployment resource type "' . $deploymentResourceType . '"');
3374+
return false;
3375+
}
3376+
break;
3377+
3378+
default:
3379+
$resource->setStatus(Resource::STATUS_SKIPPED, 'Unsupported rule type "' . $type . '"');
3380+
return false;
3381+
}
3382+
} catch (AppwriteException $e) {
3383+
// 409 means the domain is owned by another project/organization — the
3384+
// user has to release it there before re-running. Surface as a warning,
3385+
// not an error, so the rest of the migration continues.
3386+
if ($e->getCode() === 409) {
3387+
$resource->setStatus(Resource::STATUS_WARNING, 'Domain "' . $resource->getDomain() . '" is owned by another project. Remove it there and re-run the migration.');
3388+
return false;
3389+
}
3390+
3391+
throw $e;
3392+
}
3393+
3394+
return true;
3395+
}
3396+
32833397
protected function createWebhook(Webhook $resource): bool
32843398
{
32853399
$existing = $this->dbForPlatform->findOne('webhooks', [

src/Migration/Resource.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ abstract class Resource implements \JsonSerializable
8787
public const TYPE_PROJECT_LABELS = 'project-labels';
8888
public const TYPE_PROJECT_SERVICES = 'project-services';
8989

90+
// Domains
91+
public const TYPE_RULE = 'rule';
92+
9093
// Messaging
9194
public const TYPE_SUBSCRIBER = 'subscriber';
9295
public const TYPE_MESSAGE = 'message';
@@ -135,6 +138,7 @@ abstract class Resource implements \JsonSerializable
135138
self::TYPE_PROJECT_PROTOCOLS,
136139
self::TYPE_PROJECT_LABELS,
137140
self::TYPE_PROJECT_SERVICES,
141+
self::TYPE_RULE,
138142
self::TYPE_PROVIDER,
139143
self::TYPE_TOPIC,
140144
self::TYPE_SUBSCRIBER,
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
namespace Utopia\Migration\Resources\Domains;
4+
5+
use Utopia\Migration\Resource;
6+
use Utopia\Migration\Transfer;
7+
8+
class Rule extends Resource
9+
{
10+
public function __construct(
11+
string $id,
12+
private readonly string $domain,
13+
private readonly string $type,
14+
private readonly string $trigger,
15+
private readonly string $redirectUrl = '',
16+
private readonly int $redirectStatusCode = 0,
17+
private readonly string $deploymentResourceType = '',
18+
private readonly string $deploymentResourceId = '',
19+
private readonly string $deploymentVcsProviderBranch = '',
20+
string $createdAt = '',
21+
string $updatedAt = '',
22+
) {
23+
$this->id = $id;
24+
$this->createdAt = $createdAt;
25+
$this->updatedAt = $updatedAt;
26+
}
27+
28+
/**
29+
* @param array<string, mixed> $array
30+
*/
31+
public static function fromArray(array $array): self
32+
{
33+
return new self(
34+
$array['id'],
35+
$array['domain'],
36+
$array['type'],
37+
$array['trigger'] ?? 'manual',
38+
(string) ($array['redirectUrl'] ?? ''),
39+
(int) ($array['redirectStatusCode'] ?? 0),
40+
(string) ($array['deploymentResourceType'] ?? ''),
41+
(string) ($array['deploymentResourceId'] ?? ''),
42+
(string) ($array['deploymentVcsProviderBranch'] ?? ''),
43+
createdAt: $array['createdAt'] ?? '',
44+
updatedAt: $array['updatedAt'] ?? '',
45+
);
46+
}
47+
48+
/**
49+
* @return array<string, mixed>
50+
*/
51+
public function jsonSerialize(): array
52+
{
53+
return [
54+
'id' => $this->id,
55+
'domain' => $this->domain,
56+
'type' => $this->type,
57+
'trigger' => $this->trigger,
58+
'redirectUrl' => $this->redirectUrl,
59+
'redirectStatusCode' => $this->redirectStatusCode,
60+
'deploymentResourceType' => $this->deploymentResourceType,
61+
'deploymentResourceId' => $this->deploymentResourceId,
62+
'deploymentVcsProviderBranch' => $this->deploymentVcsProviderBranch,
63+
'createdAt' => $this->createdAt,
64+
'updatedAt' => $this->updatedAt,
65+
];
66+
}
67+
68+
public static function getName(): string
69+
{
70+
return Resource::TYPE_RULE;
71+
}
72+
73+
public function getGroup(): string
74+
{
75+
return Transfer::GROUP_DOMAINS;
76+
}
77+
78+
public function getDomain(): string
79+
{
80+
return $this->domain;
81+
}
82+
83+
public function getType(): string
84+
{
85+
return $this->type;
86+
}
87+
88+
public function getTrigger(): string
89+
{
90+
return $this->trigger;
91+
}
92+
93+
public function getRedirectUrl(): string
94+
{
95+
return $this->redirectUrl;
96+
}
97+
98+
public function getRedirectStatusCode(): int
99+
{
100+
return $this->redirectStatusCode;
101+
}
102+
103+
public function getDeploymentResourceType(): string
104+
{
105+
return $this->deploymentResourceType;
106+
}
107+
108+
public function getDeploymentResourceId(): string
109+
{
110+
return $this->deploymentResourceId;
111+
}
112+
113+
public function getDeploymentVcsProviderBranch(): string
114+
{
115+
return $this->deploymentVcsProviderBranch;
116+
}
117+
}

src/Migration/Source.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public function getProjectsBatchSize(): int
6161
return static::$defaultBatchSize;
6262
}
6363

64+
public function getDomainsBatchSize(): int
65+
{
66+
return static::$defaultBatchSize;
67+
}
68+
6469
/**
6570
* @param array<Resource> $resources
6671
* @return void
@@ -127,6 +132,7 @@ public function exportResources(array $resources): void
127132
Transfer::GROUP_INTEGRATIONS => Transfer::GROUP_INTEGRATIONS_RESOURCES,
128133
Transfer::GROUP_BACKUPS => Transfer::GROUP_BACKUPS_RESOURCES,
129134
Transfer::GROUP_PROJECTS => Transfer::GROUP_PROJECTS_RESOURCES,
135+
Transfer::GROUP_DOMAINS => Transfer::GROUP_DOMAINS_RESOURCES,
130136
];
131137

132138
foreach ($mapping as $group => $resources) {
@@ -170,6 +176,9 @@ public function exportResources(array $resources): void
170176
case Transfer::GROUP_PROJECTS:
171177
$this->exportGroupProjects($this->getProjectsBatchSize(), $resources);
172178
break;
179+
case Transfer::GROUP_DOMAINS:
180+
$this->exportGroupDomains($this->getDomainsBatchSize(), $resources);
181+
break;
173182
}
174183
}
175184
}
@@ -245,4 +254,12 @@ abstract protected function exportGroupBackups(int $batchSize, array $resources)
245254
* @param array<string> $resources Resources to export
246255
*/
247256
abstract protected function exportGroupProjects(int $batchSize, array $resources): void;
257+
258+
/**
259+
* Export Domains Group
260+
*
261+
* @param int $batchSize
262+
* @param array<string> $resources Resources to export
263+
*/
264+
abstract protected function exportGroupDomains(int $batchSize, array $resources): void;
248265
}

0 commit comments

Comments
 (0)