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:
@@ -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 1–2 minutes to become ready.',
|
||||
message: 'cert-manager install started. Pods are starting — allow 1–2 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 2–5 minutes for Elasticsearch and Kibana to become ready.'
|
||||
: 'Elasticsearch and Kibana are ready.',
|
||||
message: result.deploying ? 'Logging stack installed. Allow 2–5 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
Reference in New Issue
Block a user