mirror of
https://github.com/biersoeckli/QuickStack.git
synced 2026-01-01 17:20:14 -06:00
fixed build and deploy
This commit is contained in:
27
src/app/api/pods-status/route.ts
Normal file
27
src/app/api/pods-status/route.ts
Normal 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",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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...');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
44
src/server/services/namespace.service.ts
Normal file
44
src/server/services/namespace.service.ts
Normal 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;
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user