Skip to content
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

feat: ✨ server download its plugins by himself #1366

Draft
wants to merge 1 commit into
base: tech/hook-rework
Choose a base branch
from
Draft
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
4 changes: 3 additions & 1 deletion apps/server/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ KEYCLOAK_CLIENT_ID=dso-console-backend
KEYCLOAK_CLIENT_SECRET=client-secret-backend
KEYCLOAK_REDIRECT_URI=http://localhost:8080
SERVER_PORT=4000
DB_URL=postgresql://admin:admin@localhost:5432/dso-console-db?schema=public
DB_URL=postgresql://admin:admin@localhost:5432/dso-console-db?schema=public
# CSV of plugins url sources .zip or .json see pluginVersionMapper
PLUGINS_SOURCES=http://cool_leakey:8000/mapper.json,http://cool_leakey:8000/observability.zip
2 changes: 2 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@ts-rest/open-api": "^3.45.2",
"axios": "^1.7.2",
"date-fns": "^3.6.0",
"decompress": "^4.2.1",
"dotenv": "^16.4.5",
"fastify": "^4.28.0",
"fastify-keycloak-adapter": "^2.1.10",
Expand All @@ -66,6 +67,7 @@
"nanoid": "5.0.7",
"node-vault-client": "^1.0.1",
"prisma": "^5.16.1",
"semver": "^7.6.3",
"undici": "^6.19.2",
"vitest-mock-extended": "^1.3.1"
},
Expand Down
101 changes: 99 additions & 2 deletions apps/server/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { readdirSync, statSync } from 'node:fs'
import path from 'node:path'
import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmdirSync, statSync, unlinkSync } from 'node:fs'
import axios from 'axios'
// @ts-expect-error TS7016
import decompress from 'decompress'
// @ts-expect-error TS7016
import semver from 'semver'
import { type Plugin, pluginManager } from '@cpn-console/hooks'
import { plugin as argo } from '@cpn-console/argocd-plugin'
import { plugin as gitlab } from '@cpn-console/gitlab-plugin'
Expand All @@ -9,9 +15,99 @@ import { plugin as nexus } from '@cpn-console/nexus-plugin'
import { plugin as sonarqube } from '@cpn-console/sonarqube-plugin'
import { plugin as vault } from '@cpn-console/vault-plugin'
import { pluginManagerOptions } from './utils/plugins.js'
import { pluginsDir } from './utils/env.js'
import { pluginsDir, pluginsSources } from './utils/env.js'

async function downloadZip(url: string, savePath: string) {
// axios image download with response type "stream"
const response = await axios({
method: 'GET',
url,
responseType: 'stream',
})

// pipe the result stream into a file on disc
response.data.pipe(createWriteStream(savePath))

// return a promise and resolve when download finishes
return new Promise<string>((resolve, reject) => {
response.data.on('end', () => {
resolve(savePath)
})

response.data.on('error', () => {
// @ts-ignore
reject(new Error(`Unable to download archive`))
})
})
}

const hooksPackageJsonPath = path.join(
path.dirname(
path.dirname(
require.resolve('@cpn-console/hooks'),
),
),
'package.json',
)

const hooksPackageVersion = JSON.parse(readFileSync(hooksPackageJsonPath, 'utf-8')).version // 2.3.2

async function importZipPuglin(zipUrl: string, name: string, pluginsArchiveDir: string) {
const zipPath = path.join(pluginsArchiveDir, `${name}.zip`)
await downloadZip(zipUrl, zipPath)
const pluginPath = path.join(pluginsDir, name)
await decompress(zipPath, pluginPath)
}

async function getMappedVersion(mapperUrl: string) {
const mapper = await axios.get(mapperUrl)
let url
if (mapper.status !== 200) {
throw new Error(`unable to fetch mapper ${mapperUrl}`)
}
for (const [range, pluginUrl] of Object.entries(mapper.data)) {
if (semver.satisfies(hooksPackageVersion, range)) {
url = pluginUrl
break
}
}

if (!url) {
throw new Error('impossible de trouver une version correspondante')
}
return url as string
}

export async function fetchExternalPlugins(pluginUrls: string[]) {
if (!pluginUrls.length) return

const pluginsArchiveDir = mkdtempSync('/tmp/plugins')

if (!existsSync(pluginsDir)) {
mkdirSync(pluginsDir)
}

let i = 0
for (let pluginUrl of pluginUrls) {
if (pluginUrl.startsWith('!')) {
console.log('ignoring', pluginUrl)
continue
}
i++
if (pluginUrl.endsWith('.json')) {
pluginUrl = await getMappedVersion(pluginUrl)
}
await importZipPuglin(pluginUrl, `fetched-${i.toString()}`, pluginsArchiveDir)
}
const fetchedPlugins = readdirSync(pluginsArchiveDir)
for (const file of fetchedPlugins) {
unlinkSync(path.join(pluginsArchiveDir, file))
}
rmdirSync(pluginsArchiveDir)
}

export async function initPm() {
const fetchPromise = fetchExternalPlugins(pluginsSources.split(','))
const pm = pluginManager(pluginManagerOptions)
pm.register(argo)
pm.register(gitlab)
Expand All @@ -22,6 +118,7 @@ export async function initPm() {
pm.register(sonarqube)
pm.register(vault)

await fetchPromise
if (!statSync(pluginsDir, {
throwIfNoEntry: false,
})) {
Expand Down
4 changes: 1 addition & 3 deletions apps/server/src/prepare-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ export async function startServer(defaultPort: number = (port ? +port : 8080)) {
throw error
}

initPm()

app.log.info('Reading init database file')

try {
Expand Down Expand Up @@ -76,7 +74,7 @@ export async function getPreparedApp() {
throw error
}

initPm()
await initPm()

app.log.info('Reading init database file')

Expand Down
1 change: 0 additions & 1 deletion apps/server/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getPreparedApp } from './prepare-app.js'
import { closeConnections } from './connect.js'

import { isCI, isDev, isDevSetup, isProd, isTest, port } from './utils/env.js'

const app = await getPreparedApp()
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const adminsUserId = process.env.ADMIN_KC_USER_ID
export const mockPlugins = process.env.MOCK_PLUGINS === 'true'
export const projectRootDir = process.env.PROJECTS_ROOT_DIR
export const pluginsDir = process.env.PLUGINS_DIR ?? '/plugins'
export const pluginsSources = process.env.PLUGINS_SOURCES ?? ''

export const NODE_ENV = process.env.NODE_ENV === 'test'
? 'test'
: process.env.NODE_ENV === 'development'
Expand Down
6 changes: 6 additions & 0 deletions mappers/mapper.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"<1.0.0": "https://cool_leakey/0.0.0.zip",
">=3.0.0 <4.0.0": "https://cool_leakey/3.0.0.zip",
">=2.0.0 <3.0.0": "http://cool_leakey:8000/observability.zip",
">1.0.0": "https://cool_leakey/1.0.0.zip"
}
Binary file added mappers/observability.zip
Binary file not shown.
Loading