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
+83 -150
View File
@@ -1,12 +1,4 @@
import {
Injectable,
Logger,
ServiceUnavailableException,
Inject,
forwardRef,
OnModuleInit,
OnModuleDestroy,
} from '@nestjs/common';
import { Injectable, Logger, ServiceUnavailableException, Inject, forwardRef, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as k8s from '@kubernetes/client-node';
import * as crypto from 'crypto';
@@ -142,19 +134,11 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
/** Hostnames only resolvable from inside the workload cluster (not from a laptop). */
private isClusterInternalHost(host: string): boolean {
const h = host.toLowerCase();
return (
h.includes('svc.cluster.local') ||
h.includes('.cluster.') ||
h === 'elasticsearch' ||
h === 'kibana'
);
return h.includes('svc.cluster.local') || h.includes('.cluster.') || h === 'elasticsearch' || h === 'kibana';
}
private configuredElasticsearchHost(): string {
return (
this.configService.get<string>('elasticsearch.host') ||
`${this.ES_NAME}.${this.ES_NAMESPACE}.svc.cluster.local`
);
return this.configService.get<string>('elasticsearch.host') || `${this.ES_NAME}.${this.ES_NAMESPACE}.svc.cluster.local`;
}
/** HTTP target: loopback when we tunnel; cluster DNS when the API pod runs in-cluster. */
@@ -213,9 +197,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
}
const delay = Math.min(60_000, 2_000 * Math.pow(2, this.reconnectAttempt));
this.reconnectAttempt += 1;
this.logger.warn(
`Elasticsearch port-forward lost (${reason}). Reconnecting in ${Math.round(delay / 1000)}s…`,
);
this.logger.warn(`Elasticsearch port-forward lost (${reason}). Reconnecting in ${Math.round(delay / 1000)}s…`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
void this.ensureLocalElasticsearchAccess().then((ok) => {
@@ -280,10 +262,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
private async startDevPortForward(localPort: number, clusterId?: string): Promise<void> {
const targetClusterId = clusterId || null;
if (
this.portForwardChild &&
this.portForwardClusterId === targetClusterId
) {
if (this.portForwardChild && this.portForwardClusterId === targetClusterId) {
return;
}
if (this.portForwardChild) {
@@ -296,18 +275,8 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
this.portForwardKubeconfigFile = kubeconfigFile;
this.portForwardClusterId = targetClusterId;
const args = [
'--kubeconfig',
kubeconfigFile,
'port-forward',
'-n',
this.ES_NAMESPACE,
`svc/${this.ES_NAME}`,
`${localPort}:9200`,
];
this.logger.log(
`Starting kubectl port-forward to Elasticsearch on cluster "${cluster.name}" (local log search)`,
);
const args = ['--kubeconfig', kubeconfigFile, 'port-forward', '-n', this.ES_NAMESPACE, `svc/${this.ES_NAME}`, `${localPort}:9200`];
this.logger.log(`Starting kubectl port-forward to Elasticsearch on cluster "${cluster.name}" (local log search)`);
const child = spawn('kubectl', args, { stdio: ['ignore', 'pipe', 'pipe'] });
this.portForwardChild = child;
this.portForwardStartedByUs = true;
@@ -318,12 +287,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
this.portForwardStartedByUs = false;
}
if (wasOurs) {
const reason =
code !== 0 && code !== null
? `exit code ${code}`
: signal
? `signal ${signal}`
: 'connection closed';
const reason = code !== 0 && code !== null ? `exit code ${code}` : signal ? `signal ${signal}` : 'connection closed';
this.schedulePortForwardReconnect(reason);
}
});
@@ -339,10 +303,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
* When the API runs on the host with ELASTICSEARCH_HOST=127.0.0.1, open a tunnel to the cluster.
* Safe to call repeatedly (e.g. after cluster/API restart or port-forward drop).
*/
private async ensureLocalElasticsearchAccess(options?: {
waitForCluster?: boolean;
clusterId?: string;
}): Promise<boolean> {
private async ensureLocalElasticsearchAccess(options?: { waitForCluster?: boolean; clusterId?: string }): Promise<boolean> {
if (this.ensureInFlight) {
await this.ensureInFlight;
return this.probeElasticsearch();
@@ -357,10 +318,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
}
}
private async ensureLocalElasticsearchAccessImpl(options?: {
waitForCluster?: boolean;
clusterId?: string;
}): Promise<void> {
private async ensureLocalElasticsearchAccessImpl(options?: { waitForCluster?: boolean; clusterId?: string }): Promise<void> {
if (!this.shouldAutoPortForward()) {
return;
}
@@ -395,9 +353,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
this.logger.log(`Elasticsearch reachable at 127.0.0.1:${port}`);
} else {
this.stopDevPortForward();
this.logger.warn(
`Could not reach Elasticsearch on 127.0.0.1:${port}. Will retry. Manual: kubectl port-forward -n ${this.ES_NAMESPACE} svc/${this.ES_NAME} ${port}:9200`,
);
this.logger.warn(`Could not reach Elasticsearch on 127.0.0.1:${port}. Will retry. Manual: kubectl port-forward -n ${this.ES_NAMESPACE} svc/${this.ES_NAME} ${port}:9200`);
this.schedulePortForwardReconnect('probe timeout');
}
}
@@ -413,10 +369,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
}
if (this.isClusterInternalHost(configured)) {
if (this.isRunningInKubernetes()) {
return (
'Ensure the logging stack is deployed on the same cluster as this API pod ' +
`(Helm release ${LOGGING_HELM_RELEASE} in namespace ${LOGGING_HELM_NAMESPACE}).`
);
return 'Ensure the logging stack is deployed on the same cluster as this API pod ' + `(Helm release ${LOGGING_HELM_RELEASE} in namespace ${LOGGING_HELM_NAMESPACE}).`;
}
return (
'Run the API inside the cluster, or restart the API locally so it can auto port-forward Elasticsearch ' +
@@ -427,9 +380,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
}
private async getK8sClients(clusterId?: string) {
const cluster = clusterId
? await this.clustersService.findOne(clusterId)
: await this.clustersService.getDefault();
const cluster = clusterId ? await this.clustersService.findOne(clusterId) : await this.clustersService.getDefault();
const kc = new k8s.KubeConfig();
kc.loadFromString(cluster.kubeconfig);
@@ -455,18 +406,17 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
*/
async getDeployState(clusterId?: string): Promise<LoggingDeployState> {
const { cluster } = await this.getK8sClients(clusterId);
const helmStatus = await this.helmService.status(
LOGGING_HELM_RELEASE,
LOGGING_HELM_NAMESPACE,
cluster.kubeconfig,
);
const helmStatus = await this.helmService.status(LOGGING_HELM_RELEASE, LOGGING_HELM_NAMESPACE, cluster.kubeconfig);
let hasEsWorkload = false;
try {
const kc = new k8s.KubeConfig();
kc.loadFromString(cluster.kubeconfig);
const appsApi = kc.makeApiClient(k8s.AppsV1Api);
await appsApi.readNamespacedStatefulSet(this.ES_NAME, this.ES_NAMESPACE);
await appsApi.readNamespacedStatefulSet({
name: this.ES_NAME,
namespace: this.ES_NAMESPACE,
});
hasEsWorkload = true;
} catch {
hasEsWorkload = false;
@@ -512,26 +462,27 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
const kc = new k8s.KubeConfig();
kc.loadFromString(kubeconfig);
const storageApi = kc.makeApiClient(k8s.StorageV1Api);
const provisioner =
this.configService.get<string>('platform.storageProvisioner') || 'rancher.io/local-path';
const provisioner = this.configService.get<string>('platform.storageProvisioner') || 'rancher.io/local-path';
try {
await storageApi.readStorageClass(storageClass);
await storageApi.readStorageClass({ name: storageClass });
return;
} catch (err: any) {
if (err.statusCode !== 404 && err.body?.code !== 404) {
if (err.code !== 404 && err.body?.code !== 404) {
throw err;
}
}
await storageApi.createStorageClass({
apiVersion: 'storage.k8s.io/v1',
kind: 'StorageClass',
metadata: { name: storageClass },
provisioner,
allowVolumeExpansion: true,
reclaimPolicy: 'Delete',
volumeBindingMode: 'WaitForFirstConsumer',
body: {
apiVersion: 'storage.k8s.io/v1',
kind: 'StorageClass',
metadata: { name: storageClass },
provisioner,
allowVolumeExpansion: true,
reclaimPolicy: 'Delete',
volumeBindingMode: 'WaitForFirstConsumer',
},
});
this.logger.log(`Created StorageClass "${storageClass}" (provisioner: ${provisioner})`);
}
@@ -578,37 +529,25 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
try {
// Check ES pods
const esPods = await coreApi.listNamespacedPod(
this.ES_NAMESPACE,
undefined,
undefined,
undefined,
undefined,
'app=elasticsearch',
);
const esPods = await coreApi.listNamespacedPod({
namespace: this.ES_NAMESPACE,
labelSelector: 'app=elasticsearch',
});
const kibanaPods = await coreApi.listNamespacedPod(
this.ES_NAMESPACE,
undefined,
undefined,
undefined,
undefined,
'app=kibana',
);
const kibanaPods = await coreApi.listNamespacedPod({
namespace: this.ES_NAMESPACE,
labelSelector: 'app=kibana',
});
if (esPods.body.items.length === 0) {
if (esPods.items.length === 0) {
return null;
}
const esPod = esPods.body.items[0];
const isEsReady = esPod.status?.conditions?.some(
(c) => c.type === 'Ready' && c.status === 'True',
);
const esPod = esPods.items[0];
const isEsReady = esPod.status?.conditions?.some((c) => c.type === 'Ready' && c.status === 'True');
const kibanaPod = kibanaPods.body.items[0];
const isKibanaReady = kibanaPod?.status?.conditions?.some(
(c) => c.type === 'Ready' && c.status === 'True',
) || false;
const kibanaPod = kibanaPods.items[0];
const isKibanaReady = kibanaPod?.status?.conditions?.some((c) => c.type === 'Ready' && c.status === 'True') || false;
return {
status: isEsReady ? 'green' : 'yellow',
@@ -640,22 +579,14 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
timeout: '10m',
});
} catch (error: any) {
const release = await this.helmService.status(
LOGGING_HELM_RELEASE,
LOGGING_HELM_NAMESPACE,
cluster.kubeconfig,
);
const release = await this.helmService.status(LOGGING_HELM_RELEASE, LOGGING_HELM_NAMESPACE, cluster.kubeconfig);
if (!release) {
throw error;
}
this.logger.warn(
`Helm logging install reported an error but release exists (${release.status}); continuing: ${error.message}`,
);
this.logger.warn(`Helm logging install reported an error but release exists (${release.status}); continuing: ${error.message}`);
}
this.logger.log(
`Central logging stack applied via Helm (${LOGGING_HELM_RELEASE} in ${LOGGING_HELM_NAMESPACE})`,
);
this.logger.log(`Central logging stack applied via Helm (${LOGGING_HELM_RELEASE} in ${LOGGING_HELM_NAMESPACE})`);
const state = await this.getDeployState(clusterId);
@@ -688,7 +619,12 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
/**
* Get Elasticsearch connection info for apps
*/
getConnectionInfo(): { host: string; port: number; username: string; password: string } {
getConnectionInfo(): {
host: string;
port: number;
username: string;
password: string;
} {
return {
host: this.effectiveElasticsearchHost(),
port: this.configService.get<number>('elasticsearch.port') || 9200,
@@ -744,12 +680,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
const must: any[] = [
{
bool: {
should: [
{ term: { ownerId: userId } },
{ term: { 'ownerId.keyword': userId } },
{ term: { namespace } },
{ term: { 'namespace.keyword': namespace } },
],
should: [{ term: { ownerId: userId } }, { term: { 'ownerId.keyword': userId } }, { term: { namespace } }, { term: { 'namespace.keyword': namespace } }],
minimum_should_match: 1,
},
},
@@ -758,10 +689,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
if (filters.applicationId) {
must.push({
bool: {
should: [
{ term: { applicationId: filters.applicationId } },
{ term: { 'applicationId.keyword': filters.applicationId } },
],
should: [{ term: { applicationId: filters.applicationId } }, { term: { 'applicationId.keyword': filters.applicationId } }],
minimum_should_match: 1,
},
});
@@ -784,10 +712,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
if (filters.workload) {
must.push({
bool: {
should: [
{ term: { workload: filters.workload } },
{ term: { 'workload.keyword': filters.workload } },
],
should: [{ term: { workload: filters.workload } }, { term: { 'workload.keyword': filters.workload } }],
minimum_should_match: 1,
},
});
@@ -796,10 +721,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
if (filters.level) {
must.push({
bool: {
should: [
{ term: { level: filters.level.toLowerCase() } },
{ term: { 'level.keyword': filters.level.toLowerCase() } },
],
should: [{ term: { level: filters.level.toLowerCase() } }, { term: { 'level.keyword': filters.level.toLowerCase() } }],
minimum_should_match: 1,
},
});
@@ -854,9 +776,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
private async esRequest(path: string, body: unknown, clusterId?: string): Promise<any> {
const deployed = await this.isDeployed(clusterId);
if (!deployed) {
throw new ServiceUnavailableException(
'Central logging is not configured. Ask an administrator to deploy Elasticsearch.',
);
throw new ServiceUnavailableException('Central logging is not configured. Ask an administrator to deploy Elasticsearch.');
}
if (this.shouldAutoPortForward()) {
@@ -896,12 +816,7 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
private normalizeHit(hit: any): NormalizedLogEntry {
const src = hit._source || {};
const message =
src.message ||
src.log ||
src.msg ||
(typeof src.error === 'string' ? src.error : src.error?.message) ||
'';
const message = src.message || src.log || src.msg || (typeof src.error === 'string' ? src.error : src.error?.message) || '';
return {
id: hit._id || '',
@@ -942,7 +857,12 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
async searchLogStats(
userId: string,
filters: { applicationId?: string; applicationName?: string; workload?: string; period?: string },
filters: {
applicationId?: string;
applicationName?: string;
workload?: string;
period?: string;
},
clusterId?: string,
): Promise<LogStatsResult> {
const periodMap: Record<string, string> = {
@@ -965,8 +885,12 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
query: { bool: { must } },
size: 0,
aggs: {
by_level: { terms: { field: 'level.keyword', size: 10, missing: 'unknown' } },
by_workload: { terms: { field: 'workload.keyword', size: 10, missing: 'app' } },
by_level: {
terms: { field: 'level.keyword', size: 10, missing: 'unknown' },
},
by_workload: {
terms: { field: 'workload.keyword', size: 10, missing: 'app' },
},
error_count: { filter: { term: { 'level.keyword': 'error' } } },
warn_count: { filter: { term: { 'level.keyword': 'warn' } } },
},
@@ -994,7 +918,13 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
async searchRecentErrors(
userId: string,
filters: { applicationId?: string; applicationName?: string; workload?: string; hours?: number; limit?: number },
filters: {
applicationId?: string;
applicationName?: string;
workload?: string;
hours?: number;
limit?: number;
},
clusterId?: string,
): Promise<NormalizedLogEntry[]> {
const hours = filters.hours || 24;
@@ -1019,9 +949,12 @@ export class ElasticsearchService implements OnModuleInit, OnModuleDestroy {
return (result.hits?.hits || []).map((h: any) => this.normalizeHit(h));
}
async getLoggingStatus(
clusterId?: string,
): Promise<{ available: boolean; deployed: boolean; recovering?: boolean; message?: string }> {
async getLoggingStatus(clusterId?: string): Promise<{
available: boolean;
deployed: boolean;
recovering?: boolean;
message?: string;
}> {
let deployed = await this.isDeployed(clusterId);
if (!deployed && this.shouldAutoPortForward()) {
deployed = await this.waitForLoggingStack(8_000);
@@ -0,0 +1,159 @@
// @kubernetes/client-node 1.x ships as ESM (package.json "type": "module").
// Production (Node 20.19+/24) loads it fine via require(esm), but Jest's own
// CommonJS module system cannot parse it, so we mock it. Unit tests inject mock
// API clients anyway — the only runtime helper the tested paths touch is
// setHeaderOptions (used by the migrated patch calls).
jest.mock('@kubernetes/client-node', () => ({
setHeaderOptions: (key: string, value: string) => ({ headers: { [key]: value } }),
}));
import { RegistryService } from './registry.service';
import { KubernetesService } from './kubernetes.service';
/**
* Regression tests for the @kubernetes/client-node 1.x migration.
*
* v1.x changed every API method from positional arguments returning `{ body }`
* to a single options object returning the body directly, and changed the
* thrown error shape from `.statusCode` to `.code`. These tests pin that the
* migrated services:
* 1. call the client with the new single-object argument shape,
* 2. read the unwrapped response (no `.body`),
* 3. detect "not found" via the new `err.code` field.
*
* They mock the API clients so no real cluster is required.
*/
const configStub = { get: jest.fn().mockReturnValue(undefined) } as any;
describe('RegistryService — k8s v1 client shape', () => {
let service: RegistryService;
beforeEach(() => {
service = new RegistryService(configStub);
});
it('replaces the pull secret with the v1 single-object argument', async () => {
const coreApi = {
replaceNamespacedSecret: jest.fn().mockResolvedValue({}),
createNamespacedSecret: jest.fn(),
} as any;
await service.ensureRegistryPullSecret(coreApi, 'team-ns');
expect(coreApi.replaceNamespacedSecret).toHaveBeenCalledTimes(1);
const arg = coreApi.replaceNamespacedSecret.mock.calls[0][0];
// v1 passes ONE object, not positional (name, namespace, body)
expect(coreApi.replaceNamespacedSecret.mock.calls[0]).toHaveLength(1);
expect(arg).toMatchObject({ name: 'registry-pull-secret', namespace: 'team-ns' });
expect(arg.body?.metadata?.name).toBe('registry-pull-secret');
expect(coreApi.createNamespacedSecret).not.toHaveBeenCalled();
});
it('creates the secret when replace fails with the v1 err.code 404', async () => {
const coreApi = {
replaceNamespacedSecret: jest.fn().mockRejectedValue({ code: 404 }),
createNamespacedSecret: jest.fn().mockResolvedValue({}),
} as any;
await service.ensureRegistryPullSecret(coreApi, 'team-ns');
expect(coreApi.createNamespacedSecret).toHaveBeenCalledTimes(1);
const arg = coreApi.createNamespacedSecret.mock.calls[0][0];
expect(coreApi.createNamespacedSecret.mock.calls[0]).toHaveLength(1);
expect(arg).toMatchObject({ namespace: 'team-ns' });
expect(arg.body?.type).toBe('kubernetes.io/dockerconfigjson');
});
it('rethrows non-404 errors instead of creating', async () => {
const coreApi = {
replaceNamespacedSecret: jest.fn().mockRejectedValue({ code: 500 }),
createNamespacedSecret: jest.fn(),
} as any;
await expect(service.ensureRegistryPullSecret(coreApi, 'team-ns')).rejects.toEqual({ code: 500 });
expect(coreApi.createNamespacedSecret).not.toHaveBeenCalled();
});
});
describe('KubernetesService — k8s v1 client shape', () => {
let service: KubernetesService;
const makeService = (clients: { coreApi?: any; appsApi?: any; networkingApi?: any; kc?: any }) => {
const svc = new KubernetesService(
configStub,
{} as any, // clustersService
{} as any, // helmService
{} as any, // registryService
{} as any, // deploymentsRepository
);
jest.spyOn(svc as any, 'getK8sClient').mockResolvedValue({
coreApi: clients.coreApi,
appsApi: clients.appsApi,
networkingApi: clients.networkingApi,
kc: clients.kc,
});
return svc;
};
const app = {
id: 'app-1',
name: 'my-app',
userId: 'abc123-def456',
clusterId: 'cluster-1',
productType: 'web_service',
databaseType: 'none',
} as any;
it('getPodLogs lists pods and reads the log with v1 object args and unwrapped result', async () => {
const coreApi = {
listNamespacedPod: jest.fn().mockResolvedValue({ items: [{ metadata: { name: 'pod-1' } }] }),
readNamespacedPodLog: jest.fn().mockResolvedValue('hello logs'),
};
service = makeService({ coreApi });
const logs = await service.getPodLogs(app);
// unwrapped string returned directly (v0.x returned { body })
expect(logs).toBe('hello logs');
const listArg = coreApi.listNamespacedPod.mock.calls[0][0];
expect(listArg).toMatchObject({ namespace: 'user-abc123' });
expect(typeof listArg.labelSelector).toBe('string');
const logArg = coreApi.readNamespacedPodLog.mock.calls[0][0];
expect(logArg).toMatchObject({ name: 'pod-1', namespace: 'user-abc123', tailLines: 200 });
});
it('getDatabasePvcSize reads the PVC with v1 object args and unwrapped spec', async () => {
const coreApi = {
readNamespacedPersistentVolumeClaim: jest
.fn()
.mockResolvedValue({ spec: { resources: { requests: { storage: '5Gi' } } } }),
};
service = makeService({ coreApi });
const size = await service.getDatabasePvcSize(app);
expect(size).toBe('5Gi');
const arg = coreApi.readNamespacedPersistentVolumeClaim.mock.calls[0][0];
expect(arg).toMatchObject({ name: 'my-app-db', namespace: 'user-abc123' });
});
it('scaleDeployment patches with the v1 object body and a header-options 2nd arg', async () => {
const appsApi = { patchNamespacedDeployment: jest.fn().mockResolvedValue({}) };
service = makeService({ appsApi });
await service.scaleDeployment(app, 3);
expect(appsApi.patchNamespacedDeployment).toHaveBeenCalledTimes(1);
const [param, options] = appsApi.patchNamespacedDeployment.mock.calls[0];
expect(param).toMatchObject({
name: 'my-app',
namespace: 'user-abc123',
body: { spec: { replicas: 3 } },
});
// v1 takes the merge-patch content-type via the 2nd ConfigurationOptions arg
expect(options).toBeDefined();
});
});
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -24,10 +24,7 @@ export class RegistryService {
/** Registry host:port used for build push and app image pull. */
getRegistryUrl(): string {
const buildNs = this.getBuildNamespace();
const url =
this.configService.get<string>('registry.pullUrl') ||
this.configService.get<string>('registry.url') ||
`registry.${buildNs}.svc.cluster.local:5000`;
const url = this.configService.get<string>('registry.pullUrl') || this.configService.get<string>('registry.url') || `registry.${buildNs}.svc.cluster.local:5000`;
return url.replace(/^https?:\/\//, '');
}
@@ -67,15 +64,14 @@ export class RegistryService {
buildDockerConfigJson(): string {
const { username, password } = this.getRegistryCredentials();
const auth =
username && password
? Buffer.from(`${username}:${password}`).toString('base64')
: '';
const auth = username && password ? Buffer.from(`${username}:${password}`).toString('base64') : '';
const host = this.getRegistryUrl();
return JSON.stringify({
auths: {
[host]: { auth },
[`registry.${this.getBuildNamespace()}.svc.cluster.local:5000`]: { auth },
[`registry.${this.getBuildNamespace()}.svc.cluster.local:5000`]: {
auth,
},
},
});
}
@@ -97,10 +93,14 @@ export class RegistryService {
};
try {
await coreApi.replaceNamespacedSecret(secretName, namespace, secret);
await coreApi.replaceNamespacedSecret({
name: secretName,
namespace,
body: secret,
});
} catch (err: any) {
if (err.statusCode === 404 || err.body?.code === 404) {
await coreApi.createNamespacedSecret(namespace, secret);
if (err.code === 404 || err.body?.code === 404) {
await coreApi.createNamespacedSecret({ namespace, body: secret });
this.logger.log(`Created ${secretName} in ${namespace}`);
} else {
throw err;