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
1 change: 1 addition & 0 deletions apps/server-nestjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@fastify/swagger": "^8.15.0",
"@fastify/swagger-ui": "^4.2.0",
"@gitbeaker/core": "^40.6.0",
"@gitbeaker/requester-utils": "^40.6.0",
"@gitbeaker/rest": "^40.6.0",
"@keycloak/keycloak-admin-client": "^24.0.0",
"@kubernetes-models/argo-cd": "^2.7.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,58 @@ export class ConfigurationService {
= process.env.CONTACT_EMAIL
?? 'cloudpinative-relations@interieur.gouv.fr'

// argocd
argoNamespace = process.env.ARGO_NAMESPACE ?? 'argocd'
argocdUrl = process.env.ARGOCD_URL
argocdExtraRepositories = process.env.ARGOCD_EXTRA_REPOSITORIES

// dso
dsoEnvChartVersion = process.env.DSO_ENV_CHART_VERSION ?? 'dso-env-1.6.0'
dsoNsChartVersion = process.env.DSO_NS_CHART_VERSION ?? 'dso-ns-1.1.5'

// plugins
mockPlugins = process.env.MOCK_PLUGINS === 'true'
projectRootDir = process.env.PROJECTS_ROOT_DIR
projectRootPath = process.env.PROJECTS_ROOT_DIR
pluginsDir = process.env.PLUGINS_DIR ?? '/plugins'

// gitlab
gitlabToken = process.env.GITLAB_TOKEN
gitlabUrl = process.env.GITLAB_URL
gitlabInternalUrl = process.env.GITLAB_INTERNAL_URL
? process.env.GITLAB_INTERNAL_URL
: process.env.GITLAB_URL

gitlabMirrorTokenExpirationDays = Number(process.env.GITLAB_MIRROR_TOKEN_EXPIRATION_DAYS ?? 180)
gitlabMirrorTokenRotationThresholdDays = Number(process.env.GITLAB_MIRROR_TOKEN_ROTATION_THRESHOLD_DAYS ?? 90)

// vault
vaultToken = process.env.VAULT_TOKEN
vaultUrl = process.env.VAULT_URL
vaultInternalUrl = process.env.VAULT_INTERNAL_URL
? process.env.VAULT_INTERNAL_URL
: process.env.VAULT_URL

vaultKvName = process.env.VAULT_KV_NAME ?? 'forge-dso'

// registry (harbor)
harborUrl = process.env.HARBOR_URL
harborInternalUrl = process.env.HARBOR_INTERNAL_URL ?? process.env.HARBOR_URL
harborAdmin = process.env.HARBOR_ADMIN
harborAdminPassword = process.env.HARBOR_ADMIN_PASSWORD
harborRuleTemplate = process.env.HARBOR_RULE_TEMPLATE
harborRuleCount = process.env.HARBOR_RULE_COUNT
harborRetentionCron = process.env.HARBOR_RETENTION_CRON

// nexus
nexusUrl = process.env.NEXUS_URL
nexusInternalUrl = process.env.NEXUS_INTERNAL_URL ?? process.env.NEXUS_URL
nexusAdmin = process.env.NEXUS_ADMIN
nexusAdminPassword = process.env.NEXUS_ADMIN_PASSWORD
nexusSecretExposedUrl
= process.env.NEXUS__SECRET_EXPOSE_INTERNAL_URL === 'true'
? (process.env.NEXUS_INTERNAL_URL ?? process.env.NEXUS_URL)
: process.env.NEXUS_URL

NODE_ENV
= process.env.NODE_ENV === 'test'
? 'test'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { Test } from '@nestjs/testing'
import type { TestingModule } from '@nestjs/testing'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import type { Mocked } from 'vitest'
import { stringify } from 'yaml'
import { ArgoCDControllerService } from './argocd-controller.service'
import { ArgoCDDatastoreService } from './argocd-datastore.service'
import type { ProjectWithDetails } from './argocd-datastore.service'
import { ConfigurationService } from '@/cpin-module/infrastructure/configuration/configuration.service'
import { GitlabService } from '../gitlab/gitlab.service'
import { VaultService } from '../vault/vault.service'
import { generateNamespaceName } from '@cpn-console/shared'
import { makeProjectSchema } from '../gitlab/gitlab-testing.utils'

function createArgoCDControllerServiceTestingModule() {
return Test.createTestingModule({
providers: [
ArgoCDControllerService,
{
provide: ArgoCDDatastoreService,
useValue: {
getAllProjects: vi.fn(),
} satisfies Partial<ArgoCDDatastoreService>,
},
{
provide: ConfigurationService,
useValue: {
argoNamespace: 'argocd',
argocdUrl: 'https://argocd.internal',
argocdExtraRepositories: 'repo3',
dsoEnvChartVersion: 'dso-env-1.6.0',
dsoNsChartVersion: 'dso-ns-1.1.5',
} satisfies Partial<ConfigurationService>,
},
{
provide: GitlabService,
useValue: {
getOrCreateInfraGroupRepo: vi.fn(),
getProjectGroupPublicUrl: vi.fn(),
getInfraGroupRepoPublicUrl: vi.fn(),
maybeCommitUpdate: vi.fn(),
maybeCommitDelete: vi.fn(),
listFiles: vi.fn(),
} satisfies Partial<GitlabService>,
},
{
provide: VaultService,
useValue: {
readProjectValues: vi.fn(),
} satisfies Partial<VaultService>,
},
],
})
}

