feat: implement delete all network policies functionality and added network policies for db tools

This commit is contained in:
biersoeckli
2025-12-22 14:30:12 +00:00
parent c80fcd4b07
commit 2f1a169398
9 changed files with 190 additions and 11 deletions

View File

@@ -1,13 +1,13 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cleanupOldBuildJobs, cleanupOldTmpFiles, deleteAllFailedAndSuccededPods, deleteOldAppLogs, purgeRegistryImages, updateRegistry } from "../server/actions";
import { cleanupOldBuildJobs, cleanupOldTmpFiles, deleteAllFailedAndSuccededPods, deleteAllNetworkPolicies, deleteOldAppLogs, purgeRegistryImages, updateRegistry } from "../server/actions";
import { Button } from "@/components/ui/button";
import { Toast } from "@/frontend/utils/toast.utils";
import { useConfirmDialog } from "@/frontend/states/zustand.states";
import { LogsDialog } from "@/components/custom/logs-overlay";
import { Constants } from "@/shared/utils/constants";
import { RotateCcw, SquareTerminal, Trash } from "lucide-react";
import { RotateCcw, SquareTerminal, Trash, ShieldOff } from "lucide-react";
export default function QuickStackMaintenanceSettings({
qsPodName
@@ -97,5 +97,23 @@ export default function QuickStackMaintenanceSettings({
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Network Policies</CardTitle>
</CardHeader>
<CardContent className="flex gap-4 flex-wrap">
<Button variant="destructive" onClick={async () => {
if (await useConfirm.openConfirmDialog({
title: '⚠️ Delete All Network Policies',
description: 'WARNING: This is a bad idea! This action will delete ALL network policies across all namespaces. Your applications will lose all network security restrictions. Only use this for troubleshooting or emergency situations. Are you absolutely sure?',
okButton: "Yes, Delete All Policies",
})) {
Toast.fromAction(() => deleteAllNetworkPolicies());
}
}}><ShieldOff /> Delete All Network Policies</Button>
</CardContent>
</Card>
</div>;
}

View File

@@ -19,6 +19,7 @@ import maintenanceService from "@/server/services/standalone-services/maintenanc
import appLogsService from "@/server/services/standalone-services/app-logs.service";
import systemBackupService from "@/server/services/standalone-services/system-backup.service";
import backupService from "@/server/services/standalone-services/backup.service";
import networkPolicyService from "@/server/services/network-policy.service";
export const updateIngressSettings = async (prevState: any, inputData: QsIngressSettingsModel) =>
saveFormAction(inputData, qsIngressSettingsZodModel, async (validatedData) => {
@@ -194,4 +195,13 @@ export const runSystemBackupNow = async () =>
await backupService.runSystemBackup();
return new SuccessActionResult(undefined, 'System backup started successfully');
});
export const deleteAllNetworkPolicies = async () =>
simpleAction(async () => {
await getAdminUserSession();
const deletedCount = await networkPolicyService.deleteAllNetworkPolicies();
return new SuccessActionResult(undefined, `Successfully deleted all (${deletedCount}) network policies.`);
});

View File

@@ -11,6 +11,7 @@ import svcService from "../svc.service";
import podService from "../pod.service";
import appService from "../app.service";
import { AppExtendedModel } from "@/shared/model/app-extended.model";
import networkPolicyService from "../network-policy.service";
export class BaseDbToolService {
@@ -78,7 +79,7 @@ export class BaseDbToolService {
const hostnameDnsProviderHostname = await hostnameDnsProviderService.getDomainForApp(toolAppName);
console.log(`Creating DB Tool ${toolAppName} deployment for app ${appId}`);
await this.createOrUpdateDbGateDeployment(app, deplyomentBuilder);
await this.createOrUpdateDbToolDeplyoment(app, deplyomentBuilder);
console.log(`Creating service for DB Tool ${toolAppName} for app ${appId}`);
await svcService.createOrUpdateService(namespace, toolAppName, [{
@@ -90,6 +91,9 @@ export class BaseDbToolService {
console.log(`Creating ingress for DB Tool ${toolAppName} for app ${appId}`);
await this.createOrUpdateIngress(toolAppName, namespace, hostnameDnsProviderHostname);
console.log(`Creating network policy for DB Tool ${toolAppName} for app ${appId}`);
await networkPolicyService.reconcileDbToolNetworkPolicy(toolAppName, appId, namespace);
const fileBrowserPods = await podService.getPodsForApp(namespace, toolAppName);
for (const pod of fileBrowserPods) {
await podService.waitUntilPodIsRunningFailedOrSucceded(namespace, pod.podName);
@@ -97,7 +101,7 @@ export class BaseDbToolService {
}
private async createOrUpdateDbGateDeployment(app: AppExtendedModel, deplyomentBuilder: (app: AppExtendedModel) => V1Deployment | Promise<V1Deployment>) {
private async createOrUpdateDbToolDeplyoment(app: AppExtendedModel, deplyomentBuilder: (app: AppExtendedModel) => V1Deployment | Promise<V1Deployment>) {
const body = await deplyomentBuilder(app);
const toolAppName = this.appIdToToolNameConverter(app.id);
await deploymentService.applyDeployment(app.projectId, toolAppName, body);
@@ -128,6 +132,8 @@ export class BaseDbToolService {
// do not delete ingress to reduce cert-manager issues --> todo; add cleanup function in maintenance section
//await k3s.network.deleteNamespacedIngress(KubeObjectNameUtils.getIngressName(toolAppName), projectId);
}
await networkPolicyService.deleteDbToolNetworkPolicy(toolAppName, projectId);
}
private async createOrUpdateIngress(dbGateAppName: string, namespace: string, hostname: string) {

View File

@@ -108,7 +108,8 @@ class DbGateService extends BaseDbToolService {
template: {
metadata: {
labels: {
app: dbGateAppName
app: dbGateAppName,
[Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_TOOL
},
annotations: {
[Constants.QS_ANNOTATION_APP_ID]: app.id,

View File

@@ -62,7 +62,8 @@ class PgAdminService extends BaseDbToolService {
template: {
metadata: {
labels: {
app: appName
app: appName,
[Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_TOOL
},
annotations: {
[Constants.QS_ANNOTATION_APP_ID]: app.id,

View File

@@ -45,7 +45,8 @@ class PhpMyAdminService extends BaseDbToolService {
template: {
metadata: {
labels: {
app: appName
app: appName,
[Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_TOOL
},
annotations: {
[Constants.QS_ANNOTATION_APP_ID]: app.id,

View File

@@ -45,7 +45,6 @@ class NetworkPolicyService {
egress: this.getEgressRules(egressPolicy)
}
};
console.log(JSON.stringify(policy, null, 2));
await this.applyNetworkPolicy(namespace, policyName, policy);
}
@@ -93,6 +92,14 @@ class NetworkPolicyService {
}
}];
const dbToolPod: V1NetworkPolicyPeer[] = [{
podSelector: {
matchLabels: {
[Constants.QS_ANNOTATION_CONTAINER_TYPE]: Constants.QS_ANNOTATION_CONTAINER_TYPE_DB_TOOL
}
}
}];
if (policyType === 'ALLOW_ALL') {
// Allow from same namespace and from Traefik (internet traffic comes through Traefik)
rules.push({
@@ -109,7 +116,8 @@ class NetworkPolicyService {
rules.push({
from: [
...traefikFrom,
...backupPodFrom
...backupPodFrom,
...dbToolPod
]
});
} else if (policyType === 'NAMESPACE_ONLY') {
@@ -123,7 +131,8 @@ class NetworkPolicyService {
// No rules means deny all --> except the separate container for database backups
rules.push({
from: [
...backupPodFrom
...backupPodFrom,
...dbToolPod
]
});
}
@@ -240,6 +249,138 @@ class NetworkPolicyService {
const allPolicies = await k3s.network.listNamespacedNetworkPolicy(namespace);
return allPolicies.body.items.find(np => np.metadata?.name === policyName);
}
async reconcileDbToolNetworkPolicy(dbToolAppName: string, dbAppId: string, projectId: string) {
const policyName = KubeObjectNameUtils.toNetworkPolicyName(dbToolAppName);
const namespace = projectId;
const policy: V1NetworkPolicy = {
apiVersion: "networking.k8s.io/v1",
kind: "NetworkPolicy",
metadata: {
name: policyName,
namespace: namespace,
labels: {
app: dbToolAppName,
'db-tool': 'true'
},
annotations: {
[Constants.QS_ANNOTATION_APP_ID]: dbAppId,
[Constants.QS_ANNOTATION_PROJECT_ID]: projectId,
}
},
spec: {
podSelector: {
matchLabels: {
app: dbToolAppName
}
},
policyTypes: ["Ingress", "Egress"],
ingress: [
{
// Allow from Traefik (internet traffic)
from: [
{
namespaceSelector: {
matchLabels: {
'kubernetes.io/metadata.name': 'kube-system'
}
},
podSelector: {
matchLabels: {
'app.kubernetes.io/name': 'traefik'
}
}
}
]
}
],
egress: [
{
// Allow DNS
to: [
{
namespaceSelector: {
matchLabels: {
"kubernetes.io/metadata.name": "kube-system"
}
},
podSelector: {
matchLabels: {
"k8s-app": "kube-dns"
}
}
},
{
namespaceSelector: {
matchLabels: {
"kubernetes.io/metadata.name": "kube-system"
}
},
podSelector: {
matchLabels: {
"k8s-app": "coredns"
}
}
}
],
ports: [
{ protocol: 'UDP', port: 53 as any },
{ protocol: 'TCP', port: 53 as any }
]
},
{
// Allow only to database pod in same namespace
to: [
{
podSelector: {
matchLabels: {
app: dbAppId
}
}
}
]
}
]
}
};
console.log('Creating DB Tool Network Policy:', JSON.stringify(policy, null, 2));
await this.applyNetworkPolicy(namespace, policyName, policy);
}
async deleteDbToolNetworkPolicy(dbToolAppName: string, projectId: string) {
const policyName = KubeObjectNameUtils.toNetworkPolicyName(dbToolAppName);
const existingNetworkPolicy = await this.getExistingNetworkPolicy(projectId, policyName);
if (!existingNetworkPolicy) {
return;
}
await k3s.network.deleteNamespacedNetworkPolicy(policyName, projectId);
}
async deleteAllNetworkPolicies() {
const namespaces = await k3s.core.listNamespace();
let deletedCount = 0;
for (const ns of namespaces.body.items) {
const namespace = ns.metadata?.name;
if (!namespace) continue;
try {
const policies = await k3s.network.listNamespacedNetworkPolicy(namespace);
for (const policy of policies.body.items) {
const policyName = policy.metadata?.name;
if (policyName) {
await k3s.network.deleteNamespacedNetworkPolicy(policyName, namespace);
deletedCount++;
}
}
} catch (error) {
console.error(`Error deleting network policies in namespace ${namespace}:`, error);
}
}
return deletedCount;
}
}
const networkPolicyService = new NetworkPolicyService();

View File

@@ -82,6 +82,6 @@ export class KubeObjectNameUtils {
}
static toNetworkPolicyName(appId: string): string {
return `np-${appId}`;
return `np-${appId}`.substring(0, 63); // not more than 63 characters
}
}

View File

@@ -3,6 +3,7 @@ export class Constants {
static readonly QS_ANNOTATION_PROJECT_ID = 'qs-project-id';
static readonly QS_ANNOTATION_CONTAINER_TYPE = 'qs-containter-type';
static readonly QS_ANNOTATION_CONTAINER_TYPE_DB_BACKUP_JOB = 'qs-job-database-backup';
static readonly QS_ANNOTATION_CONTAINER_TYPE_DB_TOOL = 'qs-container-db-tool';
static readonly QS_ANNOTATION_DEPLOYMENT_ID = 'qs-deplyoment-id';
static readonly QS_ANNOTATION_GIT_COMMIT = 'qs-git-commit';
static readonly K3S_JOIN_TOKEN = 'k3sJoinToken';