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
25 changes: 2 additions & 23 deletions web/actions/backups.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use server";

import { desc, eq } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { db } from "@/db";
import { getBackupStorageConfig } from "@/db/queries";
import { servers, volumeBackups } from "@/db/schema";
import { volumeBackups } from "@/db/schema";
import { requireAuth } from "@/lib/auth";
import { triggerBackup } from "@/lib/backups/trigger-backup";
import { inngest } from "@/lib/inngest/client";
Expand All @@ -31,27 +31,6 @@ export async function createBackup(serviceId: string, volumeId: string) {
return { success: true, backupId: result.backupId };
}

export async function listBackups(serviceId: string) {
await requireAuth();
const backups = await db
.select({
id: volumeBackups.id,
volumeName: volumeBackups.volumeName,
status: volumeBackups.status,
sizeBytes: volumeBackups.sizeBytes,
createdAt: volumeBackups.createdAt,
completedAt: volumeBackups.completedAt,
errorMessage: volumeBackups.errorMessage,
serverName: servers.name,
})
.from(volumeBackups)
.leftJoin(servers, eq(volumeBackups.serverId, servers.id))
.where(eq(volumeBackups.serviceId, serviceId))
.orderBy(desc(volumeBackups.createdAt));

return backups;
}

export async function restoreBackup(
serviceId: string,
backupId: string,
Expand Down
103 changes: 4 additions & 99 deletions web/actions/migrations.ts
Original file line number Diff line number Diff line change
@@ -1,101 +1,20 @@
"use server";

import { and, eq } from "drizzle-orm";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { db } from "@/db";
import { getBackupStorageConfig } from "@/db/queries";
import { deployments, services, serviceVolumes } from "@/db/schema";
import { services } from "@/db/schema";
import { requireAuth } from "@/lib/auth";
import { inngest } from "@/lib/inngest/client";
import { inngestEvents } from "@/lib/inngest/events";
import { startMigrationInternal } from "@/lib/migrations";

export async function startMigration(
serviceId: string,
targetServerId: string,
) {
await requireAuth();
const storageConfig = await getBackupStorageConfig();
if (!storageConfig) {
throw new Error(
"Backup storage not configured. Configure it in Settings first.",
);
}

const service = await db
.select()
.from(services)
.where(eq(services.id, serviceId))
.then((r) => r[0]);

if (!service) {
throw new Error("Service not found");
}

if (!service.stateful) {
throw new Error("Only stateful services can be migrated");
}

if (service.migrationStatus) {
throw new Error("Migration already in progress");
}

const volumes = await db
.select()
.from(serviceVolumes)
.where(eq(serviceVolumes.serviceId, serviceId));

if (volumes.length === 0) {
throw new Error("No volumes found for this service");
}

const deployment = await db
.select({
id: deployments.id,
serverId: deployments.serverId,
containerId: deployments.containerId,
})
.from(deployments)
.where(
and(
eq(deployments.serviceId, serviceId),
eq(deployments.status, "running"),
),
)
.then((r) => r[0]);

if (!deployment?.serverId) {
throw new Error("No running deployment found");
}

if (!deployment.containerId) {
throw new Error("Deployment is missing container ID");
}

if (deployment.serverId === targetServerId) {
throw new Error("Service is already running on the target server");
}

await db
.update(services)
.set({
migrationStatus: "backing_up",
migrationTargetServerId: targetServerId,
migrationBackupId: null,
migrationError: null,
})
.where(eq(services.id, serviceId));

await inngest.send(
inngestEvents.migrationStarted.create({
serviceId,
targetServerId,
sourceServerId: deployment.serverId,
sourceDeploymentId: deployment.id,
sourceContainerId: deployment.containerId,
volumes: volumes.map((v) => ({ id: v.id, name: v.name })),
}),
);

await startMigrationInternal(serviceId, targetServerId);
revalidatePath(`/dashboard/projects`);
return { success: true };
}
Expand All @@ -117,17 +36,3 @@ export async function cancelMigration(serviceId: string) {
revalidatePath(`/dashboard/projects`);
return { success: true };
}

export async function getMigrationStatus(serviceId: string) {
await requireAuth();
const service = await db
.select({
migrationStatus: services.migrationStatus,
migrationError: services.migrationError,
})
.from(services)
.where(eq(services.id, serviceId))
.then((r) => r[0]);

return service ?? null;
}
59 changes: 2 additions & 57 deletions web/actions/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from "@/db/schema";
import { requireAuth } from "@/lib/auth";
import { DEFAULT_RESOURCE_LIMITS } from "@/lib/constants";
import { deployServiceInternal } from "@/lib/deploy-service";
import { markDeploymentUndesired } from "@/lib/deployment-status";
import { inngest } from "@/lib/inngest/client";
import { inngestEvents } from "@/lib/inngest/events";
Expand All @@ -47,7 +48,6 @@ import type {
import { getZodErrorMessage, slugify } from "@/lib/utils";
import { enqueueWork } from "@/lib/work-queue";
import { deleteBackup } from "./backups";
import { startMigration } from "./migrations";

function isValidImageReferencePart(reference: string): boolean {
const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
Expand Down Expand Up @@ -860,62 +860,7 @@ export async function updateServiceGithubRepo(

export async function deployService(serviceId: string) {
await requireAuth();
const service = await getService(serviceId);
if (!service) {
throw new Error("Service not found");
}

if (service.stateful) {
const configuredReplicas = await db
.select({
serverId: serviceReplicas.serverId,
replicas: serviceReplicas.count,
})
.from(serviceReplicas)
.where(eq(serviceReplicas.serviceId, serviceId));

const placements = configuredReplicas.filter((p) => p.replicas > 0);
const totalReplicas = placements.reduce((sum, p) => sum + p.replicas, 0);

if (totalReplicas !== 1) {
throw new Error("Stateful services can only have exactly 1 replica");
}

const serverIds = placements.map((p) => p.serverId);
if (serverIds.length !== 1) {
throw new Error(
"Stateful services must be deployed to exactly one server",
);
}

const targetServerId = serverIds[0];
if (service.lockedServerId && service.lockedServerId !== targetServerId) {
if (service.migrationStatus) {
throw new Error("Migration already in progress");
}
await startMigration(serviceId, targetServerId);
revalidatePath(`/dashboard/projects`);
return { migrationStarted: true };
}
}

const rolloutId = randomUUID();

await db.insert(rollouts).values({
id: rolloutId,
serviceId,
status: "in_progress",
currentStage: "queued",
});

await inngest.send(
inngestEvents.rolloutCreated.create({
rolloutId,
serviceId,
}),
);

return { rolloutId };
return deployServiceInternal(serviceId);
}

export async function deleteDeployments(serviceId: string) {
Expand Down
5 changes: 0 additions & 5 deletions web/actions/servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@ export async function deleteServer(id: string) {
await db.delete(servers).where(eq(servers.id, id));
}

export async function approveServer(id: string) {
await requireAuth();
await db.update(servers).set({ status: "pending" }).where(eq(servers.id, id));
}

export async function updateServerName(id: string, name: string) {
await requireAuth();
try {
Expand Down
4 changes: 2 additions & 2 deletions web/app/api/v1/agent/builds/[id]/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { and, eq, isNull } from "drizzle-orm";
import { type NextRequest, NextResponse } from "next/server";
import { deployService } from "@/actions/projects";
import { db } from "@/db";
import {
builds,
Expand All @@ -10,6 +9,7 @@ import {
services,
} from "@/db/schema";
import { verifyAgentRequest } from "@/lib/agent-auth";
import { deployServiceInternal } from "@/lib/deploy-service";
import { sendBuildFailureAlert } from "@/lib/email";
import { updateGitHubDeploymentStatus } from "@/lib/github";
import { inngest } from "@/lib/inngest/client";
Expand Down Expand Up @@ -351,7 +351,7 @@ export async function POST(
);

try {
await deployService(build.serviceId);
await deployServiceInternal(build.serviceId);
} catch (error) {
console.error("[build:complete] deployment failed:", error);
await db
Expand Down
Loading
Loading