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

Allow installing plugins declaratively #6548

Draft
wants to merge 1 commit into
base: develop
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
88 changes: 85 additions & 3 deletions server/core/lib/plugins/plugin-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { decachePlugin } from '@server/helpers/decache.js'
import { ApplicationModel } from '@server/models/application/application.js'
import { MOAuthTokenUser, MUser } from '@server/types/models/index.js'
import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins.js'
import { execShell } from '../../helpers/core-utils.js'
import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js'
import { PLUGIN_GLOBAL_CSS_PATH } from '../../initializers/constants.js'
Expand Down Expand Up @@ -334,13 +335,93 @@ export class PluginManager implements ServerHook {

// ###################### Installation ######################

async installDeclarativePlugins () {
const pluginDirectory = CONFIG.STORAGE.PLUGINS_DIR
let declaredPlugins
try {
declaredPlugins = await readJSON(join(pluginDirectory, "declarative_plugins.json"));
}
catch (err) {
logger.info("No declarative plugins file found.")
return
}

const existingDeclaredPlugins: PluginModel[] = await PluginModel.listDeclarativePluginsAndThemes()

for (const plugin of existingDeclaredPlugins) {
const npmName = PluginModel.buildNpmName(plugin.name, plugin.type)
if (!(npmName in declaredPlugins)) {
logger.info("Removing previously declared plugin %s", npmName)
await this.uninstall({npmName: npmName, unregister: false})
}
else {
logger.info("Keeping still-declared plugin %s.", npmName)
}
}

for (const npmName of Object.keys(declaredPlugins)) {
const {
pluginPath = null,
version = null,
extraArgs = null,
preInstall = null,
postInstall = null
} = declaredPlugins[npmName];

if (preInstall != null) {
logger.info("Running pre-install command for plugin %s.", npmName)
try {
await execShell(preInstall, { cwd: pluginDirectory })
} catch (result) {
logger.error("Cannot exec pre-install command.", { preInstall, err: result.err, stderr: result.stderr })

throw result.err
}
}

if (pluginPath != null) {
logger.info("Installing declared plugin %s from disk (path %s).", npmName, pluginPath)
await this.install({
toInstall: pluginPath,
fromDisk: true,
declarative: true,
extraArgs: extraArgs,
})
}
else if (version != null) {
logger.info("Installing declared plugin %s (version %s) from npm.", npmName, version)
await this.install({
toInstall: npmName,
version: version,
declarative: true,
extraArgs: extraArgs,
})
}

if (postInstall != null) {
logger.info("Running post-install command for plugin %s.", npmName)
try {
await execShell(postInstall, { cwd: pluginDirectory })
} catch (result) {
logger.error("Cannot exec post-install command.", { postInstall, err: result.err, stderr: result.stderr })

throw result.err
}
}
}

this.sortHooksByPriority()
}

async install (options: {
toInstall: string
version?: string
fromDisk?: boolean // default false
register?: boolean // default true
declarative?: boolean // default false
extraArgs?: string // default empty
}) {
const { toInstall, version, fromDisk = false, register = true } = options
const { toInstall, version, fromDisk = false, register = true, declarative = false, extraArgs = null } = options

let plugin: PluginModel
let npmName: string
Expand All @@ -349,8 +430,8 @@ export class PluginManager implements ServerHook {

try {
fromDisk
? await installNpmPluginFromDisk(toInstall)
: await installNpmPlugin(toInstall, version)
? await installNpmPluginFromDisk(toInstall, extraArgs)
: await installNpmPlugin(toInstall, version, extraArgs)

npmName = fromDisk ? basename(toInstall) : toInstall
const pluginType = PluginModel.getTypeFromNpmName(npmName)
Expand All @@ -368,6 +449,7 @@ export class PluginManager implements ServerHook {
version: packageJSON.version,
enabled: true,
uninstalled: false,
declarative: declarative,
peertubeEngine: packageJSON.engine.peertube
}, { returning: true })

Expand Down
6 changes: 4 additions & 2 deletions server/core/lib/plugins/yarn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js'
import { getLatestPluginVersion } from './plugin-index.js'

async function installNpmPlugin (npmName: string, versionArg?: string) {
async function installNpmPlugin (npmName: string, versionArg?: string, extraArgs?: string) {
// Security check
checkNpmPluginNameOrThrow(npmName)
if (versionArg) checkPluginVersionOrThrow(versionArg)
Expand All @@ -15,13 +15,15 @@ async function installNpmPlugin (npmName: string, versionArg?: string) {

let toInstall = npmName
if (version) toInstall += `@${version}`
if (extraArgs) toInstall += ` ${extraArgs}`

const { stdout } = await execYarn('add ' + toInstall)

logger.debug('Added a yarn package.', { yarnStdout: stdout })
}

async function installNpmPluginFromDisk (path: string) {
async function installNpmPluginFromDisk (path: string, extraArgs?: string) {
if (extraArgs) path += ` ${extraArgs}`
await execYarn('add file:' + path)
}

Expand Down
15 changes: 15 additions & 0 deletions server/core/models/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ export class PluginModel extends SequelizeModel<PluginModel> {
@Column(DataType.JSONB)
storage: any

@AllowNull(true)
@Column
declarative: boolean

@CreatedAt
createdAt: Date

Expand All @@ -103,6 +107,17 @@ export class PluginModel extends SequelizeModel<PluginModel> {
return PluginModel.findAll(query)
}

static listDeclarativePluginsAndThemes (): Promise<MPlugin[]> {
const query = {
where: {
declarative: true,
uninstalled: false
}
}

return PluginModel.findAll(query)
}

static loadByNpmName (npmName: string): Promise<MPlugin> {
const name = this.normalizePluginName(npmName)
const type = this.getTypeFromNpmName(npmName)
Expand Down
1 change: 1 addition & 0 deletions server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ async function startApplication () {
server.listen(port, hostname, async () => {
if (cliOptions.plugins) {
try {
await PluginManager.Instance.installDeclarativePlugins()
await PluginManager.Instance.rebuildNativePluginsIfNeeded()

await PluginManager.Instance.registerPluginsAndThemes()
Expand Down