mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-02-10 21:49:19 -06:00
feat/add container registry credentials to App model and update related components
This commit is contained in:
3
prisma/migrations/20241229131352_migration/migration.sql
Normal file
3
prisma/migrations/20241229131352_migration/migration.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "App" ADD COLUMN "containerRegistryPassword" TEXT;
|
||||
ALTER TABLE "App" ADD COLUMN "containerRegistryUsername" TEXT;
|
||||
@@ -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?
|
||||
|
||||
@@ -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 }: {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="containerRegistryUsername"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} value={field.value as string | number | readonly string[] | undefined} />
|
||||
</FormControl>
|
||||
<FormDescription>Only required if your image is stored in a private registry.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="containerRegistryPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Registry Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="password" {...field} value={field.value as string | number | readonly string[] | undefined} />
|
||||
</FormControl>
|
||||
<FormDescription>Only required if your image is stored in a private registry.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
|
||||
@@ -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`);
|
||||
|
||||
80
src/server/services/secret.service.ts
Normal file
80
src/server/services/secret.service.ts
Normal file
@@ -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;
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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<typeof appSourceInfoInputZodModel>;
|
||||
|
||||
export const appSourceInfoGitZodModel = z.object({
|
||||
gitUrl: z.string().trim(),
|
||||
gitBranch: z.string().trim(),
|
||||
@@ -26,5 +14,22 @@ export type AppSourceInfoGitModel = z.infer<typeof appSourceInfoGitZodModel>;
|
||||
|
||||
export const appSourceInfoContainerZodModel = z.object({
|
||||
containerImageSource: z.string().trim(),
|
||||
containerRegistryUsername: z.string().trim().nullish(),
|
||||
containerRegistryPassword: z.string().trim().nullish(),
|
||||
});
|
||||
export type AppSourceInfoContainerModel = z.infer<typeof appSourceInfoContainerZodModel>;
|
||||
|
||||
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<typeof appSourceInfoInputZodModel>;
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user