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
3 changes: 3 additions & 0 deletions x-pack/platform/plugins/shared/task_manager/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"xpack",
"task_manager"
],
"requiredPlugins": [
"licensing"
],
Comment on lines +17 to +19
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this check, could licensing be an optional dependency?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm following a similar pattern as the alerting plugin to check for if security is enabled: https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/alerting/kibana.jsonc

Here we have added the licensing plugin a a require plugin, I feel like it's important we determine if security is enabled, is there a downside to adding the licensing plugin to requiredPlugins? Thanks

Copy link
Copy Markdown
Contributor

@jloleysens jloleysens Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so per se, but it does keep the task manager plugin a bit less coupled should licensing ever be disabled. Making it an optionalPlugin also means task manager can work without it which I think is a nice property to maintain.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usage would just look more like:

licensing?.license$.subscribe(updateLicenseState);

"optionalPlugins": [
"cloud",
"usageCollection",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TaskManagerPlugin } from '../plugin';
import { coreMock } from '@kbn/core/server/mocks';
import type { TaskManagerConfig } from '../config';
import { BulkUpdateError } from '../lib/bulk_update_error';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';

const mockTaskTypeRunFn = jest.fn();
const mockCreateTaskRunner = jest.fn();
Expand Down Expand Up @@ -135,7 +136,9 @@ describe('managed configuration', () => {
esStart.client.asInternalUser as unknown as Client
);
coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient);
taskManagerStart = await taskManager.start(coreStart, {});
taskManagerStart = await taskManager.start(coreStart, {
licensing: licensingMock.createStart(),
});

// force rxjs timers to fire when they are scheduled for setTimeout(0) as the
// sinon fake timers cause them to stall. We need to do this a few times for the
Expand Down Expand Up @@ -233,7 +236,9 @@ describe('managed configuration', () => {
esStart.client.asInternalUser as unknown as Client
);
coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient);
taskManagerStart = taskManager.start(coreStart, {});
taskManagerStart = taskManager.start(coreStart, {
licensing: licensingMock.createStart(),
});

// force rxjs timers to fire when they are scheduled for setTimeout(0) as the
// sinon fake timers cause them to stall. We need to do this a few times for the
Expand Down Expand Up @@ -335,7 +340,9 @@ describe('managed configuration', () => {
esStart.client.asInternalUser as unknown as Client
);
coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient);
taskManagerStart = await taskManager.start(coreStart, {});
taskManagerStart = await taskManager.start(coreStart, {
licensing: licensingMock.createStart(),
});

