chore(deps): upgrade all dependencies to latest stable

Bring backend and frontend to the latest stable releases (no pre-releases),
including major upgrades that required code migration. Both projects pass
typecheck and production builds.

Backend
- NestJS 10 -> 11 (common/core/platform-express/jwt/passport/bull/cli/
  schematics/testing), @nestjs/config 3->4, @nestjs/swagger 7->11,
  @nestjs/typeorm 10->11
- @kubernetes/client-node 0.21 -> 1.4: migrate ~200+ call sites across 6
  services to the v1 single-object argument API, unwrapped responses, err.code,
  setHeaderOptions for patch content-type, applyToHTTPSOptions. Add regression
  spec k8s-client-v1-migration.spec.ts.
- typeorm 0.3 -> 1.0: relations/select string arrays -> object form
- uuid 9->14 (drops @types/uuid), multer 1->2, bcrypt 5->6, helmet 7->8,
  class-validator 0.14->0.15
- TypeScript 5->6, ESLint 8->9, @typescript-eslint 6->8, jest 29->30,
  @types/node 20->24; tsconfig: strictPropertyInitialization:false,
  ignoreDeprecations, rootDir, explicit types[]
- @nestjs/config 4: jwt.strategy uses getOrThrow; @types/express kept at 4
  (Nest 11 runs Express 4)

