Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/app/project/app/[appId]/overview/deployments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default function BuildsTab({
['status', 'Status', true, (item) => <DeploymentStatusBadge>{item.status}</DeploymentStatusBadge>],
["startTime", "Started At", true, (item) => formatDateTime(item.createdAt)],
['gitCommit', 'Git Commit', true, (item) => <ShortCommitHash>{item.gitCommit}</ShortCommitHash>],
['gitCommitMessage', 'Commit Message', true, (item) => <span className="text-muted-foreground text-sm">{item.gitCommitMessage ?? ''}</span>],
]}
data={appBuilds}
hideSearchBar={true}
Expand Down
40 changes: 40 additions & 0 deletions src/app/settings/server/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,46 @@ import { TraefikIpPropagationStatus } from "@/shared/model/traefik-ip-propagatio
import k3sUpdateService from "@/server/services/upgrade-services/k3s-update.service";
import longhornUpdateService from "@/server/services/upgrade-services/longhorn-update.service";
import longhornUiService from "@/server/services/longhorn-ui.service";
import { BuildSettingsModel, buildSettingsZodModel } from "@/shared/model/build-settings.model";

export const saveBuildSettings = async (prevState: any, inputData: BuildSettingsModel) =>
saveFormAction(inputData, buildSettingsZodModel, async (validatedData) => {
await getAdminUserSession();

const saveOrDelete = async (key: string, value: string | number | null | undefined) => {
if (value !== null && value !== undefined && value !== '') {
await paramService.save({ name: key, value: String(value) });
} else {
await paramService.deleteByNameIfExists(key);
}
};

// Resource limits only apply when using k3s native scheduling
if (validatedData.buildNode === Constants.BUILD_NODE_K3S_NATIVE_VALUE) {
await saveOrDelete(ParamService.BUILD_MEMORY_LIMIT, validatedData.memoryLimit);
await saveOrDelete(ParamService.BUILD_MEMORY_RESERVATION, validatedData.memoryReservation);
await saveOrDelete(ParamService.BUILD_CPU_LIMIT, validatedData.cpuLimit);
await saveOrDelete(ParamService.BUILD_CPU_RESERVATION, validatedData.cpuReservation);
} else {
await paramService.deleteByNameIfExists(ParamService.BUILD_MEMORY_LIMIT);
await paramService.deleteByNameIfExists(ParamService.BUILD_MEMORY_RESERVATION);
await paramService.deleteByNameIfExists(ParamService.BUILD_CPU_LIMIT);
await paramService.deleteByNameIfExists(ParamService.BUILD_CPU_RESERVATION);
}
await saveOrDelete(ParamService.BUILD_NODE, validatedData.buildNode);
});

export const getBuildSettings = async (): Promise<BuildSettingsModel> => {
await getAdminUserSession();
const [memoryLimit, memoryReservation, cpuLimit, cpuReservation, buildNode] = await Promise.all([
paramService.getNumber(ParamService.BUILD_MEMORY_LIMIT),
paramService.getNumber(ParamService.BUILD_MEMORY_RESERVATION),
paramService.getNumber(ParamService.BUILD_CPU_LIMIT),
paramService.getNumber(ParamService.BUILD_CPU_RESERVATION),
paramService.getString(ParamService.BUILD_NODE),
]);
return { memoryLimit, memoryReservation, cpuLimit, cpuReservation, buildNode };
};

