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
7 changes: 7 additions & 0 deletions server/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ export class ServerConfig {
static get HTTP_INTERCEPTOR_URL() {
return process.env.HTTP_INTERCEPTOR_URL
}

static get KUBEBLOCK_V5_UPGRADE_URL() {
return process.env.KUBEBLOCK_V5_UPGRADE_URL
}
}

export const LABEL_KEY_USER_ID = 'laf.dev/user.id'
Expand Down Expand Up @@ -255,3 +259,6 @@ export const STORAGE_LIMIT = 1000 // 1000 items

// HTTP interceptor
export const HTTP_INTERCEPTOR_TIMEOUT = 3000 // 3s

// KubeBlock v5 upgrade API
export const KUBEBLOCK_V5_UPGRADE_API_TIMEOUT = 3000 // 3s
118 changes: 118 additions & 0 deletions server/src/database/dedicated-database/dedicated-database.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ApplicationBundle } from 'src/application/entities/application-bundle'
import * as assert from 'assert'
import { extractNumber } from 'src/utils/number'
import { formatK8sErrorAsJson } from 'src/utils/k8s-error'
import { ServerConfig, KUBEBLOCK_V5_UPGRADE_API_TIMEOUT } from 'src/constants'

const getDedicatedDatabaseName = (appid: string) => appid

Expand Down Expand Up @@ -142,13 +143,18 @@ export class DedicatedDatabaseService {
appid,
'horizontalScaling',
)

if (!OpsRequestManifest) {
const result = await this.applyKubeBlockOpsRequestManifestForSpec(
region,
appid,
spec,
'horizontalScaling',
)

// Call KubeBlock v5 compatibility API if needed
await this.handleKubeBlockV5Upgrade(appid, manifest, spec.replicas)

results.push(result)
this.logger.log(
`Applied horizontalScaling ops request for ${appid}: replicas=${spec.replicas}`,
Expand Down Expand Up @@ -702,4 +708,116 @@ export class DedicatedDatabaseService {
return false
}
}

/**
* Handle KubeBlock v5 upgrade API call for horizontal scaling
* This is a compatibility feature for KubeBlock v5 mongodb-5.0 clusters
*
* @param appid - Application ID
* @param manifest - Current deployment manifest
* @param replicas - Number of replicas
*/
private async handleKubeBlockV5Upgrade(
appid: string,
manifest: KubernetesObject & { spec: any; status: any },
replicas: number,
): Promise<void> {
try {
// Early return if not a v5 mongodb-5.0 cluster
if (!this.isKubeBlockV5MongoDb(manifest)) {
return
}

const url = ServerConfig.KUBEBLOCK_V5_UPGRADE_URL
if (!url) {
this.logger.warn(
`KubeBlock v5 upgrade URL not configured (KUBEBLOCK_V5_UPGRADE_URL env var not set) for ${appid}`,
)
return
}

await this.callKubeBlockV5UpgradeAPI(appid, manifest, replicas, url)
} catch (error) {
this.logger.error(
`Failed to call KubeBlock v5 upgrade API for ${appid}: ${error.message}`,
)
Comment on lines +741 to +743
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the AbortController aborts the request due to timeout, it will throw an AbortError. However, this error is caught by the outer try-catch in handleKubeBlockV5Upgrade (line 740-745) which only logs error.message. For AbortError, the message might not clearly indicate it was a timeout.

Consider adding specific handling for timeout errors to provide clearer logging:

} catch (error) {
  if (error.name === 'AbortError') {
    this.logger.error(
      `KubeBlock v5 upgrade API timeout (${KUBEBLOCK_V5_UPGRADE_API_TIMEOUT}ms) for ${appid}`
    )
  } else {
    this.logger.error(
      `Failed to call KubeBlock v5 upgrade API for ${appid}: ${error.message}`
    )
  }
}
Suggested change
this.logger.error(
`Failed to call KubeBlock v5 upgrade API for ${appid}: ${error.message}`,
)
if (error.name === 'AbortError') {
this.logger.error(
`KubeBlock v5 upgrade API timeout (${KUBEBLOCK_V5_UPGRADE_API_TIMEOUT}ms) for ${appid}`
)
} else {
this.logger.error(
`Failed to call KubeBlock v5 upgrade API for ${appid}: ${error.message}`,
)
}

Copilot uses AI. Check for mistakes.
// Don't throw error, just log it as it's a compatibility feature
}
}

