Repository navigation
refactor(cluster): migrate module from server #2495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bb42249
29e1757
8099274
381e4df
2287cf5
263f912
047b15b
807cb65
ff92ce1
cae9d48
18d7d05
eea46ba
f37e5de
662c7ef
b4c16d4
265afea
3fda1b4
c0ed827
0b4cb4d
bdce543
fc9f44f
60e743c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| import type { CommitAction, CondensedProjectSchema, ProjectSchema, SimpleProjectSchema } from '@gitbeaker/core' | ||
| import type { ConfigType } from '@nestjs/config' | ||
| import type { ClusterEventPayload } from '../events/app-events.service' | ||
| import type { RequiredPluginResult } from '../plugin/plugin.utils' | ||
| import type { ProjectWithDetails } from './argocd-datastore.service' | ||
| import { createHmac } from 'node:crypto' | ||
| import { generateNamespaceName, inClusterLabel } from '@cpn-console/shared' | ||
| import { generateNamespaceName, inClusterLabel, KubeconfigSchema } from '@cpn-console/shared' | ||
| import { Inject, Injectable, Logger } from '@nestjs/common' | ||
| import { OnEvent } from '@nestjs/event-emitter' | ||
| import { trace } from '@opentelemetry/api' | ||
|
|
@@ -27,6 +28,7 @@ import { | |
| PROJECT_READONLY_GROUP_PATH_SUFFIX, | ||
| PROJECT_SECURITY_GROUP_PATH_SUFFIX, | ||
| } from './argocd.constants' | ||
| import { generateClusterSecretData, generateZoneVaultValues } from './argocd.utils' | ||
|
|
||
| @Injectable() | ||
| export class ArgoCDService { | ||
|
|
@@ -68,6 +70,83 @@ export class ArgoCDService { | |
| return capturePluginResult('argocd', () => this.cleanupProject(project)) | ||
| } | ||
|
|
||
| @OnEvent('cluster.upsert') | ||
| async handleClusterUpsert(payload: ClusterEventPayload): Promise<RequiredPluginResult<'argocd'>> { | ||
| return capturePluginResult('argocd', () => this.syncCluster(payload)) | ||
| } | ||
|
|
||
| @StartActiveSpan() | ||
| private async syncCluster(payload: ClusterEventPayload) { | ||
| const cluster = await this.datastore.getCluster(payload.clusterId) | ||
| if (!cluster) throw new Error(`Cluster not found for event (clusterId=${payload.clusterId})`) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constat retiré après relecture : ces handlers renvoient déjà
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Vérifié : |
||
| const span = trace.getActiveSpan() | ||
| span?.setAttribute('cluster.label', cluster.label) | ||
| span?.setAttribute('zone.slug', cluster.zone.slug) | ||
| this.logger.log(`Handling a cluster upsert event for ${cluster.label}`) | ||
| const kubeconfig = KubeconfigSchema.parse(cluster.kubeconfig) | ||
| await this.vault.upsertKvData( | ||
| `zone-${cluster.zone.slug}`, | ||
| `clusters/cluster-${cluster.label}/argocd-cluster-secret`, | ||
| { data: generateClusterSecretData(cluster, kubeconfig) }, | ||
| ) | ||
| await this.commitZoneValues(cluster.zone.slug) | ||
| if (payload.zoneId && payload.zoneId !== cluster.zone.id) { | ||
| const previousZoneSlug = await this.datastore.getZoneSlug(payload.zoneId) | ||
| if (previousZoneSlug) await this.commitZoneValues(previousZoneSlug) | ||
| } | ||
| this.logger.log(`ArgoCD cluster sync completed for ${cluster.label}`) | ||
| } | ||
|
|
||
| @OnEvent('cluster.delete') | ||
| async handleClusterDelete(payload: ClusterEventPayload): Promise<RequiredPluginResult<'argocd'>> { | ||
| return capturePluginResult('argocd', () => this.cleanupCluster(payload)) | ||
| } | ||
|
|
||
| @StartActiveSpan() | ||
| private async cleanupCluster(payload: ClusterEventPayload) { | ||
| const cluster = await this.datastore.getCluster(payload.clusterId) | ||
| if (!cluster) throw new Error(`Cluster not found for event (clusterId=${payload.clusterId})`) | ||
| const span = trace.getActiveSpan() | ||
| span?.setAttribute('cluster.label', cluster.label) | ||
| span?.setAttribute('zone.slug', cluster.zone.slug) | ||
| this.logger.log(`Handling a cluster delete event for ${cluster.label}`) | ||
| await this.vault.deleteKvMetadata( | ||
| `zone-${cluster.zone.slug}`, | ||
| `clusters/cluster-${cluster.label}/argocd-cluster-secret`, | ||
| ) | ||
| await this.commitZoneValues(cluster.zone.slug) | ||
| this.logger.log(`ArgoCD cluster cleanup completed for ${cluster.label}`) | ||
| } | ||
|
|
||
| private async commitZoneValues(zoneSlug: string) { | ||
| const infraProject = await this.gitlab.getOrCreateInfraGroupRepo(zoneSlug) | ||
| const clusters = await this.datastore.getZoneClusterNames(zoneSlug) | ||
| const vaultValues = await this.generateZoneVaultValues(zoneSlug) | ||
| const action = await this.gitlab.generateCreateOrUpdateAction( | ||
| infraProject, | ||
| 'main', | ||
| 'argocd-values.yaml', | ||
| stringify({ vault: vaultValues, clusters }), | ||
| ) | ||
| if (!action) { | ||
| this.logger.verbose(`Zone argocd-values.yaml is up to date (zone=${zoneSlug})`) | ||
| return | ||
| } | ||
| await this.gitlab.maybeCreateCommit(infraProject, `ci: :robot_face: Update zone ${zoneSlug}`, [action]) | ||
| } | ||
|
|
||
| private async generateZoneVaultValues(zoneSlug: string) { | ||
| const roleId = await this.vault.getAuthApproleRoleRoleId(`zone-${zoneSlug}`).catch(() => { | ||
| this.logger.warn(`Couldn't find zone app role (zone=${zoneSlug})`) | ||
| return undefined | ||
| }) | ||
| const secretId = await this.vault.ensureAuthApproleRoleSecretId(`zone-${zoneSlug}`).catch(() => { | ||
| this.logger.warn(`Couldn't generate zone app role secret (zone=${zoneSlug})`) | ||
| return undefined | ||
| }) | ||
| return generateZoneVaultValues(this.vaultConfig.url, zoneSlug, roleId, secretId) | ||
| } | ||
|
|
||
| @StartActiveSpan() | ||
| private async cleanupProject(project: ProjectWithDetails) { | ||
| const span = trace.getActiveSpan() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import type { Kubeconfig } from '@cpn-console/shared' | ||
| import { stringify } from 'yaml' | ||
|
|
||
| export function generateClusterTlsClientConfig(kubeconfig: Kubeconfig) { | ||
| return { | ||
| ...kubeconfig.user.username && { username: kubeconfig.user.username }, | ||
| ...kubeconfig.user.password && { password: kubeconfig.user.password }, | ||
| ...kubeconfig.user.token && { bearerToken: kubeconfig.user.token }, | ||
| tlsClientConfig: { | ||
| ...kubeconfig.user.keyData && { keyData: kubeconfig.user.keyData }, | ||
| ...kubeconfig.user.certData && { certData: kubeconfig.user.certData }, | ||
| ...kubeconfig.cluster.caData && !kubeconfig.cluster.skipTLSVerify && { caData: kubeconfig.cluster.caData }, | ||
| ...kubeconfig.cluster.skipTLSVerify && { insecure: kubeconfig.cluster.skipTLSVerify }, | ||
| serverName: kubeconfig.cluster.tlsServerName, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| export function generateZoneVaultValues(vaultUrl: string, zoneSlug: string, roleId: string | undefined, secretId: string | undefined) { | ||
| return { | ||
| url: vaultUrl, | ||
| kvName: `zone-${zoneSlug}`, | ||
| roleId: roleId ?? 'none', | ||
| secretId: secretId ?? 'none', | ||
| } | ||
| } | ||
|
|
||
| export function generateClusterSecretData(cluster: { label: string, clusterResources: boolean }, kubeconfig: Kubeconfig) { | ||
| return { | ||
| name: cluster.label, | ||
| clusterResources: String(cluster.clusterResources), | ||
| server: kubeconfig.cluster.server, | ||
| config: stringify(generateClusterTlsClientConfig(kubeconfig)), | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
important — Le corps de la PR (section « État lors de la bascule ») affirme encore que « server-nestjs n'a aucun @onevent('cluster.upsert') ni @onevent('cluster.delete') » et que l'émission sera un no-op à la bascule. C'est faux sur ce head : les listeners existent dans
argocd.service.ts(lignes 73 et 100). Mettre à jour le corps — c'est la note qui décide si #2756 peut fusionner sans risque de no-op. Profiter pour corriger la route listéeGET /:clusterId/usage→ le contrat est/usage/:clusterId.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Le corps de la PR a été corrigé entre-temps : la section « État lors de la bascule » documente désormais les listeners
@OnEvent('cluster.upsert'/'cluster.delete')d'argocd.service.tset conclut à l'absence de no-op. Marqué à jour.