feat: add application handling

This commit is contained in:
DJKnaeckebrot
2024-12-18 13:01:09 +01:00
parent 63c0912849
commit 8ea453f444
4 changed files with 446 additions and 403 deletions

View File

@@ -1,5 +1,6 @@
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -31,6 +32,7 @@ const deleteApplicationSchema = z.object({
projectName: z.string().min(1, { projectName: z.string().min(1, {
message: "Application name is required", message: "Application name is required",
}), }),
deleteVolumes: z.boolean(),
}); });
type DeleteApplication = z.infer<typeof deleteApplicationSchema>; type DeleteApplication = z.infer<typeof deleteApplicationSchema>;
@@ -50,6 +52,7 @@ export const DeleteApplication = ({ applicationId }: Props) => {
const form = useForm<DeleteApplication>({ const form = useForm<DeleteApplication>({
defaultValues: { defaultValues: {
projectName: "", projectName: "",
deleteVolumes: false,
}, },
resolver: zodResolver(deleteApplicationSchema), resolver: zodResolver(deleteApplicationSchema),
}); });
@@ -59,6 +62,7 @@ export const DeleteApplication = ({ applicationId }: Props) => {
if (formData.projectName === expectedName) { if (formData.projectName === expectedName) {
await mutateAsync({ await mutateAsync({
applicationId, applicationId,
deleteVolumes: formData.deleteVolumes,
}) })
.then((data) => { .then((data) => {
push(`/dashboard/project/${data?.projectId}`); push(`/dashboard/project/${data?.projectId}`);
@@ -134,6 +138,27 @@ export const DeleteApplication = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="deleteVolumes"
render={({ field }) => (
<FormItem>
<div className="flex items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="ml-2">
Delete volumes associated with this compose
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
</form> </form>
</Form> </Form>
</div> </div>

View File

@@ -6,6 +6,7 @@ import {
import { db } from "@/server/db"; import { db } from "@/server/db";
import { import {
apiCreateApplication, apiCreateApplication,
apiDeleteApplication,
apiFindMonitoringStats, apiFindMonitoringStats,
apiFindOneApplication, apiFindOneApplication,
apiReloadApplication, apiReloadApplication,
@@ -142,7 +143,7 @@ export const applicationRouter = createTRPCRouter({
}), }),
delete: protectedProcedure delete: protectedProcedure
.input(apiFindOneApplication) .input(apiDeleteApplication)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await checkServiceAccess( await checkServiceAccess(
@@ -178,7 +179,11 @@ export const applicationRouter = createTRPCRouter({
async () => async () =>
await removeTraefikConfig(application.appName, application.serverId), await removeTraefikConfig(application.appName, application.serverId),
async () => async () =>
await removeService(application?.appName, application.serverId), await removeService(
application?.appName,
application.serverId,
input.deleteVolumes,
),
]; ];
for (const operation of cleanupOperations) { for (const operation of cleanupOperations) {

View File

@@ -17,6 +17,7 @@ import { github } from "./github";
import { gitlab } from "./gitlab"; import { gitlab } from "./gitlab";
import { mounts } from "./mount"; import { mounts } from "./mount";
import { ports } from "./port"; import { ports } from "./port";
import { previewDeployments } from "./preview-deployments";
import { projects } from "./project"; import { projects } from "./project";
import { redirects } from "./redirects"; import { redirects } from "./redirects";
import { registry } from "./registry"; import { registry } from "./registry";
@@ -25,7 +26,6 @@ import { server } from "./server";
import { applicationStatus, certificateType } from "./shared"; import { applicationStatus, certificateType } from "./shared";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { generateAppName } from "./utils";
import { previewDeployments } from "./preview-deployments";
export const sourceType = pgEnum("sourceType", [ export const sourceType = pgEnum("sourceType", [
"docker", "docker",
@@ -518,3 +518,8 @@ export const apiUpdateApplication = createSchema
applicationId: z.string().min(1), applicationId: z.string().min(1),
}) })
.omit({ serverId: true }); .omit({ serverId: true });
export const apiDeleteApplication = z.object({
applicationId: z.string().min(1),
deleteVolumes: z.boolean(),
});

View File

@@ -23,7 +23,7 @@ interface RegistryAuth {
export const pullImage = async ( export const pullImage = async (
dockerImage: string, dockerImage: string,
onData?: (data: any) => void, onData?: (data: any) => void,
authConfig?: Partial<RegistryAuth>, authConfig?: Partial<RegistryAuth>
): Promise<void> => { ): Promise<void> => {
try { try {
if (!dockerImage) { if (!dockerImage) {
@@ -41,7 +41,7 @@ export const pullImage = async (
"-p", "-p",
authConfig.password, authConfig.password,
], ],
onData, onData
); );
} }
await spawnAsync("docker", ["pull", dockerImage], onData); await spawnAsync("docker", ["pull", dockerImage], onData);
@@ -54,7 +54,7 @@ export const pullRemoteImage = async (
dockerImage: string, dockerImage: string,
serverId: string, serverId: string,
onData?: (data: any) => void, onData?: (data: any) => void,
authConfig?: Partial<RegistryAuth>, authConfig?: Partial<RegistryAuth>
): Promise<void> => { ): Promise<void> => {
try { try {
if (!dockerImage) { if (!dockerImage) {
@@ -85,9 +85,9 @@ export const pullRemoteImage = async (
}, },
(event) => { (event) => {
onData?.(event); onData?.(event);
}, }
); );
}, }
); );
}); });
} catch (error) { } catch (error) {
@@ -185,7 +185,7 @@ export const cleanUpInactiveContainers = async () => {
try { try {
const containers = await docker.listContainers({ all: true }); const containers = await docker.listContainers({ all: true });
const inactiveContainers = containers.filter( const inactiveContainers = containers.filter(
(container) => container.State !== "running", (container) => container.State !== "running"
); );
for (const container of inactiveContainers) { for (const container of inactiveContainers) {
@@ -210,7 +210,7 @@ export const cleanUpSystemPrune = async (serverId?: string) => {
if (serverId) { if (serverId) {
await execAsyncRemote( await execAsyncRemote(
serverId, serverId,
"docker system prune --all --force --volumes", "docker system prune --all --force --volumes"
); );
} else { } else {
await execAsync("docker system prune --all --force --volumes"); await execAsync("docker system prune --all --force --volumes");
@@ -238,9 +238,17 @@ export const startServiceRemote = async (serverId: string, appName: string) => {
export const removeService = async ( export const removeService = async (
appName: string, appName: string,
serverId?: string | null, serverId?: string | null,
deleteVolumes = false
) => { ) => {
try { try {
const command = `docker service rm ${appName}`; let command: string;
if (deleteVolumes) {
command = `docker service rm --force ${appName}`;
} else {
command = `docker service rm ${appName}`;
}
if (serverId) { if (serverId) {
await execAsyncRemote(serverId, command); await execAsyncRemote(serverId, command);
} else { } else {
@@ -253,7 +261,7 @@ export const removeService = async (
export const prepareEnvironmentVariables = ( export const prepareEnvironmentVariables = (
serviceEnv: string | null, serviceEnv: string | null,
projectEnv?: string | null, projectEnv?: string | null
) => { ) => {
const projectVars = parse(projectEnv ?? ""); const projectVars = parse(projectEnv ?? "");
const serviceVars = parse(serviceEnv ?? ""); const serviceVars = parse(serviceEnv ?? "");
@@ -421,7 +429,7 @@ export const generateFileMounts = (
| MariadbNested | MariadbNested
| MysqlNested | MysqlNested
| PostgresNested | PostgresNested
| RedisNested, | RedisNested
) => { ) => {
const { mounts } = service; const { mounts } = service;
const { APPLICATIONS_PATH } = paths(!!service.serverId); const { APPLICATIONS_PATH } = paths(!!service.serverId);
@@ -447,7 +455,7 @@ export const generateFileMounts = (
export const createFile = async ( export const createFile = async (
outputPath: string, outputPath: string,
filePath: string, filePath: string,
content: string, content: string
) => { ) => {
try { try {
const fullPath = path.join(outputPath, filePath); const fullPath = path.join(outputPath, filePath);
@@ -469,7 +477,7 @@ export const encodeBase64 = (content: string) =>
export const getCreateFileCommand = ( export const getCreateFileCommand = (
outputPath: string, outputPath: string,
filePath: string, filePath: string,
content: string, content: string
) => { ) => {
const fullPath = path.join(outputPath, filePath); const fullPath = path.join(outputPath, filePath);
if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) { if (fullPath.endsWith(path.sep) || filePath.endsWith("/")) {
@@ -509,7 +517,7 @@ export const getServiceContainer = async (appName: string) => {
export const getRemoteServiceContainer = async ( export const getRemoteServiceContainer = async (
serverId: string, serverId: string,
appName: string, appName: string
) => { ) => {
try { try {
const filter = { const filter = {