diff --git a/prisma/migrations/20241229131352_migration/migration.sql b/prisma/migrations/20241229131352_migration/migration.sql new file mode 100644 index 0000000..d6786c7 --- /dev/null +++ b/prisma/migrations/20241229131352_migration/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "App" ADD COLUMN "containerRegistryPassword" TEXT; +ALTER TABLE "App" ADD COLUMN "containerRegistryUsername" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e463ebb..aea6b32 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -131,7 +131,9 @@ model App { project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) sourceType String @default("GIT") // GIT, CONTAINER - containerImageSource String? + containerImageSource String? + containerRegistryUsername String? + containerRegistryPassword String? gitUrl String? gitBranch String? diff --git a/src/app/project/app/[appId]/general/app-source.tsx b/src/app/project/app/[appId]/general/app-source.tsx index 12ac810..ffe8ade 100644 --- a/src/app/project/app/[appId]/general/app-source.tsx +++ b/src/app/project/app/[appId]/general/app-source.tsx @@ -2,7 +2,7 @@ import { SubmitButton } from "@/components/custom/submit-button"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; import { FormUtils } from "@/frontend/utils/form.utilts"; import { AppSourceInfoInputModel, appSourceInfoInputZodModel } from "@/shared/model/app-source-info.model"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -162,6 +162,38 @@ export default function GeneralAppSource({ app }: { )} /> +
+ + ( + + Registry Username + + + + Only required if your image is stored in a private registry. + + + )} + /> + + ( + + Registry Password + + + + Only required if your image is stored in a private registry. + + + )} + /> +
diff --git a/src/server/services/deployment.service.ts b/src/server/services/deployment.service.ts index e6c9be4..30bf945 100644 --- a/src/server/services/deployment.service.ts +++ b/src/server/services/deployment.service.ts @@ -15,6 +15,7 @@ import { dlog } from "./deployment-logs.service"; import registryService from "./registry.service"; import { EnvVarUtils } from "../utils/env-var.utils"; import configMapService from "./config-map.service"; +import secretService from "./secret.service"; class DeploymentService { @@ -156,6 +157,12 @@ class DeploymentService { } } + const dockerPullSecretName = await secretService.createOrUpdateDockerPullSecret(app); + if (dockerPullSecretName) { + dlog(deploymentId, `Configured credentials to pull Docker Image (${dockerPullSecretName})`); + body.spec!.template!.spec!.imagePullSecrets = [{ name: dockerPullSecretName }]; + } + if (existingDeployment) { dlog(deploymentId, `Replacing existing deployment...`); const res = await k3s.apps.replaceNamespacedDeployment(app.id, app.projectId, body); @@ -163,9 +170,11 @@ class DeploymentService { dlog(deploymentId, `Creating deployment...`); const res = await k3s.apps.createNamespacedDeployment(app.projectId, body); } + dlog(deploymentId, `Cleanup unused ressources from previous deployments...`); await configMapService.deleteUnusedConfigMaps(app); await pvcService.deleteUnusedPvcOfApp(app); await svcService.createOrUpdateService(deploymentId, app); + await secretService.delteUnusedSecrets(app); dlog(deploymentId, `Updating ingress...`); await ingressService.createOrUpdateIngressForApp(deploymentId, app); dlog(deploymentId, `Deployment applied`); diff --git a/src/server/services/secret.service.ts b/src/server/services/secret.service.ts new file mode 100644 index 0000000..53dcc7c --- /dev/null +++ b/src/server/services/secret.service.ts @@ -0,0 +1,80 @@ +import { V1Secret } from "@kubernetes/client-node"; +import k3s from "../adapter/kubernetes-api.adapter"; +import { AppExtendedModel } from "@/shared/model/app-extended.model"; +import { KubeObjectNameUtils } from "../utils/kube-object-name.utils"; + +class SecretService { + + async createOrUpdateDockerPullSecret(app: AppExtendedModel) { + if (this.appNeedsNoSecret(app)) { + return; + } + const dockerImage = app.containerImageSource; + const dockerUsername = app.containerRegistryUsername; + const dockerPassword = app.containerRegistryPassword; + + const secretName = KubeObjectNameUtils.toSecretId(app.id); + const namespace = app.projectId; + let dockerServer = dockerImage!.split("/")[0]; + + // if no registry url is provided, use Docker Hub + if (!dockerServer.includes('.')) { + dockerServer = 'https://index.docker.io/v2/'; + } + + // Create a Docker registry secret + const dockerConfigJson = { + auths: { + [dockerServer]: { + username: dockerUsername, + password: dockerPassword, + //email: dockerEmail, + auth: Buffer.from(`${dockerUsername}:${dockerPassword}`).toString('base64'), + }, + }, + }; + + const secretManifest: V1Secret = { + metadata: { + name: secretName, + }, + data: { + '.dockerconfigjson': Buffer.from(JSON.stringify(dockerConfigJson)).toString('base64'), + }, + type: 'kubernetes.io/dockerconfigjson', + }; + + const existingSecret = await this.getExistingSecret(namespace, app.id); + if (existingSecret) { + console.log(`Updating existing Docker registry secret ${secretName}...`); + await k3s.core.replaceNamespacedSecret(secretName, namespace, secretManifest); + } else { + console.log(`Creating new Docker registry secret ${secretName}...`); + await k3s.core.createNamespacedSecret(namespace, secretManifest); + } + return secretName; + } + + async delteUnusedSecrets(app: AppExtendedModel) { + if (this.appNeedsNoSecret(app)) { + const existingSecret = await this.getExistingSecret(app.projectId, app.id); + if (existingSecret) { + console.log(`Deleting secret ${existingSecret.metadata?.name}...`); + await k3s.core.deleteNamespacedSecret(existingSecret.metadata?.name!, app.projectId); + } + } + } + + private appNeedsNoSecret(app: { id: string; name: string; appType: string; projectId: string; sourceType: string; dockerfilePath: string; replicas: number; envVars: string; createdAt: Date; updatedAt: Date; project: { id: string; name: string; createdAt: Date; updatedAt: Date; }; appDomains: { id: string; createdAt: Date; updatedAt: Date; hostname: string; port: number; useSsl: boolean; redirectHttps: boolean; appId: string; }[]; appVolumes: { id: string; createdAt: Date; updatedAt: Date; appId: string; containerMountPath: string; size: number; accessMode: string; }[]; appPorts: { id: string; createdAt: Date; updatedAt: Date; port: number; appId: string; }[]; appFileMounts: { id: string; createdAt: Date; updatedAt: Date; appId: string; containerMountPath: string; content: string; }[]; containerImageSource?: string | null | undefined; containerRegistryUsername?: string | null | undefined; containerRegistryPassword?: string | null | undefined; gitUrl?: string | null | undefined; gitBranch?: string | null | undefined; gitUsername?: string | null | undefined; gitToken?: string | null | undefined; memoryReservation?: number | null | undefined; memoryLimit?: number | null | undefined; cpuReservation?: number | null | undefined; cpuLimit?: number | null | undefined; }) { + return app.sourceType === 'GIT' || !app.containerImageSource || !app.containerRegistryUsername || !app.containerRegistryPassword; + } + + async getExistingSecret(namespace: string, appId: string) { + const existingSecrets = await k3s.core.listNamespacedSecret(namespace); + const existingSecret = existingSecrets.body.items.find(s => s.metadata?.name === KubeObjectNameUtils.toSecretId(appId)); + return existingSecret; + } +} + +const secretService = new SecretService(); +export default secretService; \ No newline at end of file diff --git a/src/server/utils/kube-object-name.utils.ts b/src/server/utils/kube-object-name.utils.ts index b0d1af7..9d1823d 100644 --- a/src/server/utils/kube-object-name.utils.ts +++ b/src/server/utils/kube-object-name.utils.ts @@ -60,4 +60,8 @@ export class KubeObjectNameUtils { static getConfigMapName(id: string): `cm-${string}` { return `cm-${id}`; } + + static toSecretId(id: string): `secret-${string}` { + return `secret-${id}`; + } } \ No newline at end of file diff --git a/src/shared/model/app-source-info.model.ts b/src/shared/model/app-source-info.model.ts index 6fa22b4..639d683 100644 --- a/src/shared/model/app-source-info.model.ts +++ b/src/shared/model/app-source-info.model.ts @@ -3,18 +3,6 @@ import { z } from "zod"; export const appSourceTypeZodModel = z.enum(["GIT", "CONTAINER"]); export const appTypeZodModel = z.enum(["APP", "POSTGRES", "MYSQL", "MARIADB", "MONGODB"]); -export const appSourceInfoInputZodModel = z.object({ - sourceType: appSourceTypeZodModel, - containerImageSource: z.string().nullish(), - - gitUrl: z.string().trim().nullish(), - gitBranch: z.string().trim().nullish(), - gitUsername: z.string().trim().nullish(), - gitToken: z.string().trim().nullish(), - dockerfilePath: z.string().trim().nullish(), -}); -export type AppSourceInfoInputModel = z.infer; - export const appSourceInfoGitZodModel = z.object({ gitUrl: z.string().trim(), gitBranch: z.string().trim(), @@ -26,5 +14,22 @@ export type AppSourceInfoGitModel = z.infer; export const appSourceInfoContainerZodModel = z.object({ containerImageSource: z.string().trim(), + containerRegistryUsername: z.string().trim().nullish(), + containerRegistryPassword: z.string().trim().nullish(), }); export type AppSourceInfoContainerModel = z.infer; + +export const appSourceInfoInputZodModel = z.object({ + sourceType: appSourceTypeZodModel, + containerImageSource: z.string().nullish(), + containerRegistryUsername: z.string().nullish(), + containerRegistryPassword: z.string().nullish(), + + gitUrl: z.string().trim().nullish(), + gitBranch: z.string().trim().nullish(), + gitUsername: z.string().trim().nullish(), + gitToken: z.string().trim().nullish(), + dockerfilePath: z.string().trim().nullish(), +}); +export type AppSourceInfoInputModel = z.infer; + diff --git a/src/shared/model/generated-zod/app.ts b/src/shared/model/generated-zod/app.ts index 11d4b08..170810c 100644 --- a/src/shared/model/generated-zod/app.ts +++ b/src/shared/model/generated-zod/app.ts @@ -9,6 +9,8 @@ export const AppModel = z.object({ projectId: z.string(), sourceType: z.string(), containerImageSource: z.string().nullish(), + containerRegistryUsername: z.string().nullish(), + containerRegistryPassword: z.string().nullish(), gitUrl: z.string().nullish(), gitBranch: z.string().nullish(), gitUsername: z.string().nullish(),