describe('argoCDControllerService', () => {
let service: ArgoCDControllerService
let datastore: Mocked<ArgoCDDatastoreService>
let gitlab: Mocked<GitlabService>
let vault: Mocked<VaultService>

beforeEach(async () => {
vi.clearAllMocks()
const module: TestingModule = await createArgoCDControllerServiceTestingModule().compile()
service = module.get(ArgoCDControllerService)
datastore = module.get(ArgoCDDatastoreService)
gitlab = module.get(GitlabService)
vault = module.get(VaultService)
})

it('should be defined', () => {
expect(service).toBeDefined()
})

describe('reconcile', () => {
it('should sync project environments', async () => {
const mockProject = {
id: '123e4567-e89b-12d3-a456-426614174000',
slug: 'project-1',
name: 'Project 1',
environments: [
{ id: '123e4567-e89b-12d3-a456-426614174001', name: 'dev', clusterId: 'c1', cpu: 1, gpu: 0, memory: 1, autosync: true },
{ id: '123e4567-e89b-12d3-a456-426614174002', name: 'prod', clusterId: 'c1', cpu: 1, gpu: 0, memory: 1, autosync: true },
],
clusters: [
{ id: 'c1', label: 'cluster-1', zone: { slug: 'zone-1' } },
],
repositories: [
{
id: 'repo-1',
internalRepoName: 'infra-repo',
isInfra: true,
deployRevision: 'HEAD',
deployPath: '.',
helmValuesFiles: '',
},
],
plugins: [{ pluginName: 'argocd', key: 'extraRepositories', value: 'repo2' }],
} satisfies ProjectWithDetails

datastore.getAllProjects.mockResolvedValue([mockProject])
gitlab.getOrCreateInfraGroupRepo.mockResolvedValue(makeProjectSchema({ id: 100, http_url_to_repo: 'https://gitlab.internal/infra' }))
gitlab.getProjectGroupPublicUrl.mockResolvedValue('https://gitlab.internal/group')
gitlab.getInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo')
gitlab.listFiles.mockResolvedValue([])
vault.readProjectValues.mockResolvedValue({ secret: 'value' })

const results = await service.reconcile()

expect(results).toHaveLength(3) // 2 envs + 1 cleanup (1 zone)

// Verify Gitlab calls
expect(gitlab.maybeCommitUpdate).toHaveBeenCalledTimes(2)
expect(gitlab.maybeCommitUpdate).toHaveBeenCalledWith(
100,
[
{
content: stringify({
common: {
'dso/project': 'Project 1',
'dso/project.id': '123e4567-e89b-12d3-a456-426614174000',
'dso/project.slug': 'project-1',
'dso/environment': 'dev',
'dso/environment.id': '123e4567-e89b-12d3-a456-426614174001',
},
argocd: {
cluster: 'in-cluster',
namespace: 'argocd',
project: 'project-1-dev-6293',
envChartVersion: 'dso-env-1.6.0',
nsChartVersion: 'dso-ns-1.1.5',
},
environment: {
valueFileRepository: 'https://gitlab.internal/infra',
valueFileRevision: 'HEAD',
valueFilePath: 'Project 1/cluster-1/dev/values.yaml',
roGroup: '/project-project-1/console/dev/RO',
rwGroup: '/project-project-1/console/dev/RW',
},
application: {
quota: {
cpu: 1,
gpu: 0,
memory: '1Gi',
},
sourceRepositories: [
'https://gitlab.internal/group/**',
'repo3',
'repo2',
],
destination: {
namespace: generateNamespaceName(mockProject.id, mockProject.environments[0].id),
name: 'cluster-1',
},
autosync: true,
vault: { secret: 'value' },
repositories: [
{
repoURL: 'https://gitlab.internal/infra-repo',
targetRevision: 'HEAD',
path: '.',
valueFiles: [],
},
],
},
}),
filePath: 'Project 1/cluster-1/dev/values.yaml',
},
],
'ci: :robot_face: Update Project 1/cluster-1/dev/values.yaml',
)
})

it('should handle errors gracefully', async () => {
const mockProject = {
id: '123e4567-e89b-12d3-a456-426614174000',
slug: 'project-1',
name: 'Project 1',
environments: [{ id: '123e4567-e89b-12d3-a456-426614174001', name: 'dev', clusterId: 'c1', cpu: 1, gpu: 0, memory: 1, autosync: true }],
clusters: [
{ id: 'c1', label: 'cluster-1', zone: { slug: 'zone-1' } },
],
} as unknown as ProjectWithDetails

datastore.getAllProjects.mockResolvedValue([mockProject])
gitlab.getOrCreateInfraGroupRepo.mockRejectedValue(new Error('Sync failed'))

const results = await service.reconcile()

// 1 env (fails) + 1 cleanup (fails because getOrCreateInfraProject fails)
expect(results).toHaveLength(2)
const failed = results.filter((r: any) => r.status === 'rejected')
expect(failed).toHaveLength(2)
})
})
})
Loading
Loading