Frontend
- React 18->19, Next 14->16 (async params via official codemod),
  Tailwind 3->4 (@tailwindcss/postcss, @import + @config, inline custom @apply),
  framer-motion 11->12, zustand 4->5, three 0.169->0.184, @react-three/* majors
- TypeScript 5->6 (tsconfig target es5->ES2017), ESLint 8->9,
  eslint-config-next 14->16

Infra/docs
- Dockerfiles node:20-alpine -> node:24-alpine (require-esm for k8s client)
- Add UPGRADE.md / UPGRADE.en.md; refresh README tech-stack versions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-14 18:05:33 +03:30
parent 23386b73de
commit 8b77656bb7
31 changed files with 8974 additions and 7684 deletions
+72 -118
View File
@@ -1,21 +1,10 @@
import {
Injectable,
Logger,
BadRequestException,
Inject,
forwardRef,
} from '@nestjs/common';
import { Injectable, Logger, BadRequestException, Inject, forwardRef } from '@nestjs/common';
import * as k8s from '@kubernetes/client-node';
import { ClustersService } from './clusters.service';
import { HelmService } from '../kubernetes/helm.service';
import { ElasticsearchService } from '../kubernetes/elasticsearch.service';
import { registerKubeconfigNoProxy } from '../common/kubernetes-proxy.util';
import {
ClusterToolDefinition,
ClusterToolId,
ClusterToolState,
ClusterToolStatus,
} from './cluster-tools.types';
import { ClusterToolDefinition, ClusterToolId, ClusterToolState, ClusterToolStatus } from './cluster-tools.types';
const CERT_MANAGER_RELEASE = 'cert-manager';
const CERT_MANAGER_NAMESPACE = 'cert-manager';
@@ -27,9 +16,7 @@ const CLUSTER_ISSUER_NAME = 'letsencrypt-prod';
const ACME_PROD_SERVER = 'https://acme-v02.api.letsencrypt.org/directory';
// k3s ships Traefik by default; match the app Ingress class (INGRESS_CLASS)
// so the ACME HTTP-01 solver Ingress is actually served by the controller.
const DEFAULT_INGRESS_CLASS = (process.env.INGRESS_CLASS || 'traefik')
.trim()
.toLowerCase();
const DEFAULT_INGRESS_CLASS = (process.env.INGRESS_CLASS || 'traefik').trim().toLowerCase();
const ISSUER_GROUP = 'cert-manager.io';
const ISSUER_VERSION = 'v1';
@@ -44,8 +31,7 @@ export class ClusterToolsService {
{
id: 'cert-manager',
name: 'cert-manager',
description:
'Automated TLS certificate management. Required before creating a ClusterIssuer.',
description: 'Automated TLS certificate management. Required before creating a ClusterIssuer.',
category: 'Certificates',
dependencies: [],
installFields: [],
@@ -53,8 +39,7 @@ export class ClusterToolsService {
{
id: 'cluster-issuer',
name: "ClusterIssuer (Let's Encrypt)",
description:
'Cluster-wide ACME issuer (letsencrypt-prod) using HTTP01 challenge. Requires cert-manager.',
description: 'Cluster-wide ACME issuer (letsencrypt-prod) using HTTP01 challenge. Requires cert-manager.',
category: 'Certificates',
dependencies: ['cert-manager'],
installFields: [
@@ -71,8 +56,7 @@ export class ClusterToolsService {
{
id: 'central-elastic',
name: 'Central Elasticsearch + Kibana',
description:
'Shared logging stack powering the unified Logs page. Installed via Helm in the "logging" namespace.',
description: 'Shared logging stack powering the unified Logs page. Installed via Helm in the "logging" namespace.',
category: 'Logging',
dependencies: [],
installFields: [],
@@ -96,11 +80,7 @@ export class ClusterToolsService {
return Promise.all(
this.catalog.map(async (def) => {
try {
const { status, message, details } = await this.statusOf(
def.id,
clusterId,
kubeconfig,
);
const { status, message, details } = await this.statusOf(def.id, clusterId, kubeconfig);
return { ...def, status, message, details };
} catch (err: any) {
return {
@@ -113,11 +93,7 @@ export class ClusterToolsService {
);
}
async install(
clusterId: string,
toolId: ClusterToolId,
params: Record<string, string> = {},
): Promise<{ status: ClusterToolStatus; message: string }> {
async install(clusterId: string, toolId: ClusterToolId, params: Record<string, string> = {}): Promise<{ status: ClusterToolStatus; message: string }> {
const def = this.requireTool(toolId);
const kubeconfig = await this.getKubeconfig(clusterId);
@@ -125,9 +101,7 @@ export class ClusterToolsService {
for (const depId of def.dependencies) {
const dep = await this.statusOf(depId, clusterId, kubeconfig);
if (dep.status !== 'installed') {
throw new BadRequestException(
`"${this.requireTool(depId).name}" must be installed before "${def.name}".`,
);
throw new BadRequestException(`"${this.requireTool(depId).name}" must be installed before "${def.name}".`);
}
}
@@ -141,10 +115,7 @@ export class ClusterToolsService {
}
}
async uninstall(
clusterId: string,
toolId: ClusterToolId,
): Promise<{ status: ClusterToolStatus; message: string }> {
async uninstall(clusterId: string, toolId: ClusterToolId): Promise<{ status: ClusterToolStatus; message: string }> {
this.requireTool(toolId);
const kubeconfig = await this.getKubeconfig(clusterId);
@@ -160,9 +131,7 @@ export class ClusterToolsService {
// ── cert-manager ───────────────────────────────────────────────────
private async installCertManager(
kubeconfig: string,
): Promise<{ status: ClusterToolStatus; message: string }> {
private async installCertManager(kubeconfig: string): Promise<{ status: ClusterToolStatus; message: string }> {
await this.helmService.installRemoteChart({
repoName: CERT_MANAGER_REPO_NAME,
repoUrl: CERT_MANAGER_REPO_URL,
@@ -176,28 +145,18 @@ export class ClusterToolsService {
});
return {
status: 'installing',
message:
'cert-manager install started. Pods are starting — allow 12 minutes to become ready.',
message: 'cert-manager install started. Pods are starting — allow 12 minutes to become ready.',
};
}
private async uninstallCertManager(
kubeconfig: string,
): Promise<{ status: ClusterToolStatus; message: string }> {
await this.helmService.uninstall(
CERT_MANAGER_RELEASE,
CERT_MANAGER_NAMESPACE,
kubeconfig,
);
private async uninstallCertManager(kubeconfig: string): Promise<{ status: ClusterToolStatus; message: string }> {
await this.helmService.uninstall(CERT_MANAGER_RELEASE, CERT_MANAGER_NAMESPACE, kubeconfig);
return { status: 'not_installed', message: 'cert-manager removed.' };
}
// ── ClusterIssuer ──────────────────────────────────────────────────
private async installClusterIssuer(
kubeconfig: string,
params: Record<string, string>,
): Promise<{ status: ClusterToolStatus; message: string }> {
private async installClusterIssuer(kubeconfig: string, params: Record<string, string>): Promise<{ status: ClusterToolStatus; message: string }> {
const email = (params.email || '').trim();
if (!email) {
throw new BadRequestException('An ACME email is required for the ClusterIssuer.');
@@ -220,33 +179,31 @@ export class ClusterToolsService {
};
try {
await api.getClusterCustomObject(
ISSUER_GROUP,
ISSUER_VERSION,
ISSUER_PLURAL,
CLUSTER_ISSUER_NAME,
);
await api.replaceClusterCustomObject(
ISSUER_GROUP,
ISSUER_VERSION,
ISSUER_PLURAL,
CLUSTER_ISSUER_NAME,
await api.getClusterCustomObject({
group: ISSUER_GROUP,
version: ISSUER_VERSION,
plural: ISSUER_PLURAL,
name: CLUSTER_ISSUER_NAME,
});
await api.replaceClusterCustomObject({
group: ISSUER_GROUP,
version: ISSUER_VERSION,
plural: ISSUER_PLURAL,
name: CLUSTER_ISSUER_NAME,
body,
);
});
this.logger.log(`Updated ClusterIssuer "${CLUSTER_ISSUER_NAME}"`);
} catch (err: any) {
if (this.isNotFound(err)) {
await api.createClusterCustomObject(
ISSUER_GROUP,
ISSUER_VERSION,
ISSUER_PLURAL,
await api.createClusterCustomObject({
group: ISSUER_GROUP,
version: ISSUER_VERSION,
plural: ISSUER_PLURAL,
body,
);
});
this.logger.log(`Created ClusterIssuer "${CLUSTER_ISSUER_NAME}"`);
} else if (this.isMissingCrd(err)) {
throw new BadRequestException(
'cert-manager CRDs are not available yet. Wait for cert-manager to finish installing, then retry.',
);
throw new BadRequestException('cert-manager CRDs are not available yet. Wait for cert-manager to finish installing, then retry.');
} else {
throw err;
}
@@ -258,40 +215,35 @@ export class ClusterToolsService {
};
}
private async uninstallClusterIssuer(
kubeconfig: string,
): Promise<{ status: ClusterToolStatus; message: string }> {
private async uninstallClusterIssuer(kubeconfig: string): Promise<{ status: ClusterToolStatus; message: string }> {
const api = this.customObjectsApi(kubeconfig);
try {
await api.deleteClusterCustomObject(
ISSUER_GROUP,
ISSUER_VERSION,
ISSUER_PLURAL,
CLUSTER_ISSUER_NAME,
);
await api.deleteClusterCustomObject({
group: ISSUER_GROUP,
version: ISSUER_VERSION,
plural: ISSUER_PLURAL,
name: CLUSTER_ISSUER_NAME,
});
} catch (err: any) {
if (!this.isNotFound(err) && !this.isMissingCrd(err)) throw err;
}
return { status: 'not_installed', message: `ClusterIssuer "${CLUSTER_ISSUER_NAME}" removed.` };
return {
status: 'not_installed',
message: `ClusterIssuer "${CLUSTER_ISSUER_NAME}" removed.`,
};
}
// ── Central Elasticsearch ──────────────────────────────────────────
private async installCentralElastic(
clusterId: string,
): Promise<{ status: ClusterToolStatus; message: string }> {
private async installCentralElastic(clusterId: string): Promise<{ status: ClusterToolStatus; message: string }> {
const result = await this.elasticsearchService.deploy(clusterId);
return {
status: result.deploying ? 'installing' : 'installed',
message: result.deploying
? 'Logging stack installed. Allow 25 minutes for Elasticsearch and Kibana to become ready.'
: 'Elasticsearch and Kibana are ready.',
message: result.deploying ? 'Logging stack installed. Allow 25 minutes for Elasticsearch and Kibana to become ready.' : 'Elasticsearch and Kibana are ready.',
};
}
private async uninstallCentralElastic(
clusterId: string,
): Promise<{ status: ClusterToolStatus; message: string }> {
private async uninstallCentralElastic(clusterId: string): Promise<{ status: ClusterToolStatus; message: string }> {
await this.elasticsearchService.undeploy(clusterId);
return {
status: 'not_installed',
@@ -305,14 +257,14 @@ export class ClusterToolsService {
toolId: ClusterToolId,
clusterId: string,
kubeconfig: string,
): Promise<{ status: ClusterToolStatus; message?: string; details?: Record<string, unknown> }> {
): Promise<{
status: ClusterToolStatus;
message?: string;
details?: Record<string, unknown>;
}> {
switch (toolId) {
case 'cert-manager': {
const helm = await this.helmService.status(
CERT_MANAGER_RELEASE,
CERT_MANAGER_NAMESPACE,
kubeconfig,
);
const helm = await this.helmService.status(CERT_MANAGER_RELEASE, CERT_MANAGER_NAMESPACE, kubeconfig);
if (!helm) return { status: 'not_installed' };
return {
status: this.mapHelmStatus(helm.status),
@@ -323,17 +275,21 @@ export class ClusterToolsService {
case 'cluster-issuer': {
const api = this.customObjectsApi(kubeconfig);
try {
const res: any = await api.getClusterCustomObject(
ISSUER_GROUP,
ISSUER_VERSION,
ISSUER_PLURAL,
CLUSTER_ISSUER_NAME,
);
const conditions: any[] = res.body?.status?.conditions || [];
const res: any = await api.getClusterCustomObject({
group: ISSUER_GROUP,
version: ISSUER_VERSION,
plural: ISSUER_PLURAL,
name: CLUSTER_ISSUER_NAME,
});
const conditions: any[] = res?.status?.conditions || [];
const ready = conditions.find((c) => c.type === 'Ready');
const email = res.body?.spec?.acme?.email;
const email = res?.spec?.acme?.email;
if (ready?.status === 'True') {
return { status: 'installed', message: 'Ready', details: { email } };
return {
status: 'installed',
message: 'Ready',
details: { email },
};
}
return {
status: 'installing',
@@ -357,9 +313,7 @@ export class ClusterToolsService {
};
return {
status: map[state.status] || 'unknown',
message: state.helmReleaseStatus
? `helm: ${state.helmReleaseStatus}`
: undefined,
message: state.helmReleaseStatus ? `helm: ${state.helmReleaseStatus}` : undefined,
details: state.health ? { health: state.health.status } : undefined,
};
}
@@ -394,14 +348,14 @@ export class ClusterToolsService {
}
private isNotFound(err: any): boolean {
return err?.statusCode === 404 || err?.body?.code === 404;
return err?.code === 404 || err?.statusCode === 404 || err?.body?.code === 404;
}
/** cert-manager CRD not yet registered → API returns 404 on the group or NotFound kind. */
private isMissingCrd(err: any): boolean {
const msg = (err?.body?.message || err?.message || '').toString();
return /could not find the requested resource|the server could not find|no matches for kind|NotFound/i.test(
msg,
);
const body = err?.body;
const bodyMsg = typeof body === 'string' ? body : body?.message;
const msg = (bodyMsg || err?.message || '').toString();
return /could not find the requested resource|the server could not find|no matches for kind|NotFound/i.test(msg);
}
}
File diff suppressed because it is too large Load Diff