[Ingest Manager] Support limiting integrations on an agent config (#70542)
* Add API endpoint and hook for retrieving restricted packages * Filter out restricted packages already in use from list of integrations available for an agent config * Allow list agent configs to optionally return expanded package configs, re * Filter out agent configs which already use the restricted package already from list of agent configs available for an integration * Allow more than 20 agent configs to be shown * Rename restricted to limited; add some common methods to DRY * Add limited package check on server side * Adjust copy wording * Fix typings * Add some package config api integration tests, update es archive mappings * Move test to dockerized integation tests directory; move existing epm tests to their own directory * Remove extra assignPackageConfigs() - already handled in packageConfigService.create() * Review fixes * Fix type, reenabled skipped test * Move new EPM integration test file
This commit is contained in:
parent
c5dd942b72
commit
7debf4dd9f
|
@ -17,6 +17,7 @@ const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`;
|
|||
const EPM_PACKAGES_FILE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`;
|
||||
export const EPM_API_ROUTES = {
|
||||
LIST_PATTERN: EPM_PACKAGES_MANY,
|
||||
LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`,
|
||||
INFO_PATTERN: EPM_PACKAGES_ONE,
|
||||
INSTALL_PATTERN: EPM_PACKAGES_ONE,
|
||||
DELETE_PATTERN: EPM_PACKAGES_ONE,
|
||||
|
|
|
@ -3,11 +3,10 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import * as AgentStatusKueryHelper from './agent_status';
|
||||
|
||||
export * from './routes';
|
||||
export * as AgentStatusKueryHelper from './agent_status';
|
||||
export { packageToPackageConfigInputs, packageToPackageConfig } from './package_to_config';
|
||||
export { storedPackageConfigsToAgentInputs } from './package_configs_to_agent_inputs';
|
||||
export { configToYaml } from './config_to_yaml';
|
||||
export { AgentStatusKueryHelper };
|
||||
export { isPackageLimited, doesAgentConfigAlreadyIncludePackage } from './limited_package';
|
||||
export { decodeCloudId } from './decode_cloud_id';
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { PackageInfo, AgentConfig, PackageConfig } from '../types';
|
||||
|
||||
// Assume packages only ever include 1 config template for now
|
||||
export const isPackageLimited = (packageInfo: PackageInfo): boolean => {
|
||||
return packageInfo.config_templates?.[0]?.multiple === false;
|
||||
};
|
||||
|
||||
export const doesAgentConfigAlreadyIncludePackage = (
|
||||
agentConfig: AgentConfig,
|
||||
packageName: string
|
||||
): boolean => {
|
||||
if (agentConfig.package_configs.length && typeof agentConfig.package_configs[0] === 'string') {
|
||||
throw new Error('Unable to read full package config information');
|
||||
}
|
||||
return (agentConfig.package_configs as PackageConfig[])
|
||||
.map((packageConfig) => packageConfig.package?.name || '')
|
||||
.includes(packageName);
|
||||
};
|
|
@ -27,6 +27,10 @@ export const epmRouteService = {
|
|||
return EPM_API_ROUTES.LIST_PATTERN;
|
||||
},
|
||||
|
||||
getListLimitedPath: () => {
|
||||
return EPM_API_ROUTES.LIMITED_LIST_PATTERN;
|
||||
},
|
||||
|
||||
getInfoPath: (pkgkey: string) => {
|
||||
return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey);
|
||||
},
|
||||
|
|
|
@ -79,6 +79,7 @@ export interface RegistryConfigTemplate {
|
|||
title: string;
|
||||
description: string;
|
||||
inputs: RegistryInput[];
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
export interface RegistryInput {
|
||||
|
|
|
@ -7,7 +7,9 @@ import { AgentConfig, NewAgentConfig, FullAgentConfig } from '../models';
|
|||
import { ListWithKuery } from './common';
|
||||
|
||||
export interface GetAgentConfigsRequest {
|
||||
query: ListWithKuery;
|
||||
query: ListWithKuery & {
|
||||
full?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type GetAgentConfigsResponseItem = AgentConfig & { agents?: number };
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { HttpFetchQuery } from 'src/core/public';
|
||||
|
||||
export interface ListWithKuery {
|
||||
export interface ListWithKuery extends HttpFetchQuery {
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
sortField?: string;
|
||||
|
|
|
@ -34,6 +34,11 @@ export interface GetPackagesResponse {
|
|||
success: boolean;
|
||||
}
|
||||
|
||||
export interface GetLimitedPackagesResponse {
|
||||
response: string[];
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface GetFileRequest {
|
||||
params: {
|
||||
pkgkey: string;
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { HttpFetchQuery } from 'src/core/public';
|
||||
import {
|
||||
useRequest,
|
||||
sendRequest,
|
||||
|
@ -12,6 +11,7 @@ import {
|
|||
} from './use_request';
|
||||
import { agentConfigRouteService } from '../../services';
|
||||
import {
|
||||
GetAgentConfigsRequest,
|
||||
GetAgentConfigsResponse,
|
||||
GetOneAgentConfigResponse,
|
||||
GetFullAgentConfigResponse,
|
||||
|
@ -25,7 +25,7 @@ import {
|
|||
DeleteAgentConfigResponse,
|
||||
} from '../../types';
|
||||
|
||||
export const useGetAgentConfigs = (query: HttpFetchQuery = {}) => {
|
||||
export const useGetAgentConfigs = (query?: GetAgentConfigsRequest['query']) => {
|
||||
return useRequest<GetAgentConfigsResponse>({
|
||||
path: agentConfigRouteService.getListPath(),
|
||||
method: 'get',
|
||||
|
|
|
@ -10,6 +10,7 @@ import { epmRouteService } from '../../services';
|
|||
import {
|
||||
GetCategoriesResponse,
|
||||
GetPackagesResponse,
|
||||
GetLimitedPackagesResponse,
|
||||
GetInfoResponse,
|
||||
InstallPackageResponse,
|
||||
DeletePackageResponse,
|
||||
|
@ -30,6 +31,13 @@ export const useGetPackages = (query: HttpFetchQuery = {}) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const useGetLimitedPackages = () => {
|
||||
return useRequest<GetLimitedPackagesResponse>({
|
||||
path: epmRouteService.getListLimitedPath(),
|
||||
method: 'get',
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetPackageInfoByKey = (pkgkey: string) => {
|
||||
return useRequest<GetInfoResponse>({
|
||||
path: epmRouteService.getInfoPath(pkgkey),
|
||||
|
|
|
@ -9,6 +9,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
|
|||
import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer, EuiTextColor } from '@elastic/eui';
|
||||
import { Error } from '../../../components';
|
||||
import { AgentConfig, PackageInfo, GetAgentConfigsResponseItem } from '../../../types';
|
||||
import { isPackageLimited, doesAgentConfigAlreadyIncludePackage } from '../../../services';
|
||||
import { useGetPackageInfoByKey, useGetAgentConfigs, sendGetOneAgentConfig } from '../../../hooks';
|
||||
|
||||
export const StepSelectConfig: React.FunctionComponent<{
|
||||
|
@ -24,7 +25,12 @@ export const StepSelectConfig: React.FunctionComponent<{
|
|||
const [selectedConfigError, setSelectedConfigError] = useState<Error>();
|
||||
|
||||
// Fetch package info
|
||||
const { data: packageInfoData, error: packageInfoError } = useGetPackageInfoByKey(pkgkey);
|
||||
const {
|
||||
data: packageInfoData,
|
||||
error: packageInfoError,
|
||||
isLoading: packageInfoLoading,
|
||||
} = useGetPackageInfoByKey(pkgkey);
|
||||
const isLimitedPackage = (packageInfoData && isPackageLimited(packageInfoData.response)) || false;
|
||||
|
||||
// Fetch agent configs info
|
||||
const {
|
||||
|
@ -36,6 +42,7 @@ export const StepSelectConfig: React.FunctionComponent<{
|
|||
perPage: 1000,
|
||||
sortField: 'name',
|
||||
sortOrder: 'asc',
|
||||
full: true,
|
||||
});
|
||||
const agentConfigs = agentConfigsData?.items || [];
|
||||
const agentConfigsById = agentConfigs.reduce(
|
||||
|
@ -112,12 +119,18 @@ export const StepSelectConfig: React.FunctionComponent<{
|
|||
searchable
|
||||
allowExclusions={false}
|
||||
singleSelection={true}
|
||||
isLoading={isAgentConfigsLoading}
|
||||
options={agentConfigs.map(({ id, name, description }) => {
|
||||
isLoading={isAgentConfigsLoading || packageInfoLoading}
|
||||
options={agentConfigs.map((agentConf) => {
|
||||
const alreadyHasLimitedPackage =
|
||||
(isLimitedPackage &&
|
||||
packageInfoData &&
|
||||
doesAgentConfigAlreadyIncludePackage(agentConf, packageInfoData.response.name)) ||
|
||||
false;
|
||||
return {
|
||||
label: name,
|
||||
key: id,
|
||||
checked: selectedConfigId === id ? 'on' : undefined,
|
||||
label: agentConf.name,
|
||||
key: agentConf.id,
|
||||
checked: selectedConfigId === agentConf.id ? 'on' : undefined,
|
||||
disabled: alreadyHasLimitedPackage,
|
||||
'data-test-subj': 'agentConfigItem',
|
||||
};
|
||||
})}
|
||||
|
|
|
@ -8,8 +8,13 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiSpacer } from '@elastic/eui';
|
||||
import { Error } from '../../../components';
|
||||
import { AgentConfig, PackageInfo } from '../../../types';
|
||||
import { useGetOneAgentConfig, useGetPackages, sendGetPackageInfoByKey } from '../../../hooks';
|
||||
import { AgentConfig, PackageInfo, PackageConfig, GetPackagesResponse } from '../../../types';
|
||||
import {
|
||||
useGetOneAgentConfig,
|
||||
useGetPackages,
|
||||
useGetLimitedPackages,
|
||||
sendGetPackageInfoByKey,
|
||||
} from '../../../hooks';
|
||||
import { PackageIcon } from '../../../components/package_icon';
|
||||
|
||||
export const StepSelectPackage: React.FunctionComponent<{
|
||||
|
@ -28,12 +33,27 @@ export const StepSelectPackage: React.FunctionComponent<{
|
|||
const { data: agentConfigData, error: agentConfigError } = useGetOneAgentConfig(agentConfigId);
|
||||
|
||||
// Fetch packages info
|
||||
// Filter out limited packages already part of selected agent config
|
||||
const [packages, setPackages] = useState<GetPackagesResponse['response']>([]);
|
||||
const {
|
||||
data: packagesData,
|
||||
error: packagesError,
|
||||
isLoading: isPackagesLoading,
|
||||
} = useGetPackages();
|
||||
const packages = packagesData?.response || [];
|
||||
const {
|
||||
data: limitedPackagesData,
|
||||
isLoading: isLimitedPackagesLoading,
|
||||
} = useGetLimitedPackages();
|
||||
useEffect(() => {
|
||||
if (packagesData?.response && limitedPackagesData?.response && agentConfigData?.item) {
|
||||
const allPackages = packagesData.response;
|
||||
const limitedPackages = limitedPackagesData.response;
|
||||
const usedLimitedPackages = (agentConfigData.item.package_configs as PackageConfig[])
|
||||
.map((packageConfig) => packageConfig.package?.name || '')
|
||||
.filter((pkgName) => limitedPackages.includes(pkgName));
|
||||
setPackages(allPackages.filter((pkg) => !usedLimitedPackages.includes(pkg.name)));
|
||||
}
|
||||
}, [packagesData, limitedPackagesData, agentConfigData]);
|
||||
|
||||
// Update parent agent config state
|
||||
useEffect(() => {
|
||||
|
@ -101,7 +121,7 @@ export const StepSelectPackage: React.FunctionComponent<{
|
|||
searchable
|
||||
allowExclusions={false}
|
||||
singleSelection={true}
|
||||
isLoading={isPackagesLoading}
|
||||
isLoading={isPackagesLoading || isLimitedPackagesLoading}
|
||||
options={packages.map(({ title, name, version, icons }) => {
|
||||
const pkgkey = `${name}-${version}`;
|
||||
return {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
export { getFlattenedObject } from '../../../../../../../src/core/public';
|
||||
|
||||
export {
|
||||
AgentStatusKueryHelper,
|
||||
agentConfigRouteService,
|
||||
packageConfigRouteService,
|
||||
dataStreamRouteService,
|
||||
|
@ -21,5 +22,6 @@ export {
|
|||
packageToPackageConfigInputs,
|
||||
storedPackageConfigsToAgentInputs,
|
||||
configToYaml,
|
||||
AgentStatusKueryHelper,
|
||||
isPackageLimited,
|
||||
doesAgentConfigAlreadyIncludePackage,
|
||||
} from '../../../../common';
|
||||
|
|
|
@ -24,6 +24,7 @@ export {
|
|||
// API schema - misc setup, status
|
||||
GetFleetStatusResponse,
|
||||
// API schemas - Agent Config
|
||||
GetAgentConfigsRequest,
|
||||
GetAgentConfigsResponse,
|
||||
GetAgentConfigsResponseItem,
|
||||
GetOneAgentConfigResponse,
|
||||
|
@ -92,6 +93,7 @@ export {
|
|||
ServiceName,
|
||||
GetCategoriesResponse,
|
||||
GetPackagesResponse,
|
||||
GetLimitedPackagesResponse,
|
||||
GetInfoResponse,
|
||||
InstallPackageResponse,
|
||||
DeletePackageResponse,
|
||||
|
|
|
@ -38,8 +38,12 @@ export const getAgentConfigsHandler: RequestHandler<
|
|||
TypeOf<typeof GetAgentConfigsRequestSchema.query>
|
||||
> = async (context, request, response) => {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
const { full: withPackageConfigs = false, ...restOfQuery } = request.query;
|
||||
try {
|
||||
const { items, total, page, perPage } = await agentConfigService.list(soClient, request.query);
|
||||
const { items, total, page, perPage } = await agentConfigService.list(soClient, {
|
||||
withPackageConfigs,
|
||||
...restOfQuery,
|
||||
});
|
||||
const body: GetAgentConfigsResponse = {
|
||||
items,
|
||||
total,
|
||||
|
@ -103,6 +107,7 @@ export const createAgentConfigHandler: RequestHandler<
|
|||
TypeOf<typeof CreateAgentConfigRequestSchema.body>
|
||||
> = async (context, request, response) => {
|
||||
const soClient = context.core.savedObjects.client;
|
||||
const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser;
|
||||
const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined;
|
||||
const withSysMonitoring = request.query.sys_monitoring ?? false;
|
||||
try {
|
||||
|
@ -128,15 +133,9 @@ export const createAgentConfigHandler: RequestHandler<
|
|||
if (withSysMonitoring && newSysPackageConfig !== undefined && agentConfig !== undefined) {
|
||||
newSysPackageConfig.config_id = agentConfig.id;
|
||||
newSysPackageConfig.namespace = agentConfig.namespace;
|
||||
const sysPackageConfig = await packageConfigService.create(soClient, newSysPackageConfig, {
|
||||
await packageConfigService.create(soClient, callCluster, newSysPackageConfig, {
|
||||
user,
|
||||
});
|
||||
|
||||
if (sysPackageConfig) {
|
||||
agentConfig = await agentConfigService.assignPackageConfigs(soClient, agentConfig.id, [
|
||||
sysPackageConfig.id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
const body: CreateAgentConfigResponse = {
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
*/
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { RequestHandler, CustomHttpResponseOptions } from 'src/core/server';
|
||||
import {
|
||||
GetInfoResponse,
|
||||
InstallPackageResponse,
|
||||
DeletePackageResponse,
|
||||
GetCategoriesResponse,
|
||||
GetPackagesResponse,
|
||||
GetLimitedPackagesResponse,
|
||||
} from '../../../common';
|
||||
import {
|
||||
GetPackagesRequestSchema,
|
||||
GetFileRequestSchema,
|
||||
|
@ -12,13 +20,6 @@ import {
|
|||
InstallPackageRequestSchema,
|
||||
DeletePackageRequestSchema,
|
||||
} from '../../types';
|
||||
import {
|
||||
GetInfoResponse,
|
||||
InstallPackageResponse,
|
||||
DeletePackageResponse,
|
||||
GetCategoriesResponse,
|
||||
GetPackagesResponse,
|
||||
} from '../../../common';
|
||||
import {
|
||||
getCategories,
|
||||
getPackages,
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
getPackageInfo,
|
||||
installPackage,
|
||||
removeInstallation,
|
||||
getLimitedPackages,
|
||||
} from '../../services/epm/packages';
|
||||
|
||||
export const getCategoriesHandler: RequestHandler = async (context, request, response) => {
|
||||
|
@ -69,6 +71,25 @@ export const getListHandler: RequestHandler<
|
|||
}
|
||||
};
|
||||
|
||||
export const getLimitedListHandler: RequestHandler = async (context, request, response) => {
|
||||
try {
|
||||
const savedObjectsClient = context.core.savedObjects.client;
|
||||
const res = await getLimitedPackages({ savedObjectsClient });
|
||||
const body: GetLimitedPackagesResponse = {
|
||||
response: res,
|
||||
success: true,
|
||||
};
|
||||
return response.ok({
|
||||
body,
|
||||
});
|
||||
} catch (e) {
|
||||
return response.customError({
|
||||
statusCode: 500,
|
||||
body: { message: e.message },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileHandler: RequestHandler<TypeOf<typeof GetFileRequestSchema.params>> = async (
|
||||
context,
|
||||
request,
|
||||
|
|
|
@ -8,6 +8,7 @@ import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants';
|
|||
import {
|
||||
getCategoriesHandler,
|
||||
getListHandler,
|
||||
getLimitedListHandler,
|
||||
getFileHandler,
|
||||
getInfoHandler,
|
||||
installPackageHandler,
|
||||
|
@ -40,6 +41,15 @@ export const registerRoutes = (router: IRouter) => {
|
|||
getListHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: EPM_API_ROUTES.LIMITED_LIST_PATTERN,
|
||||
validate: false,
|
||||
options: { tags: [`access:${PLUGIN_ID}`] },
|
||||
},
|
||||
getLimitedListHandler
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: EPM_API_ROUTES.FILEPATH_PATTERN,
|
||||
|
|
|
@ -25,7 +25,7 @@ jest.mock('../../services/package_config', (): {
|
|||
assignPackageStream: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)),
|
||||
buildPackageConfigFromPackage: jest.fn(),
|
||||
bulkCreate: jest.fn(),
|
||||
create: jest.fn((soClient, newData) =>
|
||||
create: jest.fn((soClient, callCluster, newData) =>
|
||||
Promise.resolve({
|
||||
...newData,
|
||||
id: '1',
|
||||
|
@ -213,7 +213,7 @@ describe('When calling package config', () => {
|
|||
const request = getCreateKibanaRequest();
|
||||
await routeHandler(context, request, response);
|
||||
expect(response.ok).toHaveBeenCalled();
|
||||
expect(packageConfigServiceMock.create.mock.calls[0][1]).toEqual({
|
||||
expect(packageConfigServiceMock.create.mock.calls[0][2]).toEqual({
|
||||
config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
|
||||
description: '',
|
||||
enabled: true,
|
||||
|
@ -294,7 +294,7 @@ describe('When calling package config', () => {
|
|||
const request = getCreateKibanaRequest();
|
||||
await routeHandler(context, request, response);
|
||||
expect(response.ok).toHaveBeenCalled();
|
||||
expect(packageConfigServiceMock.create.mock.calls[0][1]).toEqual({
|
||||
expect(packageConfigServiceMock.create.mock.calls[0][2]).toEqual({
|
||||
config_id: 'a5ca00c0-b30c-11ea-9732-1bb05811278c',
|
||||
description: '',
|
||||
enabled: true,
|
||||
|
|
|
@ -7,7 +7,7 @@ import { TypeOf } from '@kbn/config-schema';
|
|||
import Boom from 'boom';
|
||||
import { RequestHandler } from 'src/core/server';
|
||||
import { appContextService, packageConfigService } from '../../services';
|
||||
import { ensureInstalledPackage, getPackageInfo } from '../../services/epm/packages';
|
||||
import { getPackageInfo } from '../../services/epm/packages';
|
||||
import {
|
||||
GetPackageConfigsRequestSchema,
|
||||
GetOnePackageConfigRequestSchema,
|
||||
|
@ -106,26 +106,10 @@ export const createPackageConfigHandler: RequestHandler<
|
|||
newData = updatedNewData;
|
||||
}
|
||||
|
||||
// Make sure the associated package is installed
|
||||
if (newData.package?.name) {
|
||||
await ensureInstalledPackage({
|
||||
savedObjectsClient: soClient,
|
||||
pkgName: newData.package.name,
|
||||
callCluster,
|
||||
});
|
||||
const pkgInfo = await getPackageInfo({
|
||||
savedObjectsClient: soClient,
|
||||
pkgName: newData.package.name,
|
||||
pkgVersion: newData.package.version,
|
||||
});
|
||||
newData.inputs = (await packageConfigService.assignPackageStream(
|
||||
pkgInfo,
|
||||
newData.inputs
|
||||
)) as TypeOf<typeof CreatePackageConfigRequestSchema.body>['inputs'];
|
||||
}
|
||||
|
||||
// Create package config
|
||||
const packageConfig = await packageConfigService.create(soClient, newData, { user });
|
||||
const packageConfig = await packageConfigService.create(soClient, callCluster, newData, {
|
||||
user,
|
||||
});
|
||||
const body: CreatePackageConfigResponse = { item: packageConfig, success: true };
|
||||
return response.ok({
|
||||
body,
|
||||
|
|
|
@ -141,11 +141,20 @@ class AgentConfigService {
|
|||
|
||||
public async list(
|
||||
soClient: SavedObjectsClientContract,
|
||||
options: ListWithKuery
|
||||
options: ListWithKuery & {
|
||||
withPackageConfigs?: boolean;
|
||||
}
|
||||
): Promise<{ items: AgentConfig[]; total: number; page: number; perPage: number }> {
|
||||
const { page = 1, perPage = 20, sortField = 'updated_at', sortOrder = 'desc', kuery } = options;
|
||||
const {
|
||||
page = 1,
|
||||
perPage = 20,
|
||||
sortField = 'updated_at',
|
||||
sortOrder = 'desc',
|
||||
kuery,
|
||||
withPackageConfigs = false,
|
||||
} = options;
|
||||
|
||||
const agentConfigs = await soClient.find<AgentConfigSOAttributes>({
|
||||
const agentConfigsSO = await soClient.find<AgentConfigSOAttributes>({
|
||||
type: SAVED_OBJECT_TYPE,
|
||||
sortField,
|
||||
sortOrder,
|
||||
|
@ -160,12 +169,29 @@ class AgentConfigService {
|
|||
: undefined,
|
||||
});
|
||||
|
||||
const agentConfigs = await Promise.all(
|
||||
agentConfigsSO.saved_objects.map(async (agentConfigSO) => {
|
||||
const agentConfig = {
|
||||
id: agentConfigSO.id,
|
||||
...agentConfigSO.attributes,
|
||||
};
|
||||
if (withPackageConfigs) {
|
||||
const agentConfigWithPackageConfigs = await this.get(
|
||||
soClient,
|
||||
agentConfigSO.id,
|
||||
withPackageConfigs
|
||||
);
|
||||
if (agentConfigWithPackageConfigs) {
|
||||
agentConfig.package_configs = agentConfigWithPackageConfigs.package_configs;
|
||||
}
|
||||
}
|
||||
return agentConfig;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
items: agentConfigs.saved_objects.map<AgentConfig>((agentConfigSO) => ({
|
||||
id: agentConfigSO.id,
|
||||
...agentConfigSO.attributes,
|
||||
})),
|
||||
total: agentConfigs.total,
|
||||
items: agentConfigs,
|
||||
total: agentConfigsSO.total,
|
||||
page,
|
||||
perPage,
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'src/core/server';
|
||||
import { isPackageLimited } from '../../../../common';
|
||||
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
|
||||
import { Installation, InstallationStatus, PackageInfo, KibanaAssetType } from '../../../types';
|
||||
import * as Registry from '../registry';
|
||||
|
@ -49,6 +50,28 @@ export async function getPackages(
|
|||
return packageList;
|
||||
}
|
||||
|
||||
// Get package names for packages which cannot have more than one package config on an agent config
|
||||
// Assume packages only export one config template for now
|
||||
export async function getLimitedPackages(options: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}): Promise<string[]> {
|
||||
const { savedObjectsClient } = options;
|
||||
const allPackages = await getPackages({ savedObjectsClient });
|
||||
const installedPackages = allPackages.filter(
|
||||
(pkg) => (pkg.status = InstallationStatus.installed)
|
||||
);
|
||||
const installedPackagesInfo = await Promise.all(
|
||||
installedPackages.map((pkgInstall) => {
|
||||
return getPackageInfo({
|
||||
savedObjectsClient,
|
||||
pkgName: pkgInstall.name,
|
||||
pkgVersion: pkgInstall.version,
|
||||
});
|
||||
})
|
||||
);
|
||||
return installedPackagesInfo.filter((pkgInfo) => isPackageLimited).map((pkgInfo) => pkgInfo.name);
|
||||
}
|
||||
|
||||
export async function getPackageSavedObjects(savedObjectsClient: SavedObjectsClientContract) {
|
||||
return savedObjectsClient.find<Installation>({
|
||||
type: PACKAGES_SAVED_OBJECT_TYPE,
|
||||
|
|
|
@ -11,8 +11,7 @@ import {
|
|||
Installation,
|
||||
InstallationStatus,
|
||||
KibanaAssetType,
|
||||
} from '../../../../common/types/models/epm';
|
||||
|
||||
} from '../../../types';
|
||||
export {
|
||||
getCategories,
|
||||
getFile,
|
||||
|
@ -20,6 +19,7 @@ export {
|
|||
getInstallation,
|
||||
getPackageInfo,
|
||||
getPackages,
|
||||
getLimitedPackages,
|
||||
SearchParams,
|
||||
} from './get';
|
||||
|
||||
|
|
|
@ -7,24 +7,27 @@ import { SavedObjectsClientContract } from 'src/core/server';
|
|||
import { AuthenticatedUser } from '../../../security/server';
|
||||
import {
|
||||
DeletePackageConfigsResponse,
|
||||
packageToPackageConfig,
|
||||
PackageConfigInput,
|
||||
PackageConfigInputStream,
|
||||
PackageInfo,
|
||||
ListWithKuery,
|
||||
packageToPackageConfig,
|
||||
isPackageLimited,
|
||||
doesAgentConfigAlreadyIncludePackage,
|
||||
} from '../../common';
|
||||
import { PACKAGE_CONFIG_SAVED_OBJECT_TYPE } from '../constants';
|
||||
import {
|
||||
NewPackageConfig,
|
||||
UpdatePackageConfig,
|
||||
PackageConfig,
|
||||
ListWithKuery,
|
||||
PackageConfigSOAttributes,
|
||||
RegistryPackage,
|
||||
CallESAsCurrentUser,
|
||||
} from '../types';
|
||||
import { agentConfigService } from './agent_config';
|
||||
import { outputService } from './output';
|
||||
import * as Registry from './epm/registry';
|
||||
import { getPackageInfo, getInstallation } from './epm/packages';
|
||||
import { getPackageInfo, getInstallation, ensureInstalledPackage } from './epm/packages';
|
||||
import { getAssetsData } from './epm/packages/assets';
|
||||
import { createStream } from './epm/agent/agent';
|
||||
|
||||
|
@ -37,9 +40,39 @@ function getDataset(st: string) {
|
|||
class PackageConfigService {
|
||||
public async create(
|
||||
soClient: SavedObjectsClientContract,
|
||||
callCluster: CallESAsCurrentUser,
|
||||
packageConfig: NewPackageConfig,
|
||||
options?: { id?: string; user?: AuthenticatedUser }
|
||||
): Promise<PackageConfig> {
|
||||
// Make sure the associated package is installed
|
||||
if (packageConfig.package?.name) {
|
||||
const [, pkgInfo] = await Promise.all([
|
||||
ensureInstalledPackage({
|
||||
savedObjectsClient: soClient,
|
||||
pkgName: packageConfig.package.name,
|
||||
callCluster,
|
||||
}),
|
||||
getPackageInfo({
|
||||
savedObjectsClient: soClient,
|
||||
pkgName: packageConfig.package.name,
|
||||
pkgVersion: packageConfig.package.version,
|
||||
}),
|
||||
]);
|
||||
|
||||
// Check if it is a limited package, and if so, check that the corresponding agent config does not
|
||||
// already contain a package config for this package
|
||||
if (isPackageLimited(pkgInfo)) {
|
||||
const agentConfig = await agentConfigService.get(soClient, packageConfig.config_id, true);
|
||||
if (agentConfig && doesAgentConfigAlreadyIncludePackage(agentConfig, pkgInfo.name)) {
|
||||
throw new Error(
|
||||
`Unable to create package config. Package '${pkgInfo.name}' already exists on this agent config.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
packageConfig.inputs = await this.assignPackageStream(pkgInfo, packageConfig.inputs);
|
||||
}
|
||||
|
||||
const isoDate = new Date().toISOString();
|
||||
const newSo = await soClient.create<PackageConfigSOAttributes>(
|
||||
SAVED_OBJECT_TYPE,
|
||||
|
|
|
@ -113,6 +113,7 @@ export async function setupIngestManager(
|
|||
if (!isInstalled) {
|
||||
await addPackageToConfig(
|
||||
soClient,
|
||||
callCluster,
|
||||
installedPackage,
|
||||
configWithPackageConfigs,
|
||||
defaultOutput
|
||||
|
@ -192,6 +193,7 @@ function generateRandomPassword() {
|
|||
|
||||
async function addPackageToConfig(
|
||||
soClient: SavedObjectsClientContract,
|
||||
callCluster: CallESAsCurrentUser,
|
||||
packageToInstall: Installation,
|
||||
config: AgentConfig,
|
||||
defaultOutput: Output
|
||||
|
@ -208,10 +210,6 @@ async function addPackageToConfig(
|
|||
defaultOutput.id,
|
||||
config.namespace
|
||||
);
|
||||
newPackageConfig.inputs = await packageConfigService.assignPackageStream(
|
||||
packageInfo,
|
||||
newPackageConfig.inputs
|
||||
);
|
||||
|
||||
await packageConfigService.create(soClient, newPackageConfig);
|
||||
await packageConfigService.create(soClient, callCluster, newPackageConfig);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,9 @@ import { NewAgentConfigSchema } from '../models';
|
|||
import { ListWithKuerySchema } from './index';
|
||||
|
||||
export const GetAgentConfigsRequestSchema = {
|
||||
query: ListWithKuerySchema,
|
||||
query: ListWithKuerySchema.extends({
|
||||
full: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
};
|
||||
|
||||
export const GetOneAgentConfigRequestSchema = {
|
||||
|
|
|
@ -1839,6 +1839,12 @@
|
|||
"config_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"created_by": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
|
@ -1847,6 +1853,7 @@
|
|||
},
|
||||
"inputs": {
|
||||
"type": "nested",
|
||||
"enabled": false,
|
||||
"properties": {
|
||||
"config": {
|
||||
"type": "flattened"
|
||||
|
@ -1854,20 +1861,24 @@
|
|||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"processors": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"streams": {
|
||||
"type": "nested",
|
||||
"properties": {
|
||||
"agent_stream": {
|
||||
"compiled_stream": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"config": {
|
||||
"type": "flattened"
|
||||
},
|
||||
"dataset": {
|
||||
"type": "keyword"
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
|
@ -1875,9 +1886,6 @@
|
|||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"processors": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"vars": {
|
||||
"type": "flattened"
|
||||
}
|
||||
|
@ -1915,6 +1923,12 @@
|
|||
},
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"updated_by": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,8 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
import { warnAndSkipTest } from '../helpers';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { warnAndSkipTest } from '../../helpers';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
|
@ -13,7 +13,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const dockerServers = getService('dockerServers');
|
||||
|
||||
const server = dockerServers.get('registry');
|
||||
describe('package file', () => {
|
||||
describe('EPM - package file', () => {
|
||||
it('fetches a .png screenshot image', async function () {
|
||||
if (server.enabled) {
|
||||
await supertest
|
|
@ -5,10 +5,10 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
describe('ilm', () => {
|
||||
describe('EPM - ilm', () => {
|
||||
it('setup policy', async () => {
|
||||
const policyName = 'foo';
|
||||
const es = getService('es');
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
import { warnAndSkipTest } from '../helpers';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { warnAndSkipTest } from '../../helpers';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
import { warnAndSkipTest } from '../helpers';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { warnAndSkipTest } from '../../helpers';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
|
@ -18,7 +18,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
// because `this` has to point to the Mocha context
|
||||
// see https://mochajs.org/#arrow-functions
|
||||
|
||||
describe('list', async function () {
|
||||
describe('EPM - list', async function () {
|
||||
it('lists all packages from the registry', async function () {
|
||||
if (server.enabled) {
|
||||
const fetchPackageList = async () => {
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
import { getTemplate } from '../../../plugins/ingest_manager/server/services/epm/elasticsearch/template/template';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { getTemplate } from '../../../../plugins/ingest_manager/server/services/epm/elasticsearch/template/template';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const indexPattern = 'foo';
|
||||
|
@ -20,7 +20,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
},
|
||||
};
|
||||
// This test was inspired by https://github.com/elastic/kibana/blob/master/x-pack/test/api_integration/apis/monitoring/common/mappings_exist.js
|
||||
describe('template', async () => {
|
||||
describe('EPM - template', async () => {
|
||||
it('can be loaded', async () => {
|
||||
const template = getTemplate({
|
||||
type: 'logs',
|
|
@ -5,12 +5,17 @@
|
|||
*/
|
||||
|
||||
export default function ({ loadTestFile }) {
|
||||
describe('EPM Endpoints', function () {
|
||||
describe('Ingest Manager Endpoints', function () {
|
||||
this.tags('ciGroup7');
|
||||
loadTestFile(require.resolve('./list'));
|
||||
loadTestFile(require.resolve('./file'));
|
||||
//loadTestFile(require.resolve('./template'));
|
||||
loadTestFile(require.resolve('./ilm'));
|
||||
loadTestFile(require.resolve('./install'));
|
||||
|
||||
// EPM
|
||||
loadTestFile(require.resolve('./epm/list'));
|
||||
loadTestFile(require.resolve('./epm/file'));
|
||||
//loadTestFile(require.resolve('./epm/template'));
|
||||
loadTestFile(require.resolve('./epm/ilm'));
|
||||
loadTestFile(require.resolve('./epm/install'));
|
||||
|
||||
// Package configs
|
||||
loadTestFile(require.resolve('./package_config/create'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { warnAndSkipTest } from '../../helpers';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const log = getService('log');
|
||||
const supertest = getService('supertest');
|
||||
const dockerServers = getService('dockerServers');
|
||||
|
||||
const server = dockerServers.get('registry');
|
||||
// use function () {} and not () => {} here
|
||||
// because `this` has to point to the Mocha context
|
||||
// see https://mochajs.org/#arrow-functions
|
||||
|
||||
describe('Package Config - create', async function () {
|
||||
let agentConfigId: string;
|
||||
|
||||
before(async function () {
|
||||
const { body: agentConfigResponse } = await supertest
|
||||
.post(`/api/ingest_manager/agent_configs`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'Test config',
|
||||
namespace: 'default',
|
||||
});
|
||||
agentConfigId = agentConfigResponse.item.id;
|
||||
});
|
||||
|
||||
it('should work with valid values', async function () {
|
||||
if (server.enabled) {
|
||||
const { body: apiResponse } = await supertest
|
||||
.post(`/api/ingest_manager/package_configs`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'filetest-1',
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
config_id: agentConfigId,
|
||||
enabled: true,
|
||||
output_id: '',
|
||||
inputs: [],
|
||||
package: {
|
||||
name: 'filetest',
|
||||
title: 'For File Tests',
|
||||
version: '0.1.0',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(apiResponse.success).to.be(true);
|
||||
} else {
|
||||
warnAndSkipTest(this, log);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return a 400 with an invalid namespace', async function () {
|
||||
if (server.enabled) {
|
||||
await supertest
|
||||
.post(`/api/ingest_manager/package_configs`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'filetest-1',
|
||||
description: '',
|
||||
namespace: '',
|
||||
config_id: agentConfigId,
|
||||
enabled: true,
|
||||
output_id: '',
|
||||
inputs: [],
|
||||
package: {
|
||||
name: 'filetest',
|
||||
title: 'For File Tests',
|
||||
version: '0.1.0',
|
||||
},
|
||||
})
|
||||
.expect(400);
|
||||
} else {
|
||||
warnAndSkipTest(this, log);
|
||||
}
|
||||
});
|
||||
|
||||
it('should not allow multiple limited packages on the same agent config', async function () {
|
||||
if (server.enabled) {
|
||||
await supertest
|
||||
.post(`/api/ingest_manager/package_configs`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'endpoint-1',
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
config_id: agentConfigId,
|
||||
enabled: true,
|
||||
output_id: '',
|
||||
inputs: [],
|
||||
package: {
|
||||
name: 'endpoint',
|
||||
title: 'Endpoint',
|
||||
version: '0.8.0',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
await supertest
|
||||
.post(`/api/ingest_manager/package_configs`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'endpoint-2',
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
config_id: agentConfigId,
|
||||
enabled: true,
|
||||
output_id: '',
|
||||
inputs: [],
|
||||
package: {
|
||||
name: 'endpoint',
|
||||
title: 'Endpoint',
|
||||
version: '0.8.0',
|
||||
},
|
||||
})
|
||||
.expect(500);
|
||||
} else {
|
||||
warnAndSkipTest(this, log);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue