From 9fe52fc520e524fd759459f3e55132e2c8229cb7 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 28 Aug 2026 10:16:56 +0000 Subject: [PATCH 1/2] chore(registry): make create paths idempotent and clean up comments Signed-off-by: William Phetsinorath Change-Id: Icef5672b7cc38801260f64eb34569a716a6a6964 --- .../registry/registry-client.service.ts | 50 ++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/apps/server-nestjs/src/modules/registry/registry-client.service.ts b/apps/server-nestjs/src/modules/registry/registry-client.service.ts index 0e85eff9c9..f282ca7c83 100644 --- a/apps/server-nestjs/src/modules/registry/registry-client.service.ts +++ b/apps/server-nestjs/src/modules/registry/registry-client.service.ts @@ -1,5 +1,5 @@ import type { RegistryQuery, RegistryResponse } from './registry-http-client.service' -import { Inject, Injectable } from '@nestjs/common' +import { HttpStatus, Inject, Injectable } from '@nestjs/common' import { RegistryHttpClientService } from './registry-http-client.service' import { ROBOT_LIST_PAGE_SIZE } from './registry.constants' @@ -115,8 +115,8 @@ export class RegistryClientService { }) } - async createProject(projectName: string, storageLimit: number) { - return this.http.fetch('projects', { + async ensureProject(projectName: string, storageLimit: number): Promise { + const created = await this.http.fetch('projects', { method: 'POST', body: { project_name: projectName, @@ -124,6 +124,15 @@ export class RegistryClientService { storage_limit: storageLimit, }, }) + if (created.status >= HttpStatus.BAD_REQUEST && created.status !== HttpStatus.CONFLICT) { + throw new Error(`Harbor create project failed (${created.status})`) + } + // 409: project already exists (concurrent reconcile) — fall through to get + const fetched = await this.getProjectByName(projectName) + if (fetched.status !== HttpStatus.OK || !fetched.data) { + throw new Error(`Harbor get project failed (${fetched.status})`) + } + return fetched.data } async deleteProjectByName(projectName: string) { @@ -157,12 +166,16 @@ export class RegistryClientService { }) } - async addGroupMember(projectName: string, body: HarborGroupMemberRequest) { - return this.http.fetch(`projects/${encodeURIComponent(projectName)}/members`, { + async ensureGroupMember(projectName: string, body: HarborGroupMemberRequest) { + const created = await this.http.fetch(`projects/${encodeURIComponent(projectName)}/members`, { method: 'POST', headers: { 'X-Is-Resource-Name': 'true' }, body, }) + if (created.status >= HttpStatus.BAD_REQUEST && created.status !== HttpStatus.CONFLICT) { + throw new Error(`Harbor create member failed (${created.status})`) + } + // 409: member already exists (concurrent reconcile) — no-op } async removeGroupMember(projectName: string, memberId: number) { @@ -178,11 +191,17 @@ export class RegistryClientService { }) } - async createRobot(body: HarborRobotCreateRequest) { - return this.http.fetch('robots', { + // 409 → robot already exists, secret unrecoverable; caller rotates for a fresh secret. + async ensureRobot(body: HarborRobotCreateRequest): Promise { + const created = await this.http.fetch('robots', { method: 'POST', body, }) + if (created.status === HttpStatus.CONFLICT) return null + if (created.status >= HttpStatus.BAD_REQUEST || !created.data) { + throw new Error(`Harbor create robot failed (${created.status})`) + } + return created.data } async deleteRobot(robotId: number): Promise { @@ -198,6 +217,23 @@ export class RegistryClientService { return Number.isFinite(retentionId) ? retentionId : null } + // 409 → already exists; re-read id and update instead. + async ensureRetention(projectName: string, body: HarborRetentionPolicy) { + const created = await this.createRetention(body) + if (created.status === HttpStatus.CONFLICT) { + const racedId = await this.getRetentionId(projectName) + if (racedId) { + const result = await this.updateRetention(racedId, body) + if (result.status >= HttpStatus.BAD_REQUEST) { + throw new Error(`Harbor retention policy failed (${result.status})`) + } + } + return + } + if (created.status >= HttpStatus.BAD_REQUEST) { + throw new Error(`Harbor retention policy failed (${created.status})`) + } + } async createRetention(body: HarborRetentionPolicy) { return this.http.fetch('retentions', { method: 'POST', From d84f73c6d81d145aff6422c57404963c26830da3 Mon Sep 17 00:00:00 2001 From: William Phetsinorath Date: Fri, 28 Aug 2026 19:14:03 +0200 Subject: [PATCH 2/2] chore(registry): trim redundant JSDoc on ensure* methods Signed-off-by: William Phetsinorath Change-Id: Ie82b4c47953aa13959e3dd34fb99afba6a6a6964 --- .../src/modules/registry/registry-client.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/server-nestjs/src/modules/registry/registry-client.service.ts b/apps/server-nestjs/src/modules/registry/registry-client.service.ts index f282ca7c83..356f094bda 100644 --- a/apps/server-nestjs/src/modules/registry/registry-client.service.ts +++ b/apps/server-nestjs/src/modules/registry/registry-client.service.ts @@ -127,7 +127,6 @@ export class RegistryClientService { if (created.status >= HttpStatus.BAD_REQUEST && created.status !== HttpStatus.CONFLICT) { throw new Error(`Harbor create project failed (${created.status})`) } - // 409: project already exists (concurrent reconcile) — fall through to get const fetched = await this.getProjectByName(projectName) if (fetched.status !== HttpStatus.OK || !fetched.data) { throw new Error(`Harbor get project failed (${fetched.status})`) @@ -175,7 +174,6 @@ export class RegistryClientService { if (created.status >= HttpStatus.BAD_REQUEST && created.status !== HttpStatus.CONFLICT) { throw new Error(`Harbor create member failed (${created.status})`) } - // 409: member already exists (concurrent reconcile) — no-op } async removeGroupMember(projectName: string, memberId: number) { @@ -191,7 +189,6 @@ export class RegistryClientService { }) } - // 409 → robot already exists, secret unrecoverable; caller rotates for a fresh secret. async ensureRobot(body: HarborRobotCreateRequest): Promise { const created = await this.http.fetch('robots', { method: 'POST', @@ -217,7 +214,6 @@ export class RegistryClientService { return Number.isFinite(retentionId) ? retentionId : null } - // 409 → already exists; re-read id and update instead. async ensureRetention(projectName: string, body: HarborRetentionPolicy) { const created = await this.createRetention(body) if (created.status === HttpStatus.CONFLICT) {