// force rxjs timers to fire when they are scheduled for setTimeout(0) as the
// sinon fake timers cause them to stall. We need to do this a few times for the
Expand Down Expand Up @@ -420,7 +427,9 @@ describe('managed configuration', () => {
esStart.client.asInternalUser as unknown as Client
);
coreStart.savedObjects.createInternalRepository.mockReturnValue(savedObjectsClient);
taskManagerStart = await taskManager.start(coreStart, {});
taskManagerStart = await taskManager.start(coreStart, {
licensing: licensingMock.createStart(),
});

// force rxjs timers to fire when they are scheduled for setTimeout(0) as the
// sinon fake timers cause them to stall
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { LicenseSubscriber } from './license_subscriber';
import { Subject } from 'rxjs';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import type { ILicense } from '@kbn/licensing-plugin/server';

describe('LicenseSubscriber', () => {
afterEach(() => {
jest.resetAllMocks();
});

describe('getIsSecurityEnabled', () => {
test('should return true if security is enabled', () => {
const license: Subject<ILicense> = new Subject();
const licenseSubscriber = new LicenseSubscriber(license);

const basicLicense = licensingMock.createLicense({
license: { status: 'active', type: 'basic' },
features: { security: { isEnabled: true, isAvailable: true } },
});

license.next(basicLicense);
expect(licenseSubscriber.getIsSecurityEnabled()).toEqual(true);
});

test('should return false if security doesnt exist', () => {
const license: Subject<ILicense> = new Subject();
const licenseSubscriber = new LicenseSubscriber(license);

expect(licenseSubscriber.getIsSecurityEnabled()).toEqual(false);
});

test('should return false if security is disabled', () => {
const license: Subject<ILicense> = new Subject();
const licenseSubscriber = new LicenseSubscriber(license);

const basicLicense = licensingMock.createLicense({
license: { status: 'active', type: 'basic' },
features: { security: { isEnabled: false, isAvailable: true } },
});

license.next(basicLicense);
expect(licenseSubscriber.getIsSecurityEnabled()).toEqual(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { ILicense } from '@kbn/licensing-plugin/server';
import type { Observable, Subscription } from 'rxjs';

export class LicenseSubscriber {
private subscription: Subscription;

private licenseState?: ILicense;

constructor(license: Observable<ILicense>) {
this.getIsSecurityEnabled = this.getIsSecurityEnabled.bind(this);
this.updateState = this.updateState.bind(this);

this.subscription = license.subscribe(this.updateState);
}

private updateState(license: ILicense | undefined) {
this.licenseState = license;
}

public getIsSecurityEnabled() {
if (!this.licenseState || !this.licenseState.isAvailable) {
return false;
}

return this.licenseState.getFeature('security').isEnabled;
}

public cleanup() {
this.subscription.unsubscribe();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { cloudMock } from '@kbn/cloud-plugin/public/mocks';
import { taskPollingLifecycleMock } from './polling_lifecycle.mock';
import { TaskPollingLifecycle } from './polling_lifecycle';
import type { TaskPollingLifecycle as TaskPollingLifecycleClass } from './polling_lifecycle';
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';

let mockTaskPollingLifecycle = taskPollingLifecycleMock.create({});
jest.mock('./polling_lifecycle', () => {
Expand Down Expand Up @@ -140,6 +141,7 @@ describe('TaskManagerPlugin', () => {
taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined });
taskManagerPlugin.start(coreStart, {
cloud: cloudMock.createStart(),
licensing: licensingMock.createStart(),
});

expect(TaskPollingLifecycle as jest.Mock<TaskPollingLifecycleClass>).toHaveBeenCalledTimes(1);
Expand All @@ -154,6 +156,7 @@ describe('TaskManagerPlugin', () => {
taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined });
taskManagerPlugin.start(coreStart, {
cloud: cloudMock.createStart(),
licensing: licensingMock.createStart(),
});

expect(TaskPollingLifecycle as jest.Mock<TaskPollingLifecycleClass>).not.toHaveBeenCalled();
Expand All @@ -170,6 +173,7 @@ describe('TaskManagerPlugin', () => {
taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined });
taskManagerPlugin.start(coreStart, {
cloud: cloudMock.createStart(),
licensing: licensingMock.createStart(),
});

expect(TaskPollingLifecycle as jest.Mock<TaskPollingLifecycleClass>).toHaveBeenCalledTimes(1);
Expand All @@ -188,6 +192,7 @@ describe('TaskManagerPlugin', () => {
taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined });
taskManagerPlugin.start(coreStart, {
cloud: cloudMock.createStart(),
licensing: licensingMock.createStart(),
});

await taskManagerPlugin.stop();
Expand All @@ -204,6 +209,7 @@ describe('TaskManagerPlugin', () => {
taskManagerPlugin.setup(coreMock.createSetup(), { usageCollection: undefined });
taskManagerPlugin.start(coreStart, {
cloud: cloudMock.createStart(),
licensing: licensingMock.createStart(),
});

discoveryIsStarted.mockReturnValueOnce(true);
Expand Down
12 changes: 11 additions & 1 deletion x-pack/platform/plugins/shared/task_manager/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import type {
} from '@kbn/core/server';
import type { CloudSetup, CloudStart } from '@kbn/cloud-plugin/server';
import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-shared';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/server';
import type { PublicMethodsOf } from '@kbn/utility-types';
import {
registerDeleteInactiveNodesTaskDefinition,
scheduleDeleteInactiveNodesTaskDefinition,
Expand Down Expand Up @@ -58,6 +60,7 @@ import {
scheduleMarkRemovedTasksAsUnrecognizedDefinition,
} from './removed_tasks/mark_removed_tasks_as_unrecognized';
import { getElasticsearchAndSOAvailability } from './lib/get_es_and_so_availability';
import { LicenseSubscriber } from './license_subscriber';

export interface TaskManagerSetupContract {
/**
Expand Down Expand Up @@ -92,6 +95,7 @@ export type TaskManagerStartContract = Pick<
};

export interface TaskManagerPluginsStart {
licensing: LicensingPluginStart;
cloud?: CloudStart;
usageCollection?: UsageCollectionStart;
}
Expand Down Expand Up @@ -132,6 +136,7 @@ export class TaskManagerPlugin
private heapSizeLimit: number = 0;
private numOfKibanaInstances$: Subject<number> = new BehaviorSubject(1);
private canEncryptSavedObjects: boolean;
private licenseSubscriber?: PublicMethodsOf<LicenseSubscriber>;

constructor(private readonly initContext: PluginInitializerContext) {
this.initContext = initContext;
Expand Down Expand Up @@ -286,8 +291,10 @@ export class TaskManagerPlugin

public start(
{ http, savedObjects, elasticsearch, executionContext, security }: CoreStart,
{ cloud }: TaskManagerPluginsStart
{ cloud, licensing }: TaskManagerPluginsStart
): TaskManagerStartContract {
this.licenseSubscriber = new LicenseSubscriber(licensing.license$);

const savedObjectsRepository = savedObjects.createInternalRepository([
TASK_SO_NAME,
BACKGROUND_TASK_NODE_SO_NAME,
Expand Down Expand Up @@ -320,6 +327,7 @@ export class TaskManagerPlugin
requestTimeouts: this.config.request_timeouts,
security,
canEncryptSavedObjects: this.canEncryptSavedObjects,
getIsSecurityEnabled: this.licenseSubscriber?.getIsSecurityEnabled,
});

const isServerless = this.initContext.env.packageInfo.buildFlavor === 'serverless';
Expand Down Expand Up @@ -431,6 +439,8 @@ export class TaskManagerPlugin
}

public async stop() {
this.licenseSubscriber?.cleanup();

// Stop polling for tasks
if (this.taskPollingLifecycle) {
this.taskPollingLifecycle.stop();
Expand Down
Loading