Skip to content
Draft
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
10 changes: 5 additions & 5 deletions apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ describe('argoCDService', () => {
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')
gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => {
return makeCommitAction({ filePath, content })
})
Expand Down Expand Up @@ -447,7 +447,7 @@ describe('argoCDService', () => {
),
])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')
gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => {
return makeCommitAction({ filePath, content })
})
Expand Down Expand Up @@ -538,7 +538,7 @@ describe('argoCDService', () => {
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')

gitlab.generateCreateOrUpdateAction.mockResolvedValue(null)

Expand Down Expand Up @@ -583,7 +583,7 @@ describe('argoCDService', () => {
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')
gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => {
return makeCommitAction({ filePath, content })
})
Expand Down Expand Up @@ -738,7 +738,7 @@ describe('argoCDService', () => {
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id')
vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id')
vault.ensureAuthApproleRoleSecretId.mockResolvedValue('secret-id')
gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => {
return makeCommitAction({ filePath, content })
})
Expand Down
2 changes: 1 addition & 1 deletion apps/server-nestjs/src/modules/argocd/argocd.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ export class ArgoCDService {
this.logger.warn(`Couldn't find app role (project=${projectSlug})`)
return undefined
})
const secretId = await this.vault.createAuthApproleRoleSecretId(projectSlug).catch(() => {
const secretId = await this.vault.ensureAuthApproleRoleSecretId(projectSlug).catch(() => {
this.logger.warn(`Couldn't find secret (project=${projectSlug})`)
return undefined
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import type {
import type { ConfigType } from '@nestjs/config'
import { join } from 'node:path'
import { defaultBranchName } from '@cpn-console/shared'
import { GitbeakerRequestError } from '@gitbeaker/requester-utils'
import { Gitlab as GitlabRest } from '@gitbeaker/rest'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { gitlabConfigFactory } from '../../config/gitlab.config'
Expand All @@ -36,7 +35,7 @@ import {
TOPIC_SYSTEM_MANAGED,
USER_ID_CUSTOM_ATTRIBUTE_KEY,
} from './gitlab.constants'
import { generateGitlabCIConfigContent, generateMirrorScriptContent, hasFileContentChanged, hasGitbeakerCause, isGitbeakerNotFound } from './gitlab.utils'
import { generateGitlabCIConfigContent, generateMirrorScriptContent, hasFileContentChanged, hasGitbeakerCause, isGitbeakerNotFound, isGitbeakerUnauthorized } from './gitlab.utils'

export const GITLAB_REST_CLIENT = Symbol('GITLAB_REST_CLIENT')

Expand Down Expand Up @@ -638,7 +637,7 @@ export class GitlabClientService {
const self = await client.PersonalAccessTokens.show()
return self.active && !self.revoked
} catch (error) {
if (error instanceof GitbeakerRequestError && error.cause?.response.status === 401) return false
if (isGitbeakerUnauthorized(error)) return false
throw error
}
}
Expand Down
4 changes: 4 additions & 0 deletions apps/server-nestjs/src/modules/gitlab/gitlab.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,3 +259,7 @@ export function hasGitbeakerCause(error: unknown, pattern: string | RegExp): err
: JSON.stringify(error.cause?.description ?? '')
return typeof pattern === 'string' ? description.includes(pattern) : pattern.test(description)
}

export function isGitbeakerUnauthorized(error: unknown): error is GitbeakerRequestError {
return error instanceof GitbeakerRequestError && error.cause?.response?.status === 401
}
23 changes: 12 additions & 11 deletions apps/server-nestjs/src/modules/nexus/nexus-client.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'
import { StartActiveSpan } from '../infrastructure/telemetry/telemetry.decorator'
import { NexusError, NexusHttpClientService } from './nexus-http-client.service'
import { NexusHttpClientService } from './nexus-http-client.service'
import { isNexusNotFound } from './nexus.utils'

interface NexusRepositoryStorage {
blobStoreName: string
Expand Down Expand Up @@ -114,7 +115,7 @@ export class NexusClientService {
const res = await this.http.fetch<NexusMavenHostedRepository>(`repositories/maven/hosted/${name}`)
return res.data
} catch (error) {
if (error instanceof NexusError && error.status === 404) return null
if (isNexusNotFound(error)) return null
throw error
}
}
Expand Down Expand Up @@ -145,7 +146,7 @@ export class NexusClientService {
const res = await this.http.fetch<NexusMavenGroupRepository>(`repositories/maven/group/${name}`)
return res.data
} catch (error) {
if (error instanceof NexusError && error.status === 404) return null
if (isNexusNotFound(error)) return null
throw error
}
}
Expand All @@ -156,7 +157,7 @@ export class NexusClientService {
const res = await this.http.fetch<NexusNpmHostedRepository>(`repositories/npm/hosted/${name}`)
return res.data
} catch (error) {
if (error instanceof NexusError && error.status === 404) return null
if (isNexusNotFound(error)) return null
throw error
}
}
Expand All @@ -177,7 +178,7 @@ export class NexusClientService {
const res = await this.http.fetch<NexusNpmGroupRepository>(`repositories/npm/group/${name}`)
return res.data
} catch (error) {
if (error instanceof NexusError && error.status === 404) return null
if (isNexusNotFound(error)) return null
throw error
}
}
Expand All @@ -198,7 +199,7 @@ export class NexusClientService {
const res = await this.http.fetch<NexusPrivilege>(`security/privileges/${name}`)
return res.data
} catch (error) {
if (error instanceof NexusError && error.status === 404) return null
if (isNexusNotFound(error)) return null
throw error
}
}
Expand All @@ -218,7 +219,7 @@ export class NexusClientService {
try {
await this.http.fetch(`security/privileges/${name}`, { method: 'DELETE' })
} catch (error) {
if (error instanceof NexusError && error.status === 404) return
if (isNexusNotFound(error)) return
throw error
}
}
Expand All @@ -229,7 +230,7 @@ export class NexusClientService {
const res = await this.http.fetch<NexusRole>(`security/roles/${id}`)
return res.data
} catch (error) {
if (error instanceof NexusError && error.status === 404) return null
if (isNexusNotFound(error)) return null
throw error
}
}
Expand All @@ -249,7 +250,7 @@ export class NexusClientService {
try {
await this.http.fetch(`security/roles/${id}`, { method: 'DELETE' })
} catch (error) {
if (error instanceof NexusError && error.status === 404) return
if (isNexusNotFound(error)) return
throw error
}
}
Expand Down Expand Up @@ -280,7 +281,7 @@ export class NexusClientService {
try {
await this.http.fetch(`security/users/${userId}`, { method: 'DELETE' })
} catch (error) {
if (error instanceof NexusError && error.status === 404) return
if (isNexusNotFound(error)) return
throw error
}
}
Expand All @@ -290,7 +291,7 @@ export class NexusClientService {
try {
await this.http.fetch(`repositories/${name}`, { method: 'DELETE' })
} catch (error) {
if (error instanceof NexusError && error.status === 404) return
if (isNexusNotFound(error)) return
throw error
}
}
Expand Down
6 changes: 3 additions & 3 deletions apps/server-nestjs/src/modules/nexus/nexus.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { nexusConfigFactory } from '../../config/nexus.config'
import { StartActiveSpan } from '../infrastructure/telemetry/telemetry.decorator'
import { capturePluginResult } from '../plugin/plugin.utils'
import { VaultClientService } from '../vault/vault-client.service'
import { VaultError } from '../vault/vault-http-client.service'
import { isVaultNotFound } from '../vault/vault.utils'
import { NexusClientService } from './nexus-client.service'
import { NexusDatastoreService } from './nexus-datastore.service'
import {
Expand Down Expand Up @@ -437,7 +437,7 @@ export class NexusService {
try {
existingPassword = await this.vault.read(vaultPath).then(res => res.data?.NEXUS_PASSWORD)
} catch (error) {
if (error instanceof VaultError && error.kind === 'NotFound') {
if (isVaultNotFound(error)) {
existingPassword = undefined
} else {
throw error
Expand Down Expand Up @@ -581,7 +581,7 @@ export class NexusService {
try {
await this.vault.delete(vaultPath)
} catch (error) {
if (error instanceof VaultError && error.kind === 'NotFound') return
if (isVaultNotFound(error)) return
throw error
}
}
Expand Down
15 changes: 14 additions & 1 deletion apps/server-nestjs/src/modules/nexus/nexus.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { describe, expect, it } from 'vitest'
import { generateNexusCredPath } from './nexus.utils'
import { NexusError } from './nexus-http-client.service'
import { generateNexusCredPath, isNexusNotFound } from './nexus.utils'

describe('nexus path helpers', () => {
it('scopes the NEXUS credentials to the project', () => {
expect(generateNexusCredPath('forge', 'my-project')).toBe('forge/my-project/NEXUS')
})
})

describe('isNexusNotFound', () => {
it('matches a 404 NexusError', () => {
expect(isNexusNotFound(new NexusError('HttpError', 'not found', { status: 404 }))).toBe(true)
})

it('rejects a non-404 NexusError and non-Nexus errors', () => {
expect(isNexusNotFound(new NexusError('HttpError', 'conflict', { status: 409 }))).toBe(false)
expect(isNexusNotFound(new Error('boom'))).toBe(false)
expect(isNexusNotFound(null)).toBe(false)
})
})
5 changes: 5 additions & 0 deletions apps/server-nestjs/src/modules/nexus/nexus.utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ProjectWithDetails } from './nexus-datastore.service'
import { randomBytes } from 'node:crypto'
import { NexusError } from './nexus-http-client.service'

export function getPluginConfig(project: ProjectWithDetails, key: string) {
return project.plugins?.find(p => p.key === key)?.value
Expand All @@ -23,3 +24,7 @@ export function generateMavenHostedRepoName(project: ProjectWithDetails, kind: M
export function generateNpmHostedRepoName(project: ProjectWithDetails) {
return `${project.slug}-npm`
}

export function isNexusNotFound(error: unknown): error is NexusError {
return error instanceof NexusError && error.status === 404
}
4 changes: 2 additions & 2 deletions apps/server-nestjs/src/modules/registry/registry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { find } from '../../utils/iterable.utils'
import { StartActiveSpan } from '../infrastructure/telemetry/telemetry.decorator'
import { capturePluginResult } from '../plugin/plugin.utils'
import { VaultClientService } from '../vault/vault-client.service'
import { VaultError } from '../vault/vault-http-client.service'
import { isVaultNotFound } from '../vault/vault.utils'
import { RegistryClientService, roAccess, rwAccess } from './registry-client.service'
import { RegistryDatastoreService } from './registry-datastore.service'
import {
Expand Down Expand Up @@ -106,7 +106,7 @@ export class RegistryService {
const relativeVaultPath = `REGISTRY/${robotName}`
const vaultPath = getProjectVaultPath(project, this.baseConfig.projectsRootDir, relativeVaultPath)
const vaultRobotSecret = await this.vault.read<VaultRobotSecret>(vaultPath).catch((error) => {
if (error instanceof VaultError && error.kind === 'NotFound') return null
if (isVaultNotFound(error)) return null
throw error
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,48 @@ describe('vault', () => {
expect(capturedPath).toBe('forge/my-project/GITLAB')
})
})

describe('ensureAuthApproleRoleSecretId', () => {
it('mints and persists a secret-id on first sync', async () => {
let minted = false
let persisted: unknown
server.use(
http.get(`${vaultUrl}/v1/kv/data/*`, () => {
return HttpResponse.json({}, { status: HttpStatus.NOT_FOUND })
}),
http.post(`${vaultUrl}/v1/auth/approle/role/*/secret-id`, () => {
minted = true
return HttpResponse.json({ data: { secret_id: 'minted-secret' } })
}),
http.post(`${vaultUrl}/v1/kv/data/*`, async ({ request }) => {
persisted = await request.json()
return HttpResponse.json({})
}),
)

const secretId = await service.ensureAuthApproleRoleSecretId('my-project')

expect(secretId).toBe('minted-secret')
expect(minted).toBe(true)
expect(persisted).toEqual({ data: { secret_id: 'minted-secret' } })
})

it('reuses a persisted secret-id on subsequent syncs without minting', async () => {
let minted = false
server.use(
http.get(`${vaultUrl}/v1/kv/data/*`, () => {
return HttpResponse.json({ data: { data: { secret_id: 'persisted-secret' }, metadata: { created_time: '2023-01-01T00:00:00.000Z', version: 1 } } })
}),
http.post(`${vaultUrl}/v1/auth/approle/role/*/secret-id`, () => {
minted = true
return HttpResponse.json({ data: { secret_id: 'unexpected' } })
}),
)

const secretId = await service.ensureAuthApproleRoleSecretId('my-project')

expect(secretId).toBe('persisted-secret')
expect(minted).toBe(false)
})
})
})
Loading