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

[Ingest Manager] Support registration of server side callbacks for Create Datasource API #69428

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2f86a8b
Expose `register()` method out of Ingest server `start` lifecycle
paul-tavares Jun 16, 2020
0816d17
Add external callbacks to AppContextService
paul-tavares Jun 16, 2020
9e8ed16
Add support for External Callbacks on REST `createDatasourceHandler()`
paul-tavares Jun 16, 2020
3120282
Added Endpoint Ingest handler for Create Datasources
paul-tavares Jun 16, 2020
82dd221
Handle errors from external callbacks
paul-tavares Jun 17, 2020
bab7468
expose DatasourceServices to Plugin start interface
paul-tavares Jun 17, 2020
e30dfcb
Merge remote-tracking branch 'upstream/master' into task/ingest-68914…
paul-tavares Jun 17, 2020
d5adaa8
Fix types + export types from `/server`
paul-tavares Jun 17, 2020
74db641
Merge remote-tracking branch 'upstream/master' into task/ingest-68914…
paul-tavares Jun 17, 2020
f6e38af
Reverted bad code line
paul-tavares Jun 17, 2020
4deb01d
Merge remote-tracking branch 'upstream/master' into task/ingest-68914…
paul-tavares Jun 17, 2020
5b1d540
Rename `register` method ++ move endpoint registration to EndpointApp…
paul-tavares Jun 18, 2020
8bd5cfa
Ingest: move externalCallbacks out of Plugin interface and into AppCo…
paul-tavares Jun 18, 2020
72c249a
Fix type errors
paul-tavares Jun 18, 2020
af3caa5
Merge remote-tracking branch 'upstream/master' into task/ingest-68914…
paul-tavares Jun 18, 2020
018ba8f
Fix metadata tests
paul-tavares Jun 18, 2020
2a5f67c
Merge remote-tracking branch 'upstream/master' into task/ingest-68914…
paul-tavares Jun 19, 2020
35af2c8
Fix types in metadata tests
paul-tavares Jun 20, 2020
1758eb8
Merge remote-tracking branch 'upstream/master' into task/ingest-68914…
paul-tavares Jun 23, 2020
28f46ce
Merge branch 'master' into task/ingest-68914-datasource-api-hooks
elasticmachine Jun 23, 2020
9da8165
Initial structure for datsources UT
paul-tavares Jun 23, 2020
bf12a27
Additional mocked services
paul-tavares Jun 23, 2020
fc29695
Tests for datasource create External callbacks
paul-tavares Jun 24, 2020
50d4859
Fix typings
paul-tavares Jun 24, 2020
a641f4f
Add validation to return value of callback
paul-tavares Jun 24, 2020
2edcfb3
Merge remote-tracking branch 'upstream/master' into task/ingest-68914…
paul-tavares Jun 24, 2020
4ffeed6
Fix import of Loggin services ++ adjusted test case
paul-tavares Jun 24, 2020
4811e51
Merge remote-tracking branch 'upstream/master' into task/ingest-68914…
paul-tavares Jun 24, 2020
28fae3d
One more test adjustment
paul-tavares Jun 24, 2020
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/plugins/ingest_manager/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export {
IngestManagerSetupContract,
IngestManagerSetupDeps,
IngestManagerStartContract,
ExternalCallback,
} from './plugin';

export const config = {
Expand Down Expand Up @@ -42,6 +43,8 @@ export const config = {

export type IngestManagerConfigType = TypeOf<typeof config.schema>;

export { DatasourceServiceInterface } from './services/datasource';

export const plugin = (initializerContext: PluginInitializerContext) => {
return new IngestManagerPlugin(initializerContext);
};
26 changes: 25 additions & 1 deletion x-pack/plugins/ingest_manager/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ import {
registerSettingsRoutes,
registerAppRoutes,
} from './routes';
import { IngestManagerConfigType } from '../common';
import { IngestManagerConfigType, NewDatasource } from '../common';
import {
appContextService,
licenseService,
ESIndexPatternSavedObjectService,
ESIndexPatternService,
AgentService,
datasourceService,
} from './services';
import { getAgentStatusById } from './services/agents';
import { CloudSetup } from '../../cloud/server';
Expand Down Expand Up @@ -92,12 +93,31 @@ const allSavedObjectTypes = [
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
];

/**
* Callbacks supported by the Ingest plugin
*/
export type ExternalCallback = [
'datasourceCreate',
(newDatasource: NewDatasource) => Promise<NewDatasource>
];

export type ExternalCallbacksStorage = Map<ExternalCallback[0], Set<ExternalCallback[1]>>;

/**
* Describes public IngestManager plugin contract returned at the `startup` stage.
*/
export interface IngestManagerStartContract {
esIndexPatternService: ESIndexPatternService;
agentService: AgentService;
/**
* Services for Ingest's Datasources
*/
datasourceService: typeof datasourceService;
/**
* Register callbacks for inclusion in ingest API processing
* @param args
*/
registerExternalCallback: (...args: ExternalCallback) => void;
}

export class IngestManagerPlugin
Expand Down Expand Up @@ -237,6 +257,10 @@ export class IngestManagerPlugin
agentService: {
getAgentStatusById,
},
datasourceService,
registerExternalCallback: (...args: ExternalCallback) => {
return appContextService.addExternalCallback(...args);
},
};
}