export const setNodeStatus = async (nodeName: string, schedulable: boolean) =>
simpleAction(async () => {
Expand Down
17 changes: 14 additions & 3 deletions src/app/settings/server/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import { TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import QuickStackMaintenanceSettings from "./qs-maintenance-settings";
import podService from "@/server/services/pod.service";
import { ServerSettingsTabs } from "./server-settings-tabs";
import { Settings, Network, HardDrive, Rocket, Wrench } from "lucide-react";
import { Settings, Network, HardDrive, Rocket, Wrench, Hammer } from "lucide-react";
import QsBuildSettings from "./qs-build-settings";
import { getBuildSettings } from "./actions";
import quickStackUpdateService from "@/server/services/qs-update.service";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import clusterService from "@/server/services/cluster.service";
Expand Down Expand Up @@ -57,13 +59,15 @@ export default async function ProjectPage({
traefikStatus,
qsPodInfos,
newVersionInfo,
nodeInfo
nodeInfo,
buildSettings
] = await Promise.all([
s3TargetService.getAll(),
traefikService.getStatus(),
podService.getPodsForApp(Constants.QS_NAMESPACE, Constants.QS_APP_NAME),
quickStackUpdateService.getNewVersionInfo(),
clusterService.getNodeInfo()
clusterService.getNodeInfo(),
getBuildSettings()
]);

const qsPodInfo = qsPodInfos.find(p => !!p);
Expand All @@ -90,6 +94,7 @@ export default async function ProjectPage({
<TabsTrigger value="general"><Settings className="mr-2 h-4 w-4" />General</TabsTrigger>
<TabsTrigger value="networking"><Network className="mr-2 h-4 w-4" />Networking / Traefik</TabsTrigger>
<TabsTrigger value="storage"><HardDrive className="mr-2 h-4 w-4" />Storage & Backups</TabsTrigger>
<TabsTrigger value="builds"><Hammer className="mr-2 h-4 w-4" />Builds</TabsTrigger>
<TabsTrigger value="cluster"><Network className="mr-2 h-4 w-4" />Cluster</TabsTrigger>
<TabsTrigger value="updates"><Rocket className="mr-2 h-4 w-4" />Updates {newVersionInfo && <div className="h-2 w-2 ml-2 rounded-full bg-orange-500 animate-pulse" />}</TabsTrigger>
<TabsTrigger value="maintenance"><Wrench className="mr-2 h-4 w-4" />Maintenance</TabsTrigger>
Expand Down Expand Up @@ -118,6 +123,12 @@ export default async function ProjectPage({
</div>
</TabsContent>

<TabsContent value="builds" className="space-y-4">
<div className="grid gap-6">
<QsBuildSettings buildSettings={buildSettings} nodes={nodeInfo} />
</div>
</TabsContent>

<TabsContent value="cluster" className="space-y-4">
<NodeInfo nodeInfos={nodeInfo} clusterJoinToken={clusterJoinToken} />
</TabsContent>
Expand Down
184 changes: 184 additions & 0 deletions src/app/settings/server/qs-build-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
'use client';

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 { FormUtils } from "@/frontend/utils/form.utilts";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { useFormState } from "react-dom";
import { ServerActionResult } from "@/shared/model/server-action-error-return.model";
import { Input } from "@/components/ui/input";
import { BuildSettingsModel, buildSettingsZodModel } from "@/shared/model/build-settings.model";
import { useEffect } from "react";
import { toast } from "sonner";
import { saveBuildSettings } from "./actions";
import { NodeInfoModel } from "@/shared/model/node-info.model";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertCircle } from "lucide-react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Constants } from "@/shared/utils/constants";

export default function QsBuildSettings({
buildSettings,
nodes,
}: {
buildSettings: BuildSettingsModel;
nodes: NodeInfoModel[];
}) {
const form = useForm<BuildSettingsModel>({
resolver: zodResolver(buildSettingsZodModel),
defaultValues: {
...buildSettings,
buildNode: buildSettings.buildNode || Constants.BUILD_AUTO_NODE_VALUE,
},
});

const [state, formAction] = useFormState(
(state: ServerActionResult<any, any>, payload: BuildSettingsModel) => saveBuildSettings(state, payload),
FormUtils.getInitialFormState<typeof buildSettingsZodModel>()
);

useEffect(() => {
if (state.status === 'success') {
toast.success('Build settings saved.');
}
FormUtils.mapValidationErrorsToForm<typeof buildSettingsZodModel>(state, form);
}, [state]);

const watchedBuildNode = form.watch('buildNode');
const isK3sNative = watchedBuildNode === Constants.BUILD_NODE_K3S_NATIVE_VALUE;
const showReservationAlert = !buildSettings.memoryReservation || !buildSettings.cpuReservation;

return (
<Card>
<CardHeader>
<CardTitle>Build Container Settings</CardTitle>
<CardDescription>
Configure global resource limits and node placement for all build containers.
</CardDescription>
</CardHeader>
<Form {...form}>
<form action={(e) => form.handleSubmit((data) => {
const payload = {
...data,
buildNode: data.buildNode === Constants.BUILD_AUTO_NODE_VALUE || data.buildNode === '' ? null : data.buildNode,
};
return formAction(payload);
})()}>
<CardContent className="space-y-6">

<FormField
control={form.control}
name="buildNode"
render={({ field }) => (
<FormItem>
<FormLabel>Build Node (optional)</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value || Constants.BUILD_AUTO_NODE_VALUE}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Auto (node with most available resources)" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={Constants.BUILD_AUTO_NODE_VALUE}>
Auto (node with most available resources)
</SelectItem>
<SelectItem value={Constants.BUILD_NODE_K3S_NATIVE_VALUE}>
k3s native
</SelectItem>
{nodes.map((node) => (
<SelectItem
key={node.name}
value={node.name}
disabled={!node.schedulable}
>
{node.name}{!node.schedulable ? ' (not schedulable)' : ''}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{isK3sNative && showReservationAlert && (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertTitle>Reservations not configured</AlertTitle>
<AlertDescription>
No CPU and/or memory reservations are set. Setting them is recommended for optimal build container scheduling.
</AlertDescription>
</Alert>
)}

{isK3sNative && <div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="memoryLimit"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Limit (MB)</FormLabel>
<FormControl>
<Input type="number" {...field} value={field.value as string | number | undefined ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="memoryReservation"
render={({ field }) => (
<FormItem>
<FormLabel>Memory Reservation (MB)</FormLabel>
<FormControl>
<Input type="number" {...field} value={field.value as string | number | undefined ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="cpuLimit"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Limit (m)</FormLabel>
<FormControl>
<Input type="number" {...field} value={field.value as string | number | undefined ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="cpuReservation"
render={({ field }) => (
<FormItem>
<FormLabel>CPU Reservation (m)</FormLabel>
<FormControl>
<Input type="number" {...field} value={field.value as string | number | undefined ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>}
</CardContent>
<CardFooter className="gap-4">
<SubmitButton>Save</SubmitButton>
<p className="text-red-500">{state?.message}</p>
</CardFooter>
</form>
</Form>
</Card>
);
}
4 changes: 2 additions & 2 deletions src/server/services/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ class AppService {

if (app.sourceType === 'GIT') {
// first make build
const [buildJobName, gitCommitHash, buildPromise] = await buildService.buildApp(deploymentId, app, forceBuild);
const [buildJobName, gitCommitHash, gitCommitMessage, buildPromise] = await buildService.buildApp(deploymentId, app, forceBuild);
buildPromise.then(async () => {
console.log('Build job finished, deploying...');
dlog(deploymentId, `Starting deployment with output from build "${buildJobName}"`);
await deploymentService.createDeployment(deploymentId, app, buildJobName, gitCommitHash);
await deploymentService.createDeployment(deploymentId, app, buildJobName, gitCommitHash, gitCommitMessage);
});
} else {
// only deploy
Expand Down
Loading
Loading