diff --git a/packages/create-plugin/src/codemods/migrations/migrations.ts b/packages/create-plugin/src/codemods/migrations/migrations.ts index 1f1afc64a3..97d54bc167 100644 --- a/packages/create-plugin/src/codemods/migrations/migrations.ts +++ b/packages/create-plugin/src/codemods/migrations/migrations.ts @@ -71,6 +71,12 @@ export default [ 'Fix ts-node compatibility with the latest @grafana/tsconfig: outdated module/moduleResolution/target overrides break TypeScript 5/6 builds, replaced with nodenext/nodenext/es2022.', scriptPath: import.meta.resolve('./scripts/010-ts-node-nodenext.js'), }, + { + name: '013-enable-compose-selinux-relabel', + version: '7.4.1', + description: 'Enable SELinux bindmount relabeling allowing the use of rootless podman for plugin development', + scriptPath: import.meta.resolve('./scripts/012-enable-compose-selinux-relabel.js'), + }, // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run // for those written before the switch to updates as migrations. ] satisfies Migration[]; diff --git a/packages/create-plugin/src/codemods/migrations/scripts/013-enable-compose-selinux-relabel.test.ts b/packages/create-plugin/src/codemods/migrations/scripts/013-enable-compose-selinux-relabel.test.ts new file mode 100644 index 0000000000..bb44bece43 --- /dev/null +++ b/packages/create-plugin/src/codemods/migrations/scripts/013-enable-compose-selinux-relabel.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; +import { Context } from '../../context.js'; +import migrate from './013-enable-compose-selinux-relabel.js'; +import { parse, stringify } from 'yaml'; + +describe('012-enable-compose-selinux-relabel', () => { + it('should not modify anything if base compose file does not exist', async () => { + const context = new Context('/virtual'); + context.addFile( + './docker-compose.yaml', + stringify({ + services: { + grafana: { + volumes: ['/foo:/bar'], + }, + }, + }) + ); + const initialChanges = context.listChanges(); + await migrate(context); + expect(context.listChanges()).toEqual(initialChanges); + }); + + it('should add :Z to all bindmounts', async () => { + const context = new Context('/virtual'); + context.addFile( + './.config/docker-compose-base.yaml', + stringify({ + services: { + grafana: { + volumes: ['../provisioning:/etc/grafana/provisioning', '..:/root/test-plugin'], + }, + }, + }) + ); + await migrate(context); + + const result = parse(context.getFile('./.config/docker-compose-base.yaml') || ''); + expect(result.services.grafana.volumes).toEqual([ + '../provisioning:/etc/grafana/provisioning:Z', + '..:/root/test-plugin:Z', + ]); + }); + + it('should modify existing bindmount opts', async () => { + const context = new Context('/virtual'); + context.addFile( + './.config/docker-compose-base.yaml', + stringify({ + services: { + grafana: { + volumes: ['../provisioning:/etc/grafana/provisioning:ro', '..:/root/test-plugin:z'], + }, + }, + }) + ); + await migrate(context); + + const result = parse(context.getFile('./.config/docker-compose-base.yaml') || ''); + expect(result.services.grafana.volumes).toEqual([ + '../provisioning:/etc/grafana/provisioning:roZ', + '..:/root/test-plugin:z', + ]); + }); + + it('should be idempotent', async () => { + const context = new Context('/virtual'); + context.addFile( + './.config/docker-compose-base.yaml', + stringify({ + services: { + grafana: { + volumes: ['../provisioning:/etc/grafana/provisioning:Z', '..:/root/test-plugin:Z'], + }, + }, + }) + ); + await expect(migrate).toBeIdempotent(context); + }); +}); diff --git a/packages/create-plugin/src/codemods/migrations/scripts/013-enable-compose-selinux-relabel.ts b/packages/create-plugin/src/codemods/migrations/scripts/013-enable-compose-selinux-relabel.ts new file mode 100644 index 0000000000..93ce363604 --- /dev/null +++ b/packages/create-plugin/src/codemods/migrations/scripts/013-enable-compose-selinux-relabel.ts @@ -0,0 +1,40 @@ +import { type Context } from '../../context.js'; +import { parseDocument, stringify, isSeq, isScalar } from 'yaml'; + +export default async function migrate(context: Context) { + const baseComposeContent = context.getFile('./.config/docker-compose-base.yaml'); + + if (!baseComposeContent) { + return context; + } + + const baseComposeData = parseDocument(baseComposeContent); + + const mounts = baseComposeData.getIn(['services', 'grafana', 'volumes']); + if (!isSeq(mounts)) { + return context; + } + for (const m of mounts.items) { + if (!isScalar(m) || typeof m.value !== 'string') { + continue; + } + + const parts = m.value.split(':'); + if (parts.length < 3) { + m.value += ':Z'; + continue; + } + + const opts = parts[parts.length - 1]; + if (!opts.match(/[zZ]/)) { + m.value = [...parts.slice(0, -1), opts + 'Z'].join(':'); + } + } + + context.updateFile( + './.config/docker-compose-base.yaml', + stringify(baseComposeData, { lineWidth: 120, singleQuote: true }) + ); + + return context; +} diff --git a/packages/create-plugin/templates/common/.config/docker-compose-base.yaml b/packages/create-plugin/templates/common/.config/docker-compose-base.yaml index 6306eb6eeb..ee1c635fe5 100644 --- a/packages/create-plugin/templates/common/.config/docker-compose-base.yaml +++ b/packages/create-plugin/templates/common/.config/docker-compose-base.yaml @@ -21,9 +21,9 @@ services: - SYS_PTRACE {{/if}} volumes: - - ../dist:/var/lib/grafana/plugins/{{ pluginId }} - - ../provisioning:/etc/grafana/provisioning - - ..:/root/{{ pluginId }} + - ../dist:/var/lib/grafana/plugins/{{ pluginId }}:Z + - ../provisioning:/etc/grafana/provisioning:Z + - ..:/root/{{ pluginId }}:Z environment: NODE_ENV: development