Expand Down
33 changes: 27 additions & 6 deletions x-pack/plugins/ingest_manager/server/routes/datasource/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
CreateDatasourceRequestSchema,
UpdateDatasourceRequestSchema,
DeleteDatasourcesRequestSchema,
NewDatasource,
} from '../../types';
import { CreateDatasourceResponse, DeleteDatasourcesResponse } from '../../../common';

Expand Down Expand Up @@ -76,23 +77,43 @@ export const createDatasourceHandler: RequestHandler<
const soClient = context.core.savedObjects.client;
const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser;
const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined;
const newData = { ...request.body };
const logger = appContextService.getLogger();
let newData = { ...request.body };
try {
// If we have external callbacks, then process those now before creating the actual datasource
const externalCallbacks = appContextService.getExternalCallbacks('datasourceCreate');
if (externalCallbacks && externalCallbacks.size > 0) {
let updatedNewData: NewDatasource = newData;

for (const callback of externalCallbacks) {
paul-tavares marked this conversation as resolved.
Show resolved Hide resolved
try {
updatedNewData = await callback(updatedNewData);
} catch (error) {
// Log the error, but keep going and process the other callbacks
logger.error('An external registered [datasourceCreate] callback failed when executed');
logger.error(error);
}
}

// @ts-ignore
paul-tavares marked this conversation as resolved.
Show resolved Hide resolved
newData = updatedNewData as typeof CreateDatasourceRequestSchema.body;
}

// Make sure the datasource package is installed
if (request.body.package?.name) {
if (newData.package?.name) {
await ensureInstalledPackage({
savedObjectsClient: soClient,
pkgName: request.body.package.name,
pkgName: newData.package.name,
callCluster,
});
const pkgInfo = await getPackageInfo({
savedObjectsClient: soClient,
pkgName: request.body.package.name,
pkgVersion: request.body.package.version,
pkgName: newData.package.name,
pkgVersion: newData.package.version,
});
newData.inputs = (await datasourceService.assignPackageStream(
pkgInfo,
request.body.inputs
newData.inputs
)) as TypeOf<typeof CreateDatasourceRequestSchema.body>['inputs'];
}

Expand Down
20 changes: 18 additions & 2 deletions x-pack/plugins/ingest_manager/server/services/app_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '../../../encrypted_saved_objects/server';
import { SecurityPluginSetup } from '../../../security/server';
import { IngestManagerConfigType } from '../../common';
import { IngestManagerAppContext } from '../plugin';
import { ExternalCallback, ExternalCallbacksStorage, IngestManagerAppContext } from '../plugin';
import { CloudSetup } from '../../../cloud/server';

class AppContextService {
Expand All @@ -27,6 +27,7 @@ class AppContextService {
private cloud?: CloudSetup;
private logger: Logger | undefined;
private httpSetup?: HttpServiceSetup;
private externalCallbacks: ExternalCallbacksStorage = new Map();

public async start(appContext: IngestManagerAppContext) {
this.encryptedSavedObjects = appContext.encryptedSavedObjectsStart?.getClient();
Expand All @@ -47,7 +48,9 @@ class AppContextService {
}
}

public stop() {}
public stop() {
this.externalCallbacks.clear();
}

public getEncryptedSavedObjects() {
if (!this.encryptedSavedObjects) {
Expand Down Expand Up @@ -121,6 +124,19 @@ class AppContextService {
}
return this.kibanaVersion;
}

public addExternalCallback(type: ExternalCallback[0], callback: ExternalCallback[1]) {
if (!this.externalCallbacks.has(type)) {
this.externalCallbacks.set(type, new Set());
}
this.externalCallbacks.get(type)!.add(callback);
}

public getExternalCallbacks(type: ExternalCallback[0]) {
if (this.externalCallbacks) {
return this.externalCallbacks.get(type);
}
}
}

export const appContextService = new AppContextService();
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,5 @@ async function _assignPackageStreamToStream(
return { ...stream };
}

export type DatasourceServiceInterface = DatasourceService;
export const datasourceService = new DatasourceService();
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
sendPutDatasource,
} from '../policy_list/services/ingest';
import { NewPolicyData, PolicyData } from '../../../../../../common/endpoint/types';
import { factory as policyConfigFactory } from '../../../../../../common/endpoint/models/policy_config';
import { ImmutableMiddlewareFactory } from '../../../../../common/store';

export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory<PolicyDetailsState> = (
Expand All @@ -43,23 +42,6 @@ export const policyDetailsMiddlewareFactory: ImmutableMiddlewareFactory<PolicyDe
return;
}

// Until we get the Default configuration into the Endpoint package so that the datasource has
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved this to the server side now that we have the ability to intercept the datasource create. This means that as soon as we create a datasource, the Endpoint policy will be there and we no longer have to navigate over to the Policy page and perform a "save" just so that the policy data is inserted into the DS.

// the expected data structure, we will add it here manually.
if (!policyItem.inputs.length) {
policyItem.inputs = [
{
type: 'endpoint',
enabled: true,
streams: [],
config: {
policy: {
value: policyConfigFactory(),
},
},
},
];
}

dispatch({
type: 'serverReturnedPolicyDetailsData',
payload: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '../../../../../../../src/core/server/mocks';
import { registerAlertRoutes } from '../routes';
import { alertingIndexGetQuerySchema } from '../../../../common/endpoint_alerts/schema/alert_index';
import { createMockAgentService } from '../../mocks';
import { createMockEndpointAppContextServiceStartContract } from '../../mocks';
import { EndpointAppContextService } from '../../endpoint_app_context_services';
import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__';

Expand All @@ -28,9 +28,7 @@ describe('test alerts route', () => {
routerMock = httpServiceMock.createRouter();

endpointAppContextService = new EndpointAppContextService();
endpointAppContextService.start({
agentService: createMockAgentService(),
});
endpointAppContextService.start(createMockEndpointAppContextServiceStartContract());

registerAlertRoutes(routerMock, {
logFactory: loggingServiceMock.create(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AgentService } from '../../../ingest_manager/server';
import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server';
import { handleDatasourceCreate } from './ingest_integration';

export type EndpointAppContextServiceStartContract = Pick<
IngestManagerStartContract,
'agentService'
> & {
registerIngestCallback: IngestManagerStartContract['registerExternalCallback'];
};

/**
* A singleton that holds shared services that are initialized during the start up phase
Expand All @@ -12,8 +20,9 @@ import { AgentService } from '../../../ingest_manager/server';
export class EndpointAppContextService {
private agentService: AgentService | undefined;

public start(dependencies: { agentService: AgentService }) {
public start(dependencies: EndpointAppContextServiceStartContract) {
this.agentService = dependencies.agentService;
dependencies.registerIngestCallback('datasourceCreate', handleDatasourceCreate);
}

public stop() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { factory as policyConfigFactory } from '../../common/endpoint/models/policy_config';
import { NewPolicyData } from '../../common/endpoint/types';
import { NewDatasource } from '../../../ingest_manager/common/types/models';

/**
* Callback to handle creation of Datasources in Ingest Manager
* @param newDatasource
*/
export const handleDatasourceCreate = async (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the most part, this is just a placeholder handler. I currently only moved over the logic that inserts the policy into the Datasource from the Front end Middleware

Copy link
Contributor

Choose a reason for hiding this comment

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

This is great for purposes of this PR! We'll ultimately need to get some things from CoreStart and StartPlugins in here, but that can be done with a closure or using the app context. Thanks!

newDatasource: NewDatasource
): Promise<NewDatasource> => {
// We only care about Endpoint datasources
if (newDatasource.package?.name !== 'endpoint') {
return newDatasource;
}

// We cast the type here so that any changes to the Endpoint specific data
// follow the types/schema expected
let updatedDatasource = newDatasource as NewPolicyData;

// Until we get the Default Policy Configuration in the Endpoint package,
// we will add it here manually at creation time.
// @ts-ignore
if (newDatasource.inputs.length === 0) {
updatedDatasource = {
...newDatasource,
inputs: [
{
type: 'endpoint',
enabled: true,
streams: [],
config: {
policy: {
value: policyConfigFactory(),
},
},
},
],
};
}

return updatedDatasource;
};
39 changes: 38 additions & 1 deletion x-pack/plugins/security_solution/server/endpoint/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,28 @@

import { IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server';
import { xpackMocks } from '../../../../mocks';
import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server';
import {
AgentService,
IngestManagerStartContract,
ExternalCallback,
DatasourceServiceInterface,
} from '../../../ingest_manager/server';
import { EndpointAppContextServiceStartContract } from './endpoint_app_context_services';

/**
* Crates a mocked input contract for the `EndpointAppContextService#start()` method
*/
export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked<
EndpointAppContextServiceStartContract
> => {
return {
agentService: createMockAgentService(),
registerIngestCallback: jest.fn<
ReturnType<IngestManagerStartContract['registerExternalCallback']>,
Parameters<IngestManagerStartContract['registerExternalCallback']>
>(),
};
};

/**
* Creates a mock AgentService
Expand All @@ -17,6 +38,20 @@ export const createMockAgentService = (): jest.Mocked<AgentService> => {
};
};

const createMockDatasourceService = (): jest.Mocked<DatasourceServiceInterface> => {
return {
assignPackageStream: jest.fn(),
buildDatasourceFromPackage: jest.fn(),
bulkCreate: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
get: jest.fn(),
getByIDs: jest.fn(),
list: jest.fn(),
update: jest.fn(),
};
};

/**
* Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's
* ESIndexPatternService.
Expand All @@ -32,6 +67,8 @@ export const createMockIngestManagerStartContract = (
getESIndexPattern: jest.fn().mockResolvedValue(indexPattern),
},
agentService: createMockAgentService(),
registerExternalCallback: jest.fn((...args: ExternalCallback) => {}),
datasourceService: createMockDatasourceService(),
};
};

Expand Down
Loading