Skip to content

Commit ebcfdc5

Browse files
committed
refactor(argocd): migrate ArgoCD to NestJS
Signed-off-by: William Phetsinorath <william.phetsinorath-open@interieur.gouv.fr>
1 parent ee758c5 commit ebcfdc5

12 files changed

Lines changed: 570 additions & 2 deletions

File tree

apps/server-nestjs/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"dotenv": "^16.4.7",
5555
"fastify": "^4.29.1",
5656
"fastify-keycloak-adapter": "2.3.2",
57+
"js-yaml": "^4.1.1",
5758
"json-2-csv": "^5.5.7",
5859
"keycloak-connect": "^25.0.0",
5960
"mustache": "^4.2.0",
@@ -78,6 +79,7 @@
7879
"@nestjs/testing": "^11.0.1",
7980
"@types/express": "^5.0.0",
8081
"@types/jest": "^30.0.0",
82+
"@types/js-yaml": "4.0.9",
8183
"@types/node": "^22.10.7",
8284
"@types/supertest": "^6.0.2",
8385
"@vitest/coverage-v8": "^2.1.8",

apps/server-nestjs/src/cpin-module/infrastructure/configuration/configuration.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ export class ConfigurationService {
3434
= process.env.CONTACT_EMAIL
3535
?? 'cloudpinative-relations@interieur.gouv.fr'
3636

37+
// argocd
38+
argoNamespace = process.env.ARGO_NAMESPACE ?? 'argocd'
39+
argocdUrl = process.env.ARGOCD_URL
40+
argocdExtraRepositories = process.env.ARGOCD_EXTRA_REPOSITORIES
41+
42+
// dso
43+
dsoEnvChartVersion = process.env.DSO_ENV_CHART_VERSION ?? 'dso-env-1.6.0'
44+
dsoNsChartVersion = process.env.DSO_NS_CHART_VERSION ?? 'dso-ns-1.1.5'
45+
3746
// plugins
3847
mockPlugins = process.env.MOCK_PLUGINS === 'true'
3948
projectRootDir = process.env.PROJECTS_ROOT_DIR

apps/server-nestjs/src/main.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { ScheduleModule } from '@nestjs/schedule'
55
import { CpinModule } from './cpin-module/cpin.module'
66
import { IamModule } from './modules/iam/iam.module'
77
import { KeycloakModule } from './modules/keycloak/keycloak.module'
8+
import { ArgoCDModule } from './modules/argocd/argocd.module'
9+
import { GitlabModule } from './modules/gitlab/gitlab.module'
10+
import { VaultModule } from './modules/vault/vault.module'
811

912
// This module only exists to import other module.
1013
// « One module to rule them all, and in NestJs bind them »
@@ -13,6 +16,9 @@ import { KeycloakModule } from './modules/keycloak/keycloak.module'
1316
CpinModule,
1417
IamModule,
1518
KeycloakModule,
19+
ArgoCDModule,
20+
GitlabModule,
21+
VaultModule,
1622
EventEmitterModule.forRoot(),
1723
ScheduleModule.forRoot(),
1824
],
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { describe, it, expect, beforeEach, vi } from 'vitest'
2+
import type { Mocked } from 'vitest'
3+
import { ArgoCDControllerService } from './argocd-controller.service'
4+
import type { ArgoCDDatastoreService, ProjectWithDetails } from './argocd-datastore.service'
5+
import type { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'
6+
import type { GitlabService } from '../gitlab/gitlab.service'
7+
import type { VaultService } from '../vault/vault.service'
8+
import type { Project } from '@cpn-console/hooks'
9+
10+
const mockArgoCDDatastore = {
11+
getAllProjects: vi.fn(),
12+
} as unknown as Mocked<ArgoCDDatastoreService>
13+
14+
const mockConfigService = {
15+
keycloakControllerPurgeOrphans: true,
16+
argoNamespace: 'argocd',
17+
argocdUrl: 'http://argocd',
18+
argocdExtraRepositories: 'repo3',
19+
dsoEnvChartVersion: 'dso-env-1.6.0',
20+
dsoNsChartVersion: 'dso-ns-1.1.5',
21+
} as unknown as Mocked<ConfigurationService>
22+
23+
const mockGitlabService = {
24+
getOrCreateInfraProject: vi.fn(),
25+
getPublicGroupUrl: vi.fn(),
26+
getPublicRepoUrl: vi.fn(),
27+
commitCreateOrUpdate: vi.fn(),
28+
commitDelete: vi.fn(),
29+
listFiles: vi.fn(),
30+
} as unknown as Mocked<GitlabService>
31+
32+
const mockVaultService = {
33+
getProjectValues: vi.fn(),
34+
} as unknown as Mocked<VaultService>
35+
36+
describe('argoCDControllerService', () => {
37+
let service: ArgoCDControllerService
38+
let datastore: Mocked<any>
39+
let gitlabService: Mocked<GitlabService>
40+
let vaultService: Mocked<VaultService>
41+
42+
beforeEach(() => {
43+
service = new ArgoCDControllerService(
44+
mockArgoCDDatastore,
45+
mockConfigService,
46+
mockGitlabService,
47+
mockVaultService,
48+
)
49+
datastore = mockArgoCDDatastore
50+
gitlabService = mockGitlabService
51+
vaultService = mockVaultService
52+
vi.clearAllMocks()
53+
})
54+
55+
it('should be defined', () => {
56+
expect(service).toBeDefined()
57+
})
58+
59+
describe('reconcile', () => {
60+
it('should sync project environments', async () => {
61+
const mockProject = {
62+
id: '123e4567-e89b-12d3-a456-426614174000',
63+
slug: 'project-1',
64+
name: 'Project 1',
65+
environments: [
66+
{ id: '123e4567-e89b-12d3-a456-426614174001', name: 'dev', clusterId: 'c1', cpu: 1, gpu: 0, memory: 1, autosync: true },
67+
{ id: '123e4567-e89b-12d3-a456-426614174002', name: 'prod', clusterId: 'c1', cpu: 1, gpu: 0, memory: 1, autosync: true },
68+
],
69+
clusters: [
70+
{ id: 'c1', label: 'cluster-1', zone: { slug: 'zone-1' } },
71+
],
72+
repositories: [],
73+
store: { argocd: { extraRepositories: 'repo2' } },
74+
} as unknown as ProjectWithDetails
75+
76+
datastore.getAllProjects.mockResolvedValue([mockProject])
77+
gitlabService.getOrCreateInfraProject.mockResolvedValue({ id: 100, http_url_to_repo: 'http://gitlab/infra' })
78+
gitlabService.getPublicGroupUrl.mockResolvedValue('http://gitlab/group')
79+
gitlabService.listFiles.mockResolvedValue([])
80+
vaultService.getProjectValues.mockResolvedValue({ secret: 'value' })
81+
82+
const results = await (service as any).reconcile()
83+
84+
expect(results).toHaveLength(3) // 2 envs + 1 remove call
85+
86+
// Verify Gitlab calls
87+
expect(gitlabService.commitCreateOrUpdate).toHaveBeenCalledTimes(2)
88+
expect(gitlabService.commitCreateOrUpdate).toHaveBeenCalledWith(
89+
100,
90+
expect.stringContaining('dso/project: Project 1'),
91+
'Project 1/cluster-1/dev/values.yaml',
92+
)
93+
})
94+
95+
it('should handle errors gracefully', async () => {
96+
const mockProject = {
97+
id: '123e4567-e89b-12d3-a456-426614174000',
98+
slug: 'project-1',
99+
name: 'Project 1',
100+
environments: [{ id: '123e4567-e89b-12d3-a456-426614174001', name: 'dev', clusterId: 'c1', cpu: 1, gpu: 0, memory: 1, autosync: true }],
101+
clusters: [
102+
{ id: 'c1', label: 'cluster-1', zone: { slug: 'zone-1' } },
103+
],
104+
} as unknown as ProjectWithDetails
105+
106+
datastore.getAllProjects.mockResolvedValue([mockProject])
107+
gitlabService.getOrCreateInfraProject.mockRejectedValue(new Error('Sync failed'))
108+
109+
const results = await (service as any).reconcile()
110+
111+
expect(results).toHaveLength(2) // 1 failed env + 1 remove call (which also fails in loop)
112+
const failed = results.find((r: any) => r.status === 'rejected')
113+
expect(failed).toBeDefined()
114+
})
115+
})
116+
117+
describe('handleProjectDeleted', () => {
118+
it('should delete project resources', async () => {
119+
const mockProject = {
120+
id: '123e4567-e89b-12d3-a456-426614174000',
121+
slug: 'project-1',
122+
name: 'Project 1',
123+
clusters: [
124+
{ id: 'c1', label: 'cluster-1', zone: { slug: 'zone-1' } },
125+
],
126+
} as unknown as Project
127+
128+
gitlabService.getOrCreateInfraProject.mockResolvedValue({ id: 100, http_url_to_repo: 'http://gitlab/infra' })
129+
gitlabService.listFiles.mockResolvedValue([
130+
{ name: 'values.yaml', path: 'Project 1/values.yaml', type: 'blob' },
131+
])
132+
133+
await service.handleProjectDeleted(mockProject)
134+
135+
expect(gitlabService.commitDelete).toHaveBeenCalledWith(100, ['Project 1/values.yaml'])
136+
})
137+
})
138+
})

0 commit comments

Comments
 (0)