Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
run: rm -rf ./node_modules && npm install --ignore-scripts --omit=dev

- name: Zip build folder
run: zip -r ./package.zip ./package.json ./dist ./node_modules
run: zip -r ./package.zip ./package.json ./dist ./node_modules ./files

- name: Upload release artifact
uses: actions/upload-release-asset@v1
Expand Down
Binary file added files/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
346 changes: 154 additions & 192 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cpn-console/observability-plugin",
"type": "module",
"version": "0.1.2",
"version": "0.1.2-patch-1",
"description": "Loki plugin for DSO console",
"exports": {
".": {
Expand All @@ -26,7 +26,7 @@
"prepare": "husky"
},
"dependencies": {
"@cpn-console/hooks": "^2.5.0",
"@cpn-console/hooks": "^4.0.0",
"@gitbeaker/core": "~40.6.0",
"@gitbeaker/requester-utils": "~40.6.0",
"@gitbeaker/rest": "~40.6.0",
Expand All @@ -39,12 +39,12 @@
"devDependencies": {
"@antfu/eslint-config": "^3.16.0",
"@cpn-console/eslint-config": "^1.0.2",
"@cpn-console/gitlab-plugin": "^3.0.0",
"@cpn-console/keycloak-plugin": "^2.0.6",
"@cpn-console/kubernetes-plugin": "^2.1.1",
"@cpn-console/gitlab-plugin": "^3.1.0",
"@cpn-console/keycloak-plugin": "^2.1.0",
"@cpn-console/kubernetes-plugin": "^2.3.0",
"@cpn-console/shared": "^1.2.0",
"@cpn-console/ts-config": "^1.1.0",
"@cpn-console/vault-plugin": "^2.2.1",
"@cpn-console/vault-plugin": "^2.2.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.10.7",
"@types/uuid": "^10.0.0",
Expand Down
2 changes: 2 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
/// <reference types="@cpn-console/keycloak-plugin/types/index.d.ts" />
/// <reference types="@cpn-console/kubernetes-plugin/types/index.d.ts" />
/// <reference types="@cpn-console/vault-plugin/types/index.d.ts" />
/// <reference types="@cpn-console/hooks/types/index.d.ts" />
/// <reference types="@cpn-console/shared/types/index.d.ts" />
77 changes: 50 additions & 27 deletions src/function.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import type { Environment, Project, StepCall, UserObject } from '@cpn-console/hooks'
import type { Environment, PluginResult, Project, StepCall, UserObject } from '@cpn-console/hooks'
import type { KeycloakProjectApi } from '@cpn-console/keycloak-plugin/types/class.js'
import type { Gitlab as GitlabInterface } from '@gitbeaker/core'
import type { BaseParams, Stage } from './utils.js'
import { parseError } from '@cpn-console/hooks'
import { removeTrailingSlash, requiredEnv } from '@cpn-console/shared'
import { parseError, specificallyDisabled } from '@cpn-console/hooks'
import { compressUUID, removeTrailingSlash, requiredEnv } from '@cpn-console/shared'
import { Gitlab } from '@gitbeaker/rest'
import { deleteKeycloakGroup, ensureKeycloakGroups } from './keycloak.js'
import { isNewNsName, type TenantKeycloakMapper } from './utils.js'
import { deleteGitlabYamlConfig, upsertGitlabConfig } from './yaml.js'

const getBaseParams = (project: Project, stage: Stage): BaseParams => ({ organizationName: project.organization.name, projectName: project.name, stage })
const okSkipped: PluginResult = {
status: {
result: 'OK',
message: 'Plugin disabled',
},
}

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

Expand Down Expand Up @@ -58,35 +64,52 @@ function getGitlabApi(): GitlabInterface {

export const upsertProject: StepCall<Project> = async (payload) => {
try {
if (specificallyDisabled(payload.config.observability?.enabled)) {
return okSkipped
}
// init args
const project = payload.args
const keycloakApi = payload.apis.keycloak
const vaultApi = payload.apis.vault
const keycloakApi = payload.apis.keycloak as KeycloakProjectApi
// init gitlab api
const gitlabApi = getGitlabApi()
const keycloakRootGroupPath = await keycloakApi.getProjectGroupPath()
const tenantRbacProd = [`${keycloakRootGroupPath}/grafana/prod-RW`, `${keycloakRootGroupPath}/grafana/prod-RO`]
const tenantRbacHProd = [`${keycloakRootGroupPath}/grafana/hprod-RW`, `${keycloakRootGroupPath}/grafana/hprod-RO`]

const compressedUUID = compressUUID(project.id)

const tenantsToCreate: TenantKeycloakMapper = {}

for (const environment of payload.args.environments) {
if (!environment.apis.kubernetes) {
throw new Error(`no kubernetes apis on environment ${environment.name}`)
}
const name = isNewNsName(await environment.apis.kubernetes.getNsName()) ? compressedUUID : project.slug
if (environment.stage === 'prod') {
tenantsToCreate[`prod-${name}`] = {
groups: tenantRbacProd,
name,
type: 'prod',
}
} else {
tenantsToCreate[`hprod-${name}`] = {
groups: tenantRbacHProd,
name,
type: 'hprod',
}
}
}

const hasProd = project.environments.find(env => env.stage === 'prod')
const hasNonProd = project.environments.find(env => env.stage !== 'prod')
const hProdParams = getBaseParams(project, 'hprod')
const prodParams = getBaseParams(project, 'prod')
const listPerms = getListPrems(project.environments)

await Promise.all([
ensureKeycloakGroups(listPerms, keycloakApi),
// Upsert or delete Gitlab config based on prod/non-prod environment
...(hasProd
? [await upsertGitlabConfig(prodParams, keycloakRootGroupPath, project, gitlabApi, vaultApi)]
: [await deleteGitlabYamlConfig(prodParams, project, gitlabApi)]),
...(hasNonProd
? [await upsertGitlabConfig(hProdParams, keycloakRootGroupPath, project, gitlabApi, vaultApi)]
: [await deleteGitlabYamlConfig(hProdParams, project, gitlabApi)]),
])
// Upsert or delete Gitlab config based on prod/non-prod environment
const yamlResult = await upsertGitlabConfig(project, gitlabApi, tenantsToCreate)
await ensureKeycloakGroups(listPerms, keycloakApi)

return {
status: {
result: 'OK',
message: 'Created',
message: yamlResult,
},
}
} catch (error) {
Expand All @@ -102,16 +125,16 @@ export const upsertProject: StepCall<Project> = async (payload) => {

export const deleteProject: StepCall<Project> = async (payload) => {
try {
if (specificallyDisabled(payload.config.observability?.enabled)) {
return okSkipped
}
const project = payload.args
const gitlabApi = getGitlabApi()
const keycloakApi = payload.apis.keycloak
const hProdParams = getBaseParams(project, 'hprod')
const prodParams = getBaseParams(project, 'prod')
const keycloakApi = payload.apis.keycloak as KeycloakProjectApi

await Promise.all([
deleteKeycloakGroup(keycloakApi),
deleteGitlabYamlConfig(prodParams, project, gitlabApi),
deleteGitlabYamlConfig(hProdParams, project, gitlabApi),
deleteGitlabYamlConfig(project, gitlabApi),
])

return {
Expand Down
1 change: 0 additions & 1 deletion src/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export async function getGitlabYamlFileContent(api: IGitlab, project: Project, f

// Fonction pour éditer, committer et pousser un fichier YAML
export async function commitAndPushYamlFile(api: IGitlab, project: Project, filePath: string, branch: string, commitMessage: string, yamlString: string): Promise<void> {
console.log('yamlString: ', yamlString)
const encodedContent = Buffer.from(yamlString).toString('utf-8')
try {
// Vérifier si le fichier existe déjà
Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Plugin } from '@cpn-console/hooks'
import type { DeclareModuleGenerator, Plugin } from '@cpn-console/hooks'
import { requiredEnv } from '@cpn-console/shared'
import { deleteProject, upsertProject } from './function.js'
import infos from './infos.js'
Expand All @@ -19,3 +19,7 @@ export const plugin: Plugin = {
},
start: () => { requiredEnv('GRAFANA_URL') }, // to check is the variable is set, unless it crashes the app
}

declare module '@cpn-console/hooks' {
interface Config extends DeclareModuleGenerator<typeof infos, 'global'> {}
}
75 changes: 62 additions & 13 deletions src/infos.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
import type { ServiceInfos } from '@cpn-console/hooks'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { compressUUID, ENABLED } from '@cpn-console/shared'
import { getConfig } from './utils.js'

const infos: ServiceInfos = {
const imageData = Buffer.from((readFileSync(join(import.meta.dirname, '../files/logo.png'))).toString('base64'))

const infos = {
name: 'observability',
to: ({ project, organization }) => [
{
to: `${getConfig().grafanaUrl}/hprod-${organization}-${project}`,
title: 'Hors production',
},
{
to: `${getConfig().grafanaUrl}/prod-${organization}-${project}`,
title: 'Production',
},
],
// @ts-ignore retro compatibility
to: ({ project, projectId, organization }) => {
let isInfV9 = false
const params = {
id: '',
slug: '',
}
const grafanaUrl = getConfig().grafanaUrl
if (typeof project === 'string' && typeof organization === 'string') {
params.id = projectId
params.slug = `${organization}-${project}`
isInfV9 = true
} else {
params.id = project.id
params.slug = project.slug
}
return [
{
to: `${grafanaUrl}/prod-${compressUUID(String(params.id))}`,
title: isInfV9 ? 'Production' : undefined,
description: 'Production',
},
{
to: `${grafanaUrl}/prod-${params.slug}`,
title: isInfV9 ? 'Production ancien' : undefined,
description: 'Production ancien',
},
{
to: `${grafanaUrl}/hprod-${compressUUID(String(params.id))}`,
title: isInfV9 ? 'Hors production' : undefined,
description: 'Hors production',
},
{
to: `${grafanaUrl}/hprod-${params.slug}`,
title: isInfV9 ? 'Hors production ancien' : undefined,
description: 'Hors production ancien',
},
]
},
title: 'Grafana',
imgSrc: 'https://upload.wikimedia.org/wikipedia/commons/a/a1/Grafana_logo.svg',
imgSrc: `data:image/png;base64,${imageData}`,
description: 'Grafana est un outil de métrique et de logs',
}
config: {
global: [{
kind: 'switch',
key: 'enabled',
initialValue: ENABLED,
permissions: {
admin: { read: true, write: true },
user: { read: true, write: false },
},
title: 'Activer le plugin',
value: ENABLED,
description: 'Activer le plugin',
}],
project: [],
},
} as const satisfies ServiceInfos

export default infos
16 changes: 12 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,16 @@ export function getCustomK8sApi(): CustomObjectsApi {

export type Stage = 'prod' | 'hprod'

export interface BaseParams {
organizationName: string
projectName: string
stage: Stage
export interface TenantInfo {
groups: string[]
type: 'prod' | 'hprod'
name: string // tenant name, short-uuid or slug
}
export interface TenantKeycloakMapper {
[x: string]: TenantInfo // fullName, type + (short-uuid or slug)
}

const re = /[a-z0-9]{25}--[a-z0-9]{25}/
export function isNewNsName(ns: string) {
return re.test(ns)
}
Loading