Skip to content

Commit 862be71

Browse files
committed
feat: ✨ manage project observability repository
1 parent ab5ccc8 commit 862be71

File tree

5 files changed

+88
-16
lines changed

5 files changed

+88
-16
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ Ceci est un plugin additionnel pour la console [Cloud-Pi-Native](https://github.
44
Il permet :
55
- d'alimenter un fichier de values qui pilote la création d'instance Grafana et de ses datasources (prometheus, alert-manager, loki)
66
- de propager les permissions des utilisateurs via Keycloak
7+
8+
## Usage
9+
10+
Les variables d'environnement suivantes doivent être définies :
11+
- `GRAFANA_URL` : l'URL racine à utiliser pour accéder aux instances déployées pour chaque projet
12+
- `DSO_OBSERVABILITY_CHART_VERSION` : la version du Chart dso-observability à utiliser dans les dépôts de dashboards et alertes projet.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@cpn-console/observability-plugin",
33
"type": "module",
4-
"version": "1.1.0",
4+
"version": "1.1.1",
55
"description": "Observability plugin for DSO console",
66
"exports": {
77
".": {

src/function.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import type { GitlabProjectApi } from '@cpn-console/gitlab-plugin/types/class.js'
12
import type { Environment, PluginResult, Project, StepCall, UserObject } from '@cpn-console/hooks'
23
import type { KeycloakProjectApi } from '@cpn-console/keycloak-plugin/types/class.js'
3-
import { parseError, specificallyDisabled } from '@cpn-console/hooks'
4+
import { okStatus, parseError, specificallyDisabled } from '@cpn-console/hooks'
45
import { compressUUID } from '@cpn-console/shared'
56
import { deleteKeycloakGroup, ensureKeycloakGroups } from './keycloak.js'
6-
import { type EnvType, type ObservabilityProject, ObservabilityRepoManager } from './observability-repo-manager.js'
7+
import { type EnvType, type ObservabilityProject, ObservabilityRepoManager, observabilityRepository } from './observability-repo-manager.js'
78

89
const okSkipped: PluginResult = {
910
status: {
@@ -58,14 +59,32 @@ function getListPerms(environments: Environment[]): ListPerms {
5859
return listPerms
5960
}
6061

62+
// Create and update (if needed) the project repository for custom dashboards and alerts
63+
export const ensureProjectRepository: StepCall<Project> = async (payload) => {
64+
const gitlabProjectApi = payload.apis.gitlab as GitlabProjectApi
65+
try {
66+
await gitlabProjectApi.getProjectId(observabilityRepository)
67+
} catch (e) {
68+
console.log('Repository not fond', e)
69+
await gitlabProjectApi.createEmptyProjectRepository({
70+
repoName: observabilityRepository,
71+
description: 'Respository for custom Observability infrastructure resources',
72+
clone: false,
73+
})
74+
}
75+
// Reference to avoid deletion
76+
gitlabProjectApi.addSpecialRepositories(observabilityRepository)
77+
return okStatus
78+
}
79+
6180
export const upsertProject: StepCall<Project> = async (payload) => {
6281
try {
6382
if (specificallyDisabled(payload.config.observability?.enabled)) {
6483
return okSkipped
6584
}
66-
// init args
6785
const project = payload.args
6886
const keycloakApi = payload.apis.keycloak as KeycloakProjectApi
87+
const gitlabApi = payload.apis.gitlab as GitlabProjectApi
6988

7089
const keycloakRootGroupPath = await keycloakApi.getProjectGroupPath()
7190
const tenantRbacProd = [`${keycloakRootGroupPath}/grafana/prod-RW`, `${keycloakRootGroupPath}/grafana/prod-RO`]
@@ -75,6 +94,10 @@ export const upsertProject: StepCall<Project> = async (payload) => {
7594

7695
const projectValue: ObservabilityProject = {
7796
projectName: project.slug,
97+
projectRepository: {
98+
url: await gitlabApi.getRepoUrl(observabilityRepository),
99+
path: '.',
100+
},
78101
envs: {
79102
hprod: {
80103
groups: tenantRbacHProd,
@@ -110,7 +133,7 @@ export const upsertProject: StepCall<Project> = async (payload) => {
110133
const listPerms = getListPerms(project.environments)
111134

112135
// Upsert or delete Gitlab config based on prod/non-prod environment
113-
const observabilityRepoManager = new ObservabilityRepoManager()
136+
const observabilityRepoManager = new ObservabilityRepoManager(gitlabApi)
114137
const yamlResult = await observabilityRepoManager.updateProjectConfig(project, projectValue)
115138

116139
await ensureKeycloakGroups(listPerms, keycloakApi)
@@ -142,7 +165,7 @@ export const deleteProject: StepCall<Project> = async (payload) => {
142165
}
143166
const project = payload.args
144167
const keycloakApi = payload.apis.keycloak as KeycloakProjectApi
145-
const observabilityRepoManager = new ObservabilityRepoManager()
168+
const observabilityRepoManager = new ObservabilityRepoManager(payload.apis.gitlab)
146169

147170
await Promise.all([
148171
deleteKeycloakGroup(keycloakApi),

src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import type { DeclareModuleGenerator, Plugin } from '@cpn-console/hooks'
22
import { requiredEnv } from '@cpn-console/shared'
3-
import { deleteProject, upsertProject } from './function.js'
3+
import { deleteProject, ensureProjectRepository, upsertProject } from './function.js'
44
import infos from './infos.js'
55

66
export const plugin: Plugin = {
77
infos,
88
subscribedHooks: {
99
upsertProject: {
1010
steps: {
11-
post: upsertProject,
11+
pre: ensureProjectRepository,
12+
main: upsertProject,
1213
},
1314
},
1415
deleteProject: {
@@ -17,7 +18,7 @@ export const plugin: Plugin = {
1718
},
1819
},
1920
},
20-
start: () => { requiredEnv('GRAFANA_URL') }, // to check is the variable is set, unless it crashes the app
21+
start: () => { requiredEnv('GRAFANA_URL') && requiredEnv('DSO_OBSERVABILITY_CHART_VERSION') }, // to check is the variable is set, unless it crashes the app
2122
}
2223

2324
declare module '@cpn-console/hooks' {

src/observability-repo-manager.ts

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,31 @@
1+
import type { GitlabProjectApi } from '@cpn-console/gitlab-plugin/types/class.js'
12
import type { Project } from '@cpn-console/hooks'
3+
import type { Gitlab as IGitlab, ProjectSchema } from '@gitbeaker/core'
24
import { removeTrailingSlash, requiredEnv } from '@cpn-console/shared'
3-
import { Gitlab, type ProjectSchema } from '@gitbeaker/core'
5+
import { Gitlab } from '@gitbeaker/rest'
46
import yaml from 'js-yaml'
57

68
const valuesPath = 'helm/values.yaml'
79
const valuesBranch = 'main'
810
const groupName = 'observability'
911
const repoName = 'observability'
12+
export const observabilityRepository = 'infra-observability'
13+
const observabilityChartVersion = requiredEnv('DSO_OBSERVABILITY_CHART_VERSION')
14+
const observabilityChartContent = `
15+
apiVersion: v2
16+
name: dso-observability
17+
type: application
18+
version: 0.1.0
19+
appVersion: "0.0.1"
20+
dependencies:
21+
- name: charts/dso-observability
22+
version: ${observabilityChartVersion}
23+
repository: https://cloud-pi-native.github.io/helm-charts/
24+
`
25+
const observabilityTemplateContent = `
26+
{{- include "grafana-dashboards.dashboards" . -}}
27+
{{- include "grafana-dashboards.rules" . -}}
28+
`
1029

1130
export type EnvType = 'prod' | 'hprod'
1231
interface Tenant {}
@@ -19,6 +38,10 @@ interface Env {
1938
}
2039
export interface ObservabilityProject {
2140
projectName: string // slug
41+
projectRepository: {
42+
url: string
43+
path: string
44+
}
2245
envs: {
2346
prod: Env
2447
hprod: Env
@@ -39,20 +62,22 @@ const yamlInitData = `
3962
`
4063

4164
export class ObservabilityRepoManager {
42-
private gitlabApi: Gitlab
65+
private gitlabApi: IGitlab
66+
private gitlabProjectApi: GitlabProjectApi
4367

44-
constructor() {
68+
constructor(gitlabProjectApi: GitlabProjectApi) {
4569
const gitlabUrl = removeTrailingSlash(requiredEnv('GITLAB_URL'))
4670
const gitlabToken = requiredEnv('GITLAB_TOKEN')
4771
this.gitlabApi = new Gitlab({ token: gitlabToken, host: gitlabUrl })
72+
this.gitlabProjectApi = gitlabProjectApi
4873
}
4974

5075
private async findOrCreateRepo(): Promise<ProjectSchema> {
5176
try {
5277
// Find or create parent Gitlab group
5378
const groups = await this.gitlabApi.Groups.search(groupName)
5479
let group = groups.find(g => g.full_path === groupName || g.name === groupName)
55-
if(!group) {
80+
if (!group) {
5681
group = await this.gitlabApi.Groups.create(groupName, groupName)
5782
}
5883
// Find or create parent Gitlab repository
@@ -119,16 +144,33 @@ export class ObservabilityRepoManager {
119144
}
120145

121146
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
147+
// Repository created during 'pre' step if needed
148+
const projectId = await this.gitlabProjectApi.getProjectId(observabilityRepository)
149+
const observabilityProjectRepository = await this.gitlabProjectApi.getProjectById(projectId)
150+
151+
// Add or update chart files
152+
const chartUpdated = await this.gitlabProjectApi.commitCreateOrUpdate(
153+
observabilityProjectRepository.id,
154+
observabilityChartContent,
155+
'Chart.yaml',
156+
)
157+
const templateUpdated = await this.gitlabProjectApi.commitCreateOrUpdate(
158+
observabilityProjectRepository.id,
159+
observabilityTemplateContent,
160+
'templates/includes.yaml',
161+
)
162+
163+
// Dépôt d'infra scruté par ArgoCD (charts dso-grafana et dso-observatorium)
123164
const gitlabRepo = await this.findOrCreateRepo()
124165

125166
// Récupérer le fichier values.yaml
126-
const yamlFile = await this.getValuesFile(gitlabRepo)
167+
const yamlFile = await this.getValuesFile(gitlabRepo)
127168
|| yaml.load(Buffer.from(yamlInitData, 'base64').toString('utf-8')) as ObservabilityData
128169

129170
const projects = yamlFile.global?.projects || {}
130171

131-
if (JSON.stringify(projects[project.id]) === JSON.stringify(projectValue)) {
172+
if (!chartUpdated && !templateUpdated
173+
&& JSON.stringify(projects[project.id]) === JSON.stringify(projectValue)) {
132174
return 'Already up-to-date'
133175
}
134176

0 commit comments

Comments
 (0)