fixed build and deploy

This commit is contained in:
biersoeckli
2024-11-08 13:37:06 +00:00
parent 3e053c1ae7
commit cb3fd00cae
6 changed files with 213 additions and 35 deletions

View File

@@ -0,0 +1,27 @@
// Prevents this route's response from being cached
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const encoder = new TextEncoder()
// Create a streaming response
const customReadable = new ReadableStream({
start(controller) {
const message = "A sample message."
controller.enqueue(encoder.encode(`data: ${message}\n\n`))
},
cancel() {
console.log("Stream closed.")
}
})
// Return the stream response and keep the connection alive
return new Response(customReadable, {
// Set the headers for Server-Sent Events (SSE)
headers: {
Connection: "keep-alive",
"Content-Encoding": "none",
"Cache-Control": "no-cache, no-transform",
"Content-Type": "text/event-stream; charset=utf-8",
},
})
}

View File

@@ -8,6 +8,7 @@ import { ServiceException } from "@/model/service.exception.model";
import { StringUtils } from "../utils/string.utils";
import deploymentService from "./deployment.service";
import buildService, { buildNamespace } from "./build.service";
import namespaceService from "./namespace.service";
class AppService {
@@ -15,7 +16,7 @@ class AppService {
const app = await this.getExtendedById(appId);
if (app.sourceType === 'GIT') {
// first make build
await deploymentService.createNamespaceIfNotExists(buildNamespace);
await namespaceService.createNamespaceIfNotExists(buildNamespace);
const [buildJobName, buildPromise] = await buildService.buildApp(app);
buildPromise.then(async () => {
console.warn('Build job finished, deploying...');

View File

@@ -5,15 +5,17 @@ import { StringUtils } from "../utils/string.utils";
import { BuildJobModel } from "@/model/build-job";
import { ServiceException } from "@/model/service.exception.model";
import { PodsInfoModel } from "@/model/pods-info.model";
import namespaceService from "./namespace.service";
const kanikoImage = "gcr.io/kaniko-project/executor:latest";
export const registryURL = "registry-svc.registry-and-build.svc.cluster.local"
export const registryURLExternal = "localhost:30100"
export const registryURLInternal = "registry-svc.registry-and-build.svc.cluster.local:5000"
export const buildNamespace = "registry-and-build";
class BuildService {
async buildApp(app: AppExtendedModel): Promise<[string, Promise<void>]> {
await this.deployRegistryIfNotExists();
const runningJobsForApp = await this.getBuildsForApp(app.id);
if (runningJobsForApp.some((job) => job.status === 'RUNNING')) {
throw new ServiceException("A build job is already running for this app.");
@@ -35,9 +37,13 @@ class BuildService {
{
name: buildName,
image: kanikoImage,
args: [`--dockerfile=${app.dockerfilePath}`,
`--context=${app.gitUrl!.replace("https://", "git://")}#refs/heads/${app.gitBranch}`,
`--destination=${this.createContainerRegistryUrlForAppId(app.id)}`]
args: [
`--dockerfile=${app.dockerfilePath}`,
`--insecure`,
`--log-format=text`,
`--context=${app.gitUrl!.replace("https://", "git://")}#refs/heads/${app.gitBranch}`,
`--destination=${this.createInternalContainerRegistryUrlForAppId(app.id)}`
]
},
],
restartPolicy: "Never",
@@ -60,18 +66,24 @@ class BuildService {
];
}
await k3s.batch.createNamespacedJob(buildNamespace, jobDefinition);
//revalidateTag(Tags.appBuilds(app.id));
const buildJobPromise = this.waitForJobCompletion(jobDefinition.metadata!.name!)
return [buildName, buildJobPromise];
}
createInternalContainerRegistryUrlForAppId(appId?: string) {
if (!appId) {
return undefined;
}
return `${registryURLInternal}/${appId}:latest`;
}
createContainerRegistryUrlForAppId(appId?: string) {
if (!appId) {
return undefined;
}
return `${registryURL}/${appId}:latest`;
return `${registryURLExternal}/${appId}:latest`;
}
async deleteBuild(buildName: string) {
@@ -171,6 +183,123 @@ class BuildService {
return 'UNKNOWN';
}
async deployRegistryIfNotExists() {
const deployments = await k3s.apps.listNamespacedDeployment(buildNamespace);
if (deployments.body.items.length > 0) {
return;
}
console.log("Deploying registry because it is not deployed...");
// Create Namespace
console.log("Creating namespace...");
await namespaceService.createNamespaceIfNotExists(buildNamespace);
// Create PersistentVolumeClaim
console.log("Creating Registry PVC...");
const pvcManifest = {
apiVersion: 'v1',
kind: 'PersistentVolumeClaim',
metadata: {
name: 'registry-data-pvc',
namespace: buildNamespace,
},
spec: {
accessModes: ['ReadWriteOnce'],
storageClassName: 'longhorn',
resources: {
requests: {
storage: '5Gi',
},
},
},
};
await k3s.core.createNamespacedPersistentVolumeClaim(buildNamespace, pvcManifest)
// Create Deployment
console.log("Creating Registry Deployment...");
const deploymentManifest = {
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: {
name: 'registry',
namespace: buildNamespace,
},
spec: {
replicas: 1,
strategy: {
type: 'Recreate',
},
selector: {
matchLabels: {
app: 'registry',
},
},
template: {
metadata: {
labels: {
app: 'registry',
},
},
spec: {
containers: [
{
name: 'registry',
image: 'registry:latest',
volumeMounts: [
{
name: 'registry-data-pv',
mountPath: '/var/lib/registry',
},
],
},
],
volumes: [
{
name: 'registry-data-pv',
persistentVolumeClaim: {
claimName: 'registry-data-pvc',
},
},
],
},
},
},
};
await k3s.apps.createNamespacedDeployment(buildNamespace, deploymentManifest);
// Create Service
console.log("Creating Registry Service...");
const serviceManifest = {
apiVersion: 'v1',
kind: 'Service',
metadata: {
name: 'registry-svc',
namespace: buildNamespace,
},
spec: {
selector: {
app: 'registry',
},
ports: [
{
nodePort: 30100,
protocol: 'TCP',
port: 5000,
targetPort: 5000,
},
],
type: 'NodePort',
},
};
await k3s.core.createNamespacedService(buildNamespace, serviceManifest);
console.log("Registry deployed successfully.");
}
}
const buildService = new BuildService();

View File

@@ -10,14 +10,9 @@ import { PodsInfoModel } from "@/model/pods-info.model";
import { StringUtils } from "../utils/string.utils";
import pvcService from "./pvc.service";
import ingressService from "./ingress.service";
import namespaceService from "./namespace.service";
class DeploymentService {
async getNamespaces() {
const k3sResponse = await k3s.core.listNamespace();
return k3sResponse.body.items.map((item) => item.metadata?.name).filter((name) => !!name);
}
async getDeployment(projectId: string, appId: string) {
const allDeployments = await k3s.apps.listNamespacedDeployment(projectId);
if (allDeployments.body.items.some((item) => item.metadata?.name === appId)) {
@@ -99,7 +94,7 @@ class DeploymentService {
async createDeployment(app: AppExtendedModel, buildJobName?: string) {
await this.validateDeployment(app);
await this.createNamespaceIfNotExists(app.projectId);
await namespaceService.createNamespaceIfNotExists(app.projectId);
const appHasPvcChanges = await pvcService.doesAppConfigurationIncreaseAnyPvcSize(app)
if (appHasPvcChanges) {
await this.setReplicasForDeployment(app.projectId, app.id, 0); // update of PVCs is only possible if deployment is scaled down
@@ -187,25 +182,6 @@ class DeploymentService {
return k3s.apps.replaceNamespacedDeployment(appId, projectId, existingDeployment);
}
async createNamespaceIfNotExists(namespace: string) {
const existingNamespaces = await this.getNamespaces();
if (existingNamespaces.includes(namespace)) {
return;
}
await k3s.core.createNamespace({
metadata: {
name: namespace
}
});
}
async deleteNamespace(namespace: string) {
const nameSpaces = await this.getNamespaces();
if (nameSpaces.includes(namespace)) {
await k3s.core.deleteNamespace(namespace);
}
}
async getPodsForApp(projectId: string, appId: string) {
const res = await k3s.core.listNamespacedPod(projectId, undefined, undefined, undefined, undefined, `app=${appId}`);
return res.body.items.map((item) => ({

View File

@@ -0,0 +1,44 @@
import { AppExtendedModel } from "@/model/app-extended.model";
import k3s from "../adapter/kubernetes-api.adapter";
import { V1Deployment, V1Ingress, V1PersistentVolumeClaim } from "@kubernetes/client-node";
import buildService from "./build.service";
import { ListUtils } from "../utils/list.utils";
import { DeploymentInfoModel, DeplyomentStatus } from "@/model/deployment-info.model";
import { BuildJobStatus } from "@/model/build-job";
import { ServiceException } from "@/model/service.exception.model";
import { PodsInfoModel } from "@/model/pods-info.model";
import { StringUtils } from "../utils/string.utils";
import pvcService from "./pvc.service";
import ingressService from "./ingress.service";
class NamespaceService {
async getNamespaces() {
const k3sResponse = await k3s.core.listNamespace();
return k3sResponse.body.items.map((item) => item.metadata?.name).filter((name) => !!name);
}
async createNamespaceIfNotExists(namespace: string) {
const existingNamespaces = await this.getNamespaces();
if (existingNamespaces.includes(namespace)) {
return;
}
await k3s.core.createNamespace({
metadata: {
name: namespace
}
});
}
async deleteNamespace(namespace: string) {
const nameSpaces = await this.getNamespaces();
if (nameSpaces.includes(namespace)) {
await k3s.core.deleteNamespace(namespace);
}
}
}
const namespaceService = new NamespaceService();
export default namespaceService;

View File

@@ -4,6 +4,7 @@ import { Tags } from "../utils/cache-tag-generator.utils";
import { Prisma, Project } from "@prisma/client";
import { StringUtils } from "../utils/string.utils";
import deploymentService from "./deployment.service";
import namespaceService from "./namespace.service";
class ProjectService {
@@ -55,7 +56,7 @@ class ProjectService {
data: item as Prisma.ProjectUncheckedCreateInput
});
}
await deploymentService.createNamespaceIfNotExists(savedItem.id);
await namespaceService.createNamespaceIfNotExists(savedItem.id);
} finally {
revalidateTag(Tags.projects());
}