/**
* Check if the manifest is a KubeBlock v5 mongodb-5.0 cluster
*/
private isKubeBlockV5MongoDb(
manifest: KubernetesObject & { spec: any; status: any },
): boolean {
return (
manifest?.metadata?.labels?.['clusterversion.kubeblocks.io/name'] ===
'mongodb-5.0'
)
}

/**
* Call KubeBlock v5 upgrade API with timeout control
*/
private async callKubeBlockV5UpgradeAPI(
appid: string,
manifest: KubernetesObject & { spec: any; status: any },
replicas: number,
url: string,
): Promise<void> {
const clusterName = manifest.metadata.name
const namespace = manifest.metadata.namespace
Comment on lines +769 to +770
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null/undefined safety checks for manifest.metadata.name and manifest.metadata.namespace. While the manifest is passed from the calling function and likely has these properties, adding defensive checks would prevent potential runtime errors and align with the defensive programming pattern used elsewhere in the method (e.g., line 727).

Consider adding validation:

const clusterName = manifest?.metadata?.name
const namespace = manifest?.metadata?.namespace

if (!clusterName || !namespace) {
  throw new Error('Invalid manifest: missing name or namespace')
}
Suggested change
const clusterName = manifest.metadata.name
const namespace = manifest.metadata.namespace
const clusterName = manifest?.metadata?.name
const namespace = manifest?.metadata?.namespace
if (!clusterName || !namespace) {
throw new Error('Invalid manifest: missing name or namespace')
}

Copilot uses AI. Check for mistakes.

// Create AbortController for timeout
const controller = new AbortController()
const timeoutId = setTimeout(
() => controller.abort(),
KUBEBLOCK_V5_UPGRADE_API_TIMEOUT,
)

try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
namespace,
database_name: clusterName,
replicas,
}),
signal: controller.signal,
})
Comment on lines +780 to +791
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetch API is being used directly without any visible import. While fetch is globally available in Node.js 18+, this codebase appears to use @nestjs/axios (HttpService) for HTTP requests elsewhere in the project (e.g., in http-interceptor.service.ts, wechat-pay.service.ts). For consistency and better error handling, consider using the injected HttpService instead of the global fetch API.

Using HttpService would provide:

  • Consistent error handling patterns across the codebase
  • Built-in timeout support via axios configuration
  • Better integration with NestJS ecosystem

Copilot uses AI. Check for mistakes.

const responseData = await this.parseKubeBlockV5Response(response)

if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}, statusText: ${
response.statusText
}, body: ${JSON.stringify(responseData)}`,
)
}

Comment on lines +793 to +802
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The response parsing happens before checking response.ok, which means the response body is consumed before the error check. If the response is not OK and has a large body, this could be inefficient. More importantly, if parseKubeBlockV5Response fails (e.g., malformed JSON), the error message won't include the HTTP status information.

Consider checking response.ok first, then parsing the response body only when needed:

if (!response.ok) {
  const errorBody = await this.parseKubeBlockV5Response(response)
  throw new Error(
    `HTTP error! status: ${response.status}, statusText: ${response.statusText}, body: ${JSON.stringify(errorBody)}`
  )
}

const responseData = await this.parseKubeBlockV5Response(response)
Suggested change
const responseData = await this.parseKubeBlockV5Response(response)
if (!response.ok) {
throw new Error(
`HTTP error! status: ${response.status}, statusText: ${
response.statusText
}, body: ${JSON.stringify(responseData)}`,
)
}
if (!response.ok) {
const errorBody = await this.parseKubeBlockV5Response(response)
throw new Error(
`HTTP error! status: ${response.status}, statusText: ${
response.statusText
}, body: ${JSON.stringify(errorBody)}`,
)
}
const responseData = await this.parseKubeBlockV5Response(response)

Copilot uses AI. Check for mistakes.
this.logger.log(
`Called KubeBlock v5 upgrade API for ${appid}: cluster=${clusterName}, replicas=${replicas}, response: ${JSON.stringify(
responseData,
)}`,
)
} finally {
clearTimeout(timeoutId)
}
}

/**
* Parse response body from KubeBlock v5 API
*/
private async parseKubeBlockV5Response(response: Response): Promise<any> {
const contentType = response.headers.get('content-type')
if (contentType?.includes('application/json')) {
return await response.json()
}
return await response.text()
}
}
Loading