Enhance schedule management with new fields and improved components

- Introduced new fields in the schedule schema: `serviceName`, `scheduleType`, and `script`, allowing for more flexible schedule configurations.
- Updated the `HandleSchedules` component to incorporate the new fields, enhancing user input options for schedule creation and updates.
- Refactored the `ShowSchedules` component to support the new `scheduleType` and display relevant information based on the selected type.
- Improved API handling for schedule creation and updates to accommodate the new fields, ensuring proper validation and error handling.
- Added a new `ShowSchedulesModal` component for better integration of schedule viewing in server settings, enhancing user experience.
This commit is contained in:
Mauricio Siu
2025-05-02 20:17:21 -06:00
parent 49e55961db
commit 98d0f1d5bf
24 changed files with 17632 additions and 237 deletions

View File

@@ -13,6 +13,7 @@ import type { RedisNested } from "../databases/redis";
import { execAsync, execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import { getRemoteDocker } from "../servers/remote-docker";
import type { Compose } from "@dokploy/server/services/compose";
interface RegistryAuth {
username: string;
@@ -541,3 +542,67 @@ export const getRemoteServiceContainer = async (
throw error;
}
};
export const getServiceContainerIV2 = async (
appName: string,
serverId?: string | null,
) => {
try {
const filter = {
status: ["running"],
label: [`com.docker.swarm.service.name=${appName}`],
};
const remoteDocker = await getRemoteDocker(serverId);
const containers = await remoteDocker.listContainers({
filters: JSON.stringify(filter),
});
if (containers.length === 0 || !containers[0]) {
throw new Error(`No container found with name: ${appName}`);
}
const container = containers[0];
return container;
} catch (error) {
throw error;
}
};
export const getComposeContainer = async (
compose: Compose,
serviceName: string,
) => {
try {
const { appName, composeType, serverId } = compose;
// 1. Determine the correct labels based on composeType
const labels: string[] = [];
if (composeType === "stack") {
// Labels for Docker Swarm stack services
labels.push(`com.docker.stack.namespace=${appName}`);
labels.push(`com.docker.swarm.service.name=${appName}_${serviceName}`);
} else {
// Labels for Docker Compose projects (default)
labels.push(`com.docker.compose.project=${appName}`);
labels.push(`com.docker.compose.service=${serviceName}`);
}
const filter = {
status: ["running"],
label: labels,
};
const remoteDocker = await getRemoteDocker(serverId);
const containers = await remoteDocker.listContainers({
filters: JSON.stringify(filter),
limit: 1,
});
if (containers.length === 0 || !containers[0]) {
throw new Error(`No container found with name: ${appName}`);
}
const container = containers[0];
return container;
} catch (error) {
throw error;
}
};

View File

@@ -1,15 +1,15 @@
import type { Schedule } from "@dokploy/server/db/schema/schedule";
import { findScheduleById } from "@dokploy/server/services/schedule";
import { scheduleJob as scheduleJobNode } from "node-schedule";
import {
getRemoteServiceContainer,
getServiceContainer,
} from "../docker/utils";
import { getComposeContainer, getServiceContainerIV2 } from "../docker/utils";
import { execAsyncRemote } from "../process/execAsync";
import { spawnAsync } from "../process/spawnAsync";
import { createDeploymentSchedule } from "@dokploy/server/services/deployment";
import { createWriteStream } from "node:fs";
import { updateDeploymentStatus } from "@dokploy/server/services/deployment";
import { paths } from "@dokploy/server/constants";
import path from "node:path";
export const scheduleJob = (schedule: Schedule) => {
const { cronExpression, scheduleId } = schedule;
@@ -19,17 +19,16 @@ export const scheduleJob = (schedule: Schedule) => {
};
export const runCommand = async (scheduleId: string) => {
const { application, command, shellType } =
await findScheduleById(scheduleId);
const isServer = !!application.serverId;
const { Id: containerId } = isServer
? await getRemoteServiceContainer(
application.serverId || "",
application.appName,
)
: await getServiceContainer(application.appName);
const {
application,
command,
shellType,
scheduleType,
compose,
serviceName,
appName,
serverId,
} = await findScheduleById(scheduleId);
const deployment = await createDeploymentSchedule({
scheduleId,
@@ -37,51 +36,109 @@ export const runCommand = async (scheduleId: string) => {
description: "Schedule",
});
if (isServer) {
try {
await execAsyncRemote(
if (scheduleType === "application" || scheduleType === "compose") {
let containerId = "";
let serverId = "";
if (scheduleType === "application" && application) {
const container = await getServiceContainerIV2(
application.appName,
application.serverId,
`
set -e
echo "Running command: docker exec ${containerId} ${shellType} -c \"${command}\"" >> ${deployment.logPath};
docker exec ${containerId} ${shellType} -c "${command}" || {
echo "❌ Command failed" >> ${deployment.logPath};
exit 1;
}
`,
);
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
containerId = container.Id;
serverId = application.serverId || "";
}
if (scheduleType === "compose" && compose) {
const container = await getComposeContainer(compose, serviceName || "");
containerId = container.Id;
serverId = compose.serverId || "";
}
} else {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
if (serverId) {
try {
await execAsyncRemote(
serverId,
`
set -e
echo "Running command: docker exec ${containerId} ${shellType} -c \"${command}\"" >> ${deployment.logPath};
docker exec ${containerId} ${shellType} -c "${command}" >> ${deployment.logPath} 2>> ${deployment.logPath} || {
echo "❌ Command failed" >> ${deployment.logPath};
exit 1;
}
echo "✅ Command executed successfully" >> ${deployment.logPath};
`,
);
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
} else {
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
try {
writeStream.write(
`docker exec ${containerId} ${shellType} -c "${command}"\n`,
);
await spawnAsync(
"docker",
["exec", containerId, shellType, "-c", command],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
);
writeStream.write("✅ Command executed successfully\n");
} catch (error) {
writeStream.write("❌ Command failed\n");
writeStream.write(
error instanceof Error ? error.message : "Unknown error",
);
writeStream.end();
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
}
} else if (scheduleType === "dokploy-server") {
try {
writeStream.write(
`docker exec ${containerId} ${shellType} -c "${command}"\n`,
);
const writeStream = createWriteStream(deployment.logPath, { flags: "a" });
const { SCHEDULES_PATH } = paths();
const fullPath = path.join(SCHEDULES_PATH, appName || "");
await spawnAsync(
"docker",
["exec", containerId, shellType, "-c", command],
"bash",
["-c", "./script.sh"],
(data) => {
if (writeStream.writable) {
writeStream.write(data);
}
},
{
cwd: fullPath,
},
);
writeStream.write("✅ Command executed successfully\n");
} catch (error) {
writeStream.write("❌ Command failed\n");
writeStream.write(
error instanceof Error ? error.message : "Unknown error",
);
writeStream.end();
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
} else if (scheduleType === "server") {
try {
const { SCHEDULES_PATH } = paths(true);
const fullPath = path.join(SCHEDULES_PATH, appName || "");
const command = `
set -e
echo "Running script" >> ${deployment.logPath};
bash -c ${fullPath}/script.sh >> ${deployment.logPath} 2>> ${deployment.logPath} || {
echo "❌ Command failed" >> ${deployment.logPath};
exit 1;
}
echo "✅ Command executed successfully" >> ${deployment.logPath};
`;
await execAsyncRemote(serverId, command);
} catch (error) {
await updateDeploymentStatus(deployment.deploymentId, "error");
throw error;
}
}
await updateDeploymentStatus(deployment.deploymentId, "done");
};