Skip to content
Open
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
2 changes: 1 addition & 1 deletion apps/nginx-strangler/conf.d/routing.conf
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ server {
proxy_set_header X-Forwarded-Proto $scheme;
}

location /api/v1/deployments {
location ~ ^/api/v1/projects/[^/]+/deployments(.*)$ {
proxy_pass http://server-nestjs;
proxy_http_version 1.1;
proxy_set_header Host $host;
Expand Down
57 changes: 57 additions & 0 deletions apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,63 @@ describe('argoCDService', () => {
expect(service).toBeDefined()
})

describe('handleArgoCDProjectUpdate', () => {
it('should return OK result when ensureProject succeeds', async () => {
const mockProject = makeProjectWithDetails({
slug: 'project-1',
name: 'Project 1',
environments: [],
repositories: [],
deployments: [],
})

const infraProject = makeProjectSchema({ id: 100, http_url_to_repo: 'https://gitlab.internal/infra' })
gitlab.getOrCreateInfraGroupRepo.mockResolvedValue(infraProject)
gitlab.getOrCreateProjectGroupPublicUrl.mockResolvedValue('https://gitlab.internal/group')
gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.readProjectValues.mockResolvedValue({})
gitlab.generateCreateOrUpdateAction.mockResolvedValue(null)
gitlab.maybeCreateCommit.mockResolvedValue(undefined)

const result = await service.handleArgoCDProjectUpdate(mockProject)

expect(result).toEqual({
argocd: expect.objectContaining({
status: 'OK',
message: 'Up to date',
executionTime: expect.any(Number),
}),
})
})

it('should return KO result when ensureProject throws', async () => {
const mockProject = makeProjectWithDetails({
slug: 'project-1',
name: 'Project 1',
environments: [
makeProjectEnvironment({ name: 'dev', cluster: { id: 'c1', label: 'cluster-1', zone: { slug: 'zone-1' } } }),
],
repositories: [makeProjectRepository({ internalRepoName: 'infra-repo', isInfra: true })],
deployments: [],
})

const error = new Error('GitLab unreachable')
gitlab.getOrCreateInfraGroupRepo.mockRejectedValue(error)

const result = await service.handleArgoCDProjectUpdate(mockProject)

expect(result).toEqual({
argocd: expect.objectContaining({
status: 'KO',
message: 'GitLab unreachable',
executionTime: expect.any(Number),
error,
}),
})
})
})

it('should sync project environments', async () => {
const mockProject = makeProjectWithDetails({
slug: 'project-1',
Expand Down
44 changes: 37 additions & 7 deletions apps/server-nestjs/src/modules/argocd/argocd.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { CommitAction, CondensedProjectSchema, ProjectSchema, SimpleProjectSchema } from '@gitbeaker/core'
import type { RequiredServiceResult } from '../plugin/plugin.utils'
import type { ProjectWithDetails } from './argocd-datastore.service'
import { createHmac } from 'node:crypto'
import { generateNamespaceName, inClusterLabel } from '@cpn-console/shared'
Expand Down Expand Up @@ -36,6 +37,40 @@ export class ArgoCDService {
this.logger.log('ArgoCDService initialized')
}

@OnEvent('project.argocd.update')
@StartActiveSpan()
async handleArgoCDProjectUpdate(project: ProjectWithDetails): Promise<RequiredServiceResult<'argocd'>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Better just using project.upsert, its designed to run on any changes in a GitOps style.

const start = process.hrtime.bigint()
const span = trace.getActiveSpan()
span?.setAttribute('project.slug', project.slug)
this.logger.log(`Handling a argocd project update event for ${project.slug}`)
try {
await this.ensureProject(project)
const end = process.hrtime.bigint()
const executionTime = Number(end - start) / 1_000_000
this.logger.log(`ArgoCD sync completed for project ${project.slug}`)
return {
argocd: {
status: 'OK',
message: 'Up to date',
executionTime,
},
}
} catch (error: unknown) {
const end = process.hrtime.bigint()
const executionTime = Number(end - start) / 1_000_000
this.logger.error(`ArgoCD sync failed for project ${project.slug}`, error)
return {
argocd: {
error,
status: 'KO',
message: error instanceof Error ? error.message : 'Erreur inconnue',
executionTime,
},
}
}
}

@OnEvent('project.upsert')
@StartActiveSpan()
async handleUpsert(project: ProjectWithDetails) {
Expand Down Expand Up @@ -220,7 +255,7 @@ export class ArgoCDService {
'environment.id': environment.id,
'environment.name': environment.name,
})
const vaultValues = await this.vault.readProjectValues(project.id) ?? {}
const vaultValues = await this.vault.readProjectValues(project.slug) ?? {}
const cluster = environment.cluster
if (!cluster) {
this.logger.warn(`Cluster not found for environment ${environment.id} in project ${project.slug}`)
Expand All @@ -230,11 +265,6 @@ export class ArgoCDService {

const valueFilePath = formatEnvironmentValuesFilePath(project, cluster, environment)

const repo = project.repositories.find(r => r.isInfra)
if (!repo) {
this.logger.warn(`Infrastructure repository not found for project ${project.slug} (projectId=${project.id})`)
return null
}
const gitlabPublicProjectUrl = `${(await this.gitlab.getOrCreateProjectGroupPublicUrl())}/${project.slug}`

const values = formatValues({
Expand Down Expand Up @@ -297,7 +327,7 @@ export class ArgoCDService {
'environment.id': environment.id,
'environment.name': environment.name,
})
const vaultValues = await this.vault.readProjectValues(project.id) ?? {}
const vaultValues = await this.vault.readProjectValues(project.slug) ?? {}
const cluster = environment.cluster
if (!cluster) {
this.logger.warn(`Cluster not found for environment ${environment.id} in project ${project.slug}`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('deploymentDatastoreService', () => {
include: { repository: true },
},
},
orderBy: { createdAt: 'asc' },
})
expect(result).toEqual(deployments)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class DeploymentDatastoreService {
include: { repository: true },
},
},
orderBy: { createdAt: 'asc' },
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Deployment, DeploymentSource, Environment, Prisma, Repository } from '@prisma/client'
import { faker } from '@faker-js/faker'

export function makeDeployment(overrides: Partial<Deployment> = {}): Deployment {
return {
id: faker.string.uuid(),
projectId: faker.string.uuid(),
name: faker.string.alphanumeric(8).toLowerCase(),
autosync: true,
environmentId: faker.string.uuid(),
createdAt: faker.date.past(),
updatedAt: faker.date.past(),
...overrides,
}
}

export function makeEnvironment(overrides: Partial<Environment> = {}): Environment {
return {
id: faker.string.uuid(),
name: faker.string.alphanumeric(8).toLowerCase(),
projectId: faker.string.uuid(),
memory: 2,
cpu: 1,
gpu: 0,
autosync: true,
createdAt: faker.date.past(),
updatedAt: faker.date.past(),
clusterId: faker.string.uuid(),
stageId: faker.string.uuid(),
...overrides,
}
}

export function makeRepository(overrides: Partial<Repository> = {}): Repository {
return {
id: faker.string.uuid(),
projectId: faker.string.uuid(),
internalRepoName: faker.string.alphanumeric(8).toLowerCase(),
externalRepoUrl: '',
externalUserName: '',
isInfra: false,
isPrivate: false,
deployRevision: '',
deployPath: '',
helmValuesFiles: '',
createdAt: faker.date.past(),
updatedAt: faker.date.past(),
...overrides,
}
}

type DeploymentSourceWithRepository = DeploymentSource & { repository: Repository }

export function makeDeploymentSource(overrides: Partial<DeploymentSourceWithRepository> = {}): DeploymentSourceWithRepository {
const repositoryId = overrides.repositoryId ?? faker.string.uuid()
return {
id: faker.string.uuid(),
createdAt: faker.date.past(),
updatedAt: faker.date.past(),
deploymentId: faker.string.uuid(),
repositoryId,
type: 'git',
targetRevision: 'main',
path: '/app',
helmValuesFiles: '',
repository: makeRepository({ id: repositoryId }),
...overrides,
}
}

export type DeploymentWithRelations = Prisma.DeploymentGetPayload<{
include: {
environment: true
deploymentSources: { include: { repository: true } }
}
}>

export function makeDeploymentWithRelations(overrides: Partial<DeploymentWithRelations> = {}): DeploymentWithRelations {
const base = makeDeployment(overrides)
return {
...base,
environment: overrides.environment ?? makeEnvironment({ id: base.environmentId, projectId: base.projectId }),
deploymentSources: overrides.deploymentSources ?? [makeDeploymentSource({ deploymentId: base.id })],
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { CreateDeployment, UpdateDeployment } from '@cpn-console/shared'
import type { TestingModule } from '@nestjs/testing'
import type { DeepMockProxy } from 'vitest-mock-extended'
import type { ProjectExecutionContext } from '../infrastructure/permission/project/project-context.decorator'
import { Test } from '@nestjs/testing'
import { beforeEach, describe, expect, it } from 'vitest'
import { mockDeep } from 'vitest-mock-extended'
import { ProjectGuard } from '../infrastructure/permission/project/project.guard'
import { makeDeployment, makeDeploymentWithRelations } from './deployment-testing.utils'
import { DeploymentController } from './deployment.controller'
import { DeploymentService } from './deployment.service'

Expand All @@ -12,18 +15,11 @@ describe('deploymentController', () => {
let controller: DeploymentController
let service: DeepMockProxy<DeploymentService>

beforeEach(async () => {
service = mockDeep<DeploymentService>()

module = await Test.createTestingModule({
controllers: [DeploymentController],
providers: [
{ provide: DeploymentService, useValue: service },
],
}).compile()

controller = module.get<DeploymentController>(DeploymentController)
})
const projectCtx: ProjectExecutionContext = {
projectId: '11111111-1111-1111-1111-111111111111',
userId: 'user-uuid-1234',
requestId: 'request-uuid-5678',
}

const validCreateDeployment = {
name: 'dev',
Expand Down Expand Up @@ -54,12 +50,17 @@ describe('deploymentController', () => {
} satisfies UpdateDeployment

beforeEach(async () => {
service = mockDeep<DeploymentService>()

module = await Test.createTestingModule({
controllers: [DeploymentController],
providers: [
{ provide: DeploymentService, useValue: service },
],
}).compile()
})
.overrideGuard(ProjectGuard)
.useValue({ canActivate: () => true })
.compile()

controller = module.get<DeploymentController>(DeploymentController)
})
Expand All @@ -71,7 +72,7 @@ describe('deploymentController', () => {
describe('list', () => {
it('should call deploymentService.listByProjectId with projectId', async () => {
const projectId = '11111111-1111-1111-1111-111111111111'
const expectedResult = [{ id: 'deployment-1' }]
const expectedResult = [makeDeploymentWithRelations({ projectId })]

service.listByProjectId.mockResolvedValue(expectedResult)

Expand All @@ -84,14 +85,14 @@ describe('deploymentController', () => {

describe('create', () => {
it('should validate body and call deploymentService.createDeployment', async () => {
const expectedResult = { id: 'new-deployment-id' }
const expectedResult = makeDeployment()

service.createDeployment.mockResolvedValue(expectedResult)

const result = await controller.create(validCreateDeployment)
const result = await controller.create(validCreateDeployment, projectCtx)

expect(service.createDeployment).toHaveBeenCalledWith(
validCreateDeployment.projectId,
projectCtx,
validCreateDeployment,
)
expect(result).toEqual(expectedResult)
Expand All @@ -101,13 +102,14 @@ describe('deploymentController', () => {
describe('update', () => {
it('should validate body and call deploymentService.updateDeployment', async () => {
const deploymentId = '55555555-5555-5555-5555-555555555555'
const expectedResult = { id: deploymentId }
const expectedResult = makeDeployment({ id: deploymentId })

service.updateDeployment.mockResolvedValue(expectedResult)

const result = await controller.update(deploymentId, validUpdateDeployment)
const result = await controller.update(deploymentId, validUpdateDeployment, projectCtx)

expect(service.updateDeployment).toHaveBeenCalledWith(
projectCtx,
deploymentId,
validUpdateDeployment,
)
Expand All @@ -121,9 +123,9 @@ describe('deploymentController', () => {

service.deleteDeployment.mockResolvedValue(undefined)

const result = await controller.delete(deploymentId)
const result = await controller.delete(deploymentId, projectCtx)

expect(service.deleteDeployment).toHaveBeenCalledWith(deploymentId)
expect(service.deleteDeployment).toHaveBeenCalledWith(projectCtx, deploymentId)
expect(result).toBeUndefined()
})
})
Expand Down
Loading