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(),