Skip to content

Commit ab5ccc8

Browse files
committed
refactor: 🚧 use a class to mutualize gitlab api calls
1 parent 1f030a6 commit ab5ccc8

File tree

5 files changed

+194
-276
lines changed

5 files changed

+194
-276
lines changed

src/function.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import type { Environment, PluginResult, Project, StepCall, UserObject } from '@cpn-console/hooks'
22
import type { KeycloakProjectApi } from '@cpn-console/keycloak-plugin/types/class.js'
3-
import type { Gitlab as GitlabInterface } from '@gitbeaker/core'
43
import { parseError, specificallyDisabled } from '@cpn-console/hooks'
5-
import { compressUUID, removeTrailingSlash, requiredEnv } from '@cpn-console/shared'
6-
import { Gitlab } from '@gitbeaker/rest'
4+
import { compressUUID } from '@cpn-console/shared'
75
import { deleteKeycloakGroup, ensureKeycloakGroups } from './keycloak.js'
8-
import { isNewNsName } from './utils.js'
9-
import { deleteProjectConfig, type EnvType, type ObservabilityProject, upsertGitlabConfig } from './yaml.js'
6+
import { type EnvType, type ObservabilityProject, ObservabilityRepoManager } from './observability-repo-manager.js'
107

118
const okSkipped: PluginResult = {
129
status: {
@@ -17,6 +14,11 @@ const okSkipped: PluginResult = {
1714

1815
export type ListPerms = Record<'prod' | 'hors-prod', Record<'view' | 'edit', UserObject['id'][]>>
1916

17+
const re = /[a-z0-9]{25}--[a-z0-9]{25}/
18+
function isNewNsName(ns: string) {
19+
return re.test(ns)
20+
}
21+
2022
function getListPerms(environments: Environment[]): ListPerms {
2123
const allProdPerms = environments
2224
.filter(env => env.stage === 'prod')
@@ -56,12 +58,6 @@ function getListPerms(environments: Environment[]): ListPerms {
5658
return listPerms
5759
}
5860

59-
function getGitlabApi(): GitlabInterface {
60-
const gitlabUrl = removeTrailingSlash(requiredEnv('GITLAB_URL'))
61-
const gitlabToken = requiredEnv('GITLAB_TOKEN')
62-
return new Gitlab({ token: gitlabToken, host: gitlabUrl })
63-
}
64-
6561
export const upsertProject: StepCall<Project> = async (payload) => {
6662
try {
6763
if (specificallyDisabled(payload.config.observability?.enabled)) {
@@ -70,8 +66,7 @@ export const upsertProject: StepCall<Project> = async (payload) => {
7066
// init args
7167
const project = payload.args
7268
const keycloakApi = payload.apis.keycloak as KeycloakProjectApi
73-
// init gitlab api
74-
const gitlabApi = getGitlabApi()
69+
7570
const keycloakRootGroupPath = await keycloakApi.getProjectGroupPath()
7671
const tenantRbacProd = [`${keycloakRootGroupPath}/grafana/prod-RW`, `${keycloakRootGroupPath}/grafana/prod-RO`]
7772
const tenantRbacHProd = [`${keycloakRootGroupPath}/grafana/hprod-RW`, `${keycloakRootGroupPath}/grafana/hprod-RO`]
@@ -115,7 +110,9 @@ export const upsertProject: StepCall<Project> = async (payload) => {
115110
const listPerms = getListPerms(project.environments)
116111

117112
// Upsert or delete Gitlab config based on prod/non-prod environment
118-
const yamlResult = await upsertGitlabConfig(project, gitlabApi, projectValue)
113+
const observabilityRepoManager = new ObservabilityRepoManager()
114+
const yamlResult = await observabilityRepoManager.updateProjectConfig(project, projectValue)
115+
119116
await ensureKeycloakGroups(listPerms, keycloakApi)
120117

121118
return {
@@ -144,12 +141,12 @@ export const deleteProject: StepCall<Project> = async (payload) => {
144141
return okSkipped
145142
}
146143
const project = payload.args
147-
const gitlabApi = getGitlabApi()
148144
const keycloakApi = payload.apis.keycloak as KeycloakProjectApi
145+
const observabilityRepoManager = new ObservabilityRepoManager()
149146

150147
await Promise.all([
151148
deleteKeycloakGroup(keycloakApi),
152-
deleteProjectConfig(project, gitlabApi),
149+
observabilityRepoManager.deleteProjectConfig(project),
153150
])
154151

155152
return {

src/gitlab.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.

src/observability-repo-manager.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import type { Project } from '@cpn-console/hooks'
2+
import { removeTrailingSlash, requiredEnv } from '@cpn-console/shared'
3+
import { Gitlab, type ProjectSchema } from '@gitbeaker/core'
4+
import yaml from 'js-yaml'
5+
6+
const valuesPath = 'helm/values.yaml'
7+
const valuesBranch = 'main'
8+
const groupName = 'observability'
9+
const repoName = 'observability'
10+
11+
export type EnvType = 'prod' | 'hprod'
12+
interface Tenant {}
13+
14+
interface Env {
15+
groups?: string[]
16+
tenants: {
17+
[x: `${EnvType}-${string}`]: Tenant
18+
}
19+
}
20+
export interface ObservabilityProject {
21+
projectName: string // slug
22+
envs: {
23+
prod: Env
24+
hprod: Env
25+
}
26+
}
27+
28+
interface ObservabilityData {
29+
global: {
30+
projects: {
31+
[x: string]: ObservabilityProject
32+
}
33+
}
34+
}
35+
36+
const yamlInitData = `
37+
global:
38+
tenants: []
39+
`
40+
41+
export class ObservabilityRepoManager {
42+
private gitlabApi: Gitlab
43+
44+
constructor() {
45+
const gitlabUrl = removeTrailingSlash(requiredEnv('GITLAB_URL'))
46+
const gitlabToken = requiredEnv('GITLAB_TOKEN')
47+
this.gitlabApi = new Gitlab({ token: gitlabToken, host: gitlabUrl })
48+
}
49+
50+
private async findOrCreateRepo(): Promise<ProjectSchema> {
51+
try {
52+
// Find or create parent Gitlab group
53+
const groups = await this.gitlabApi.Groups.search(groupName)
54+
let group = groups.find(g => g.full_path === groupName || g.name === groupName)
55+
if(!group) {
56+
group = await this.gitlabApi.Groups.create(groupName, groupName)
57+
}
58+
// Find or create parent Gitlab repository
59+
const projects: ProjectSchema[] = await this.gitlabApi.Groups.allProjects(group.id)
60+
const repo = projects.find(p => p.name === repoName)
61+
if (!repo) {
62+
return this.gitlabApi.Projects.create({
63+
name: repoName,
64+
path: repoName,
65+
namespaceId: group.id,
66+
description: 'Repo for Observatorium values, managed by DSO console',
67+
})
68+
}
69+
return repo
70+
} catch (error) {
71+
throw new Error(`Unexpected error: ${error}`)
72+
}
73+
}
74+
75+
// Fonction pour récupérer le fichier values.yaml
76+
private async getValuesFile(project: ProjectSchema): Promise<ObservabilityData | null> {
77+
try {
78+
// Essayer de récupérer le fichier
79+
const file = await this.gitlabApi.RepositoryFiles.show(project.id, valuesPath, valuesBranch)
80+
return yaml.load(Buffer.from(file.content, 'base64').toString('utf-8')) as ObservabilityData
81+
} catch (error) {
82+
console.log(error)
83+
return null
84+
}
85+
}
86+
87+
private writeYamlFile(data: object): string {
88+
try {
89+
return yaml.dump(data, {
90+
styles: {
91+
'!!seq': 'flow',
92+
},
93+
sortKeys: false,
94+
lineWidth: -1, // Pour éviter le retour à la ligne automatique
95+
})
96+
} catch (e) {
97+
console.error(e)
98+
return ''
99+
}
100+
}
101+
102+
// Fonction pour éditer, committer et pousser un fichier YAML
103+
public async commitAndPushYamlFile(project: ProjectSchema, filePath: string, branch: string, commitMessage: string, yamlString: string): Promise<void> {
104+
const encodedContent = Buffer.from(yamlString).toString('utf-8')
105+
try {
106+
// Vérifier si le fichier existe déjà
107+
await this.gitlabApi.RepositoryFiles.show(project.id, filePath, branch)
108+
// Si le fichier existe, mise à jour
109+
await this.gitlabApi.RepositoryFiles.edit(project.id, filePath, branch, encodedContent, commitMessage)
110+
console.log(`Fichier YAML commité et poussé: ${filePath}`)
111+
} catch (error: any) {
112+
console.log('Le fichier n\'existe pas')
113+
// Si le fichier n'existe pas, création
114+
console.log(`error : ${JSON.stringify(error)}`)
115+
console.log(error)
116+
await this.gitlabApi.RepositoryFiles.create(project.id, filePath, branch, encodedContent, commitMessage)
117+
console.log(`Fichier YAML créé et poussé: ${filePath}`)
118+
}
119+
}
120+
121+
public async updateProjectConfig(project: Project, projectValue: ObservabilityProject): Promise<string> {
122+
// Déplacer toute la logique de création ou de récupération de groupe et de repo ici
123+
const gitlabRepo = await this.findOrCreateRepo()
124+
125+
// Récupérer le fichier values.yaml
126+
const yamlFile = await this.getValuesFile(gitlabRepo)
127+
|| yaml.load(Buffer.from(yamlInitData, 'base64').toString('utf-8')) as ObservabilityData
128+
129+
const projects = yamlFile.global?.projects || {}
130+
131+
if (JSON.stringify(projects[project.id]) === JSON.stringify(projectValue)) {
132+
return 'Already up-to-date'
133+
}
134+
135+
projects[project.id] = projectValue
136+
137+
const yamlString = this.writeYamlFile({
138+
...yamlFile,
139+
global: {
140+
...yamlFile.global,
141+
projects,
142+
},
143+
})
144+
145+
await this.commitAndPushYamlFile(
146+
gitlabRepo,
147+
valuesPath,
148+
valuesBranch,
149+
`Update project ${project.slug}`,
150+
yamlString,
151+
)
152+
return `Update: ${project.slug}`
153+
}
154+
155+
public async deleteProjectConfig(project: Project) {
156+
// Même logique de groupe et de repo que pour l'upsert
157+
const gitlabRepo = await this.findOrCreateRepo()
158+
159+
// Récupérer le fichier values.yaml
160+
const yamlFile = await this.getValuesFile(gitlabRepo)
161+
162+
// Rechercher le projet à supprimer
163+
if (!yamlFile || (yamlFile.global?.projects && !(project.id in yamlFile.global.projects))) {
164+
return
165+
}
166+
167+
// Modifier le fichier YAML et commiter
168+
const yamlFileStripped = structuredClone(yamlFile)
169+
delete yamlFileStripped.global?.projects?.[project.id]
170+
171+
const yamlString = this.writeYamlFile(yamlFileStripped)
172+
173+
return this.commitAndPushYamlFile(
174+
gitlabRepo,
175+
valuesPath,
176+
valuesBranch,
177+
`Delete project ${project.name}`,
178+
yamlString,
179+
)
180+
}
181+
}

src/utils.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,3 @@ export function getConfig(): Required<typeof config> {
2626
// @ts-ignore
2727
return config
2828
}
29-
30-
export type Stage = 'prod' | 'hprod'
31-
32-
export interface TenantInfo {
33-
groups: string[]
34-
type: 'prod' | 'hprod'
35-
name: string // tenant name, short-uuid or slug
36-
}
37-
export interface TenantKeycloakMapper {
38-
[x: string]: TenantInfo // fullName, type + (short-uuid or slug)
39-
}
40-
41-
const re = /[a-z0-9]{25}--[a-z0-9]{25}/
42-
export function isNewNsName(ns: string) {
43-
return re.test(ns)
44-
}

0 commit comments

Comments
 (0)