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 d3103a2e3..efbf280d0 100644 --- a/apps/server-nestjs/src/modules/argocd/argocd-datastore.service.ts +++ b/apps/server-nestjs/src/modules/argocd/argocd-datastore.service.ts @@ -101,4 +101,11 @@ export class ArgoCDDatastoreService { select: projectSelect, }) } + + async getAllZoneSlugs(): Promise { + const zones = await this.prisma.zone.findMany({ + select: { slug: true }, + }) + return zones.map(zone => zone.slug) + } } 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 95266ad47..cc41c30cc 100644 --- a/apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts +++ b/apps/server-nestjs/src/modules/argocd/argocd.service.spec.ts @@ -34,6 +34,8 @@ describe('argoCDService', () => { deployVaultConnectionInNamespaces: false, }) + datastore.getAllZoneSlugs.mockResolvedValue(['zone-1']) + const module = await Test.createTestingModule({ providers: [ ArgoCDService, @@ -352,6 +354,53 @@ describe('argoCDService', () => { expect(gitlab.generateCreateOrUpdateAction).toHaveBeenCalledTimes(1) }) + it('should delete leftover values files when the project has no environments', async () => { + const mockProject = makeProjectWithDetails({ + slug: 'project-1', + name: 'Project 1', + environments: [], + repositories: [makeProjectRepository({ internalRepoName: 'infra-repo', isInfra: true })], + deployments: [], + }) + + const infraProject = makeProjectSchema({ id: 100, http_url_to_repo: 'https://gitlab.internal/infra' }) + datastore.getAllProjects.mockResolvedValue([mockProject]) + // The project no longer has any environment, so the zone must be discovered + // from the platform zone list rather than from the project's environments. + datastore.getAllZoneSlugs.mockResolvedValue(['zone-1']) + gitlab.getOrCreateInfraGroupRepo.mockResolvedValue(infraProject) + gitlab.getOrCreateProjectGroupPublicUrl.mockResolvedValue('https://gitlab.internal/group') + gitlab.getOrCreateInfraGroupRepoPublicUrl.mockResolvedValue('https://gitlab.internal/infra-repo') + gitlab.listFiles.mockResolvedValue([ + makeRepositoryTreeSchema( + { name: 'values.yaml', path: 'Project 1/cluster-1/dev/values.yaml' }, + ), + makeRepositoryTreeSchema( + { name: 'values.yaml', path: 'Project 1/cluster-1/prod/values.yaml' }, + ), + ]) + + await expect(service.handleCron()).resolves.not.toThrow() + + expect(gitlab.maybeCreateCommit).toHaveBeenCalledTimes(1) + expect(gitlab.maybeCreateCommit).toHaveBeenCalledWith( + infraProject, + 'ci: :robot_face: Sync project-1', + expect.arrayContaining([ + { + action: 'delete', + filePath: 'Project 1/cluster-1/dev/values.yaml', + }, + { + action: 'delete', + filePath: 'Project 1/cluster-1/prod/values.yaml', + }, + ]), + ) + + expect(gitlab.generateCreateOrUpdateAction).not.toHaveBeenCalled() + }) + it('should not commit when there is no diff', async () => { const mockProject = makeProjectWithDetails({ slug: 'project-1', diff --git a/apps/server-nestjs/src/modules/argocd/argocd.service.ts b/apps/server-nestjs/src/modules/argocd/argocd.service.ts index 7cd254fda..4d98330d7 100644 --- a/apps/server-nestjs/src/modules/argocd/argocd.service.ts +++ b/apps/server-nestjs/src/modules/argocd/argocd.service.ts @@ -101,7 +101,9 @@ export class ArgoCDService { ): Promise { const span = trace.getActiveSpan() span?.setAttribute('project.slug', project.slug) - const zones = getDistinctZones(project) + // Visit every zone, not only those with a current environment, so leftover + // values files are purged from zones the project no longer deploys to. + const zones = await this.argoCDDatastore.getAllZoneSlugs() span?.setAttribute('argocd.zones.count', zones.length) this.logger.verbose(`Reconciling ArgoCD zones for project ${project.slug} (count=${zones.length})`) await Promise.all(zones.map(zoneSlug => this.ensureZone(project, zoneSlug))) @@ -167,11 +169,6 @@ export class ArgoCDService { zoneSlug: string, ): Promise { const neededFiles = new Set() - const clusterLabelsInZone = new Set( - project.environments - .filter(e => e.cluster.zone.slug === zoneSlug) - .map(e => e.cluster.label), - ) project.environments.forEach((env) => { if (env.cluster?.zone.slug !== zoneSlug) return @@ -189,10 +186,6 @@ export class ArgoCDService { if (existingFile.name !== 'values.yaml') return false if (!existingFile.path.startsWith(projectPrefix)) return false - const remaining = existingFile.path.slice(projectPrefix.length) - const clusterLabel = remaining.split('/')[0] - if (!clusterLabel || !clusterLabelsInZone.has(clusterLabel)) return false - return !neededFiles.has(existingFile.path) }) .map(existingFile => ({ action: 'delete', filePath: existingFile.path } satisfies CommitAction)) @@ -445,12 +438,6 @@ function formatEnvironmentValuesFilePath(project: { name: string }, cluster: { l return `${project.name}/${cluster.label}/${env.name}/values.yaml` } -function getDistinctZones(project: ProjectWithDetails) { - const zones = new Set() - project.environments.forEach(e => zones.add(e.cluster.zone.slug)) - return [...zones] -} - function splitExtraRepositories(extraRepositories: string | undefined): string[] { if (!extraRepositories) return [] return extraRepositories.split(',').map(r => r.trim()).filter(r => r.length > 0)