From e45f052d591d917fc7d93d0f38e110f569a3f369 Mon Sep 17 00:00:00 2001 From: Kevin Powell Noumbissie <10553243+KepoParis@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:40:50 +0200 Subject: [PATCH] feat(argocd): emit deployment value sources in generated values --- .../argocd/argocd-datastore.service.ts | 20 +++++ .../modules/argocd/argocd-testing.utils.ts | 27 ++++++ .../src/modules/argocd/argocd.service.spec.ts | 89 ++++++++++++++++++- .../src/modules/argocd/argocd.service.ts | 35 ++++++++ .../modules/project/project-queries.utils.ts | 20 +++++ 5 files changed, 189 insertions(+), 2 deletions(-) diff --git a/apps/server-nestjs/src/modules/argocd/argocd-datastore.service.ts b/apps/server-nestjs/src/modules/argocd/argocd-datastore.service.ts index efbf280d09..407933a2d0 100644 --- a/apps/server-nestjs/src/modules/argocd/argocd-datastore.service.ts +++ b/apps/server-nestjs/src/modules/argocd/argocd-datastore.service.ts @@ -82,6 +82,26 @@ export const projectSelect = { internalRepoName: true, }, }, + internalValueSources: { + orderBy: { order: 'asc' }, + select: { + order: true, + path: true, + }, + }, + externalValueSource: { + select: { + order: true, + path: true, + ref: true, + targetRevision: true, + repository: { + select: { + internalRepoName: true, + }, + }, + }, + }, }, }, }, diff --git a/apps/server-nestjs/src/modules/argocd/argocd-testing.utils.ts b/apps/server-nestjs/src/modules/argocd/argocd-testing.utils.ts index 65d58f8955..e24dc87c12 100644 --- a/apps/server-nestjs/src/modules/argocd/argocd-testing.utils.ts +++ b/apps/server-nestjs/src/modules/argocd/argocd-testing.utils.ts @@ -11,10 +11,35 @@ export function makeProjectDeploymentSource( targetRevision: 'HEAD', helmValuesFiles: '', repository: makeProjectRepository(), + internalValueSources: [], + externalValueSource: null, ...overrides, } satisfies ProjectWithDetails['deployments'][number]['deploymentSources'][number] } +export function makeProjectInternalValueSource( + overrides: Partial = {}, +): ProjectWithDetails['deployments'][number]['deploymentSources'][number]['internalValueSources'][number] { + return { + order: 0, + path: 'values.yaml', + ...overrides, + } satisfies ProjectWithDetails['deployments'][number]['deploymentSources'][number]['internalValueSources'][number] +} + +export function makeProjectExternalValueSource( + overrides: Partial> = {}, +): NonNullable { + return { + order: 0, + path: 'values.yaml', + ref: '', + targetRevision: '', + repository: { internalRepoName: faker.word.noun() }, + ...overrides, + } satisfies NonNullable +} + export function makeProjectDeployment( overrides: Partial = {}, ): ProjectWithDetails['deployments'][number] { @@ -30,6 +55,8 @@ export function makeProjectDeployment( targetRevision: 'HEAD', helmValuesFiles: '', repository: makeProjectRepository(), + internalValueSources: [], + externalValueSource: null, }, ], ...overrides, diff --git a/apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts b/apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts index cc41c30cce..53727b91af 100644 --- a/apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts +++ b/apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts @@ -3,13 +3,21 @@ import { generateNamespaceName } from '@cpn-console/shared' import { Test } from '@nestjs/testing' import { beforeEach, describe, expect, it } from 'vitest' import { mockDeep } from 'vitest-mock-extended' -import { stringify } from 'yaml' +import { parse, stringify } from 'yaml' import { GitlabClientService } from '../gitlab/gitlab-client.service' import { makeCommitAction, makeProjectSchema, makeRepositoryTreeSchema } from '../gitlab/gitlab-testing.utils' import { ConfigurationService } from '../infrastructure/configuration/configuration.service' import { VaultClientService } from '../vault/vault-client.service' import { ArgoCDDatastoreService } from './argocd-datastore.service' -import { makeProjectDeployment, makeProjectDeploymentSource, makeProjectEnvironment, makeProjectRepository, makeProjectWithDetails } from './argocd-testing.utils' +import { + makeProjectDeployment, + makeProjectDeploymentSource, + makeProjectEnvironment, + makeProjectExternalValueSource, + makeProjectInternalValueSource, + makeProjectRepository, + makeProjectWithDetails, +} from './argocd-testing.utils' import { ArgoCDService } from './argocd.service' describe('argoCDService', () => { @@ -206,6 +214,7 @@ describe('argoCDService', () => { targetRevision: 'HEAD', path: '.', valueFiles: [], + valueSources: [], }, ], }, @@ -281,6 +290,7 @@ describe('argoCDService', () => { targetRevision: 'HEAD', path: '.', valueFiles: [], + valueSources: [], }, ], }, @@ -541,6 +551,7 @@ describe('argoCDService', () => { targetRevision: 'dev', path: '.', valueFiles: [], + valueSources: [], }, { name: 'infra-repo', @@ -549,6 +560,7 @@ describe('argoCDService', () => { targetRevision: '1.0.0', path: 'service-1', valueFiles: [], + valueSources: [], }, ], }, @@ -570,4 +582,77 @@ describe('argoCDService', () => { expect(gitlab.generateCreateOrUpdateAction).toHaveBeenCalledTimes(4) // 2 environments + 2 deployments }) + + it('should generate multi-source valueSources (internal + external) for a deployment', async () => { + const mockDevEnv = makeProjectEnvironment({ + name: 'dev', + cluster: { + id: 'c1', + label: 'cluster-1', + zone: { slug: 'zone-1' }, + }, + }) + const mockAppRepo = makeProjectRepository({ internalRepoName: 'app-repo' }) + const mockValueRepo = makeProjectRepository({ internalRepoName: 'infra-values' }) + const mockProject = makeProjectWithDetails({ + name: 'Project 1', + slug: 'project-1', + environments: [mockDevEnv], + repositories: [mockAppRepo, mockValueRepo], + plugins: [], + deployments: [ + makeProjectDeployment({ + environment: mockDevEnv, + deploymentSources: [ + makeProjectDeploymentSource({ + repository: mockAppRepo, + targetRevision: 'dev', + internalValueSources: [ + makeProjectInternalValueSource({ order: 0, path: 'values.yaml' }), + ], + externalValueSource: makeProjectExternalValueSource({ + order: 1, + ref: 'infra-values', + path: 'envs/dev/values.yaml', + targetRevision: 'main', + repository: { internalRepoName: 'infra-values' }, + }), + }), + ], + }), + ], + }) + + const infraProject = makeProjectSchema({ id: 100, http_url_to_repo: 'https://gitlab.internal/infra' }) + datastore.getAllProjects.mockResolvedValue([mockProject]) + gitlab.getOrCreateInfraGroupRepo.mockResolvedValue(infraProject) + gitlab.getOrCreateProjectGroupPublicUrl.mockResolvedValue('https://gitlab.internal/group') + gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo') + gitlab.listFiles.mockResolvedValue([]) + vault.getAuthApproleRoleRoleId.mockResolvedValue('role-id') + vault.createAuthApproleRoleSecretId.mockResolvedValue('secret-id') + gitlab.generateCreateOrUpdateAction.mockImplementation(async (_repoId, _ref, filePath: string, content: string) => { + return makeCommitAction({ filePath, content }) + }) + + await expect(service.handleCron()).resolves.not.toThrow() + + const actions = gitlab.maybeCreateCommit.mock.calls[0][2] + const parsedValues = actions + .filter((action): action is typeof action & { content: string } => 'content' in action) + .map(action => parse(action.content)) + const values = parsedValues.find(v => v.application?.repositories?.[0]?.valueSources?.length) + expect(values).toBeDefined() + + expect(values.application.repositories[0].valueSources).toStrictEqual([ + { type: 'internal', path: 'values.yaml' }, + { + type: 'external', + ref: 'infra-values', + repoURL: 'https://gitlab.internal/group/project-1/infra-values.git', + targetRevision: 'main', + path: 'envs/dev/values.yaml', + }, + ]) + }) }) diff --git a/apps/server-nestjs/src/modules/argocd/argocd.service.ts b/apps/server-nestjs/src/modules/argocd/argocd.service.ts index 4d98330d72..456d834661 100644 --- a/apps/server-nestjs/src/modules/argocd/argocd.service.ts +++ b/apps/server-nestjs/src/modules/argocd/argocd.service.ts @@ -350,6 +350,10 @@ export class ArgoCDService { } } +type ValueSource + = | { type: 'internal', path: string } + | { type: 'external', ref: string, repoURL: string, targetRevision: string, path: string } + interface ValuesSchema { common: { 'dso/project': string @@ -401,6 +405,7 @@ interface ValuesSchema { targetRevision: string path: string valueFiles: string[] + valueSources: ValueSource[] }[] } features: { @@ -459,10 +464,38 @@ function formatRepositoriesValues( targetRevision: repository.deployRevision || 'HEAD', path: repository.deployPath || '.', valueFiles, + valueSources: [], } satisfies ValuesSchema['application']['repositories'][number] }) } +type DeploymentSource = ProjectWithDetails['deployments'][number]['deploymentSources'][number] + +function formatDeploymentSourceValueSources( + source: DeploymentSource, + gitlabPublicProjectUrl: string, +): ValueSource[] { + const internalValueSources = source.internalValueSources.map(valueSource => ({ + order: valueSource.order, + value: { type: 'internal', path: valueSource.path } satisfies ValueSource, + })) + const externalValueSource = source.externalValueSource + ? [{ + order: source.externalValueSource.order, + value: { + type: 'external', + ref: source.externalValueSource.ref, + repoURL: `${gitlabPublicProjectUrl}/${source.externalValueSource.repository.internalRepoName}.git`, + targetRevision: source.externalValueSource.targetRevision || 'HEAD', + path: source.externalValueSource.path, + } satisfies ValueSource, + }] + : [] + return [...internalValueSources, ...externalValueSource] + .sort((a, b) => a.order - b.order) + .map(entry => entry.value) +} + function formatRepositoriesValuesFromDeployments( deployments: ProjectWithDetails['deployments'][number][], gitlabPublicProjectUrl: string, @@ -472,6 +505,7 @@ function formatRepositoriesValuesFromDeployments( deployment.deploymentSources .map((source) => { const valueFiles = splitExtraRepositories(source.helmValuesFiles?.replaceAll('', envName)) + const valueSources = formatDeploymentSourceValueSources(source, gitlabPublicProjectUrl) return { name: source.repository.internalRepoName, id: source.repository.id, @@ -479,6 +513,7 @@ function formatRepositoriesValuesFromDeployments( targetRevision: source.targetRevision || 'HEAD', path: source.path || '.', valueFiles, + valueSources, } satisfies ValuesSchema['application']['repositories'][number] }), ) diff --git a/apps/server-nestjs/src/modules/project/project-queries.utils.ts b/apps/server-nestjs/src/modules/project/project-queries.utils.ts index 8469ab9cee..1ad28331f4 100644 --- a/apps/server-nestjs/src/modules/project/project-queries.utils.ts +++ b/apps/server-nestjs/src/modules/project/project-queries.utils.ts @@ -147,6 +147,26 @@ export const projectSelect = { internalRepoName: true, }, }, + internalValueSources: { + orderBy: { order: 'asc' }, + select: { + order: true, + path: true, + }, + }, + externalValueSource: { + select: { + order: true, + path: true, + ref: true, + targetRevision: true, + repository: { + select: { + internalRepoName: true, + }, + }, + }, + }, }, }, },