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

[Fleet] add ilm policy per data stream #85492

Merged
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export enum ElasticsearchAssetType {
indexTemplate = 'index_template',
ilmPolicy = 'ilm_policy',
transform = 'transform',
dataStreamIlmPolicy = 'data_stream_ilm_policy',
}

export type DataType = typeof dataTypes;
Expand Down Expand Up @@ -207,6 +208,7 @@ export type ElasticsearchAssetTypeToParts = Record<

export interface RegistryDataStream {
type: string;
ilm_policy?: string;
dataset: string;
title: string;
release: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const AssetTitleMap: Record<AssetType, string> = {
visualization: 'Visualization',
input: 'Agent input',
map: 'Map',
data_stream_ilm_policy: 'Data Stream ILM Policy',
};

export const ServiceTitleMap: Record<Extract<ServiceName, 'kibana'>, string> = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* 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 { SavedObjectsClientContract } from 'kibana/server';
import {
ElasticsearchAssetType,
EsAssetReference,
InstallablePackage,
RegistryDataStream,
} from '../../../../../common/types/models';
import { CallESAsCurrentUser } from '../../../../types';
import { getInstallation } from '../../packages';
import { deleteIlmRefs, deleteIlms } from './remove';
import { saveInstalledEsRefs } from '../../packages/install';
import { getAsset } from '../transform/common';

interface IlmInstallation {
installationName: string;
content: string;
}

interface IlmPathDataset {
path: string;
dataStream: RegistryDataStream;
}

export const installIlmForDataStream = async (
registryPackage: InstallablePackage,
paths: string[],
callCluster: CallESAsCurrentUser,
savedObjectsClient: SavedObjectsClientContract
) => {
const installation = await getInstallation({ savedObjectsClient, pkgName: registryPackage.name });
let previousInstalledIlmEsAssets: EsAssetReference[] = [];
if (installation) {
previousInstalledIlmEsAssets = installation.installed_es.filter(
({ type, id }) => type === ElasticsearchAssetType.dataStreamIlmPolicy
);
}

// delete all previous ilm
await deleteIlms(
callCluster,
previousInstalledIlmEsAssets.map((asset) => asset.id)
);
// install the latest dataset
const dataStreams = registryPackage.data_streams;
if (!dataStreams?.length) return [];
const dataStreamIlmPaths = paths.filter((path) => isDataStreamIlm(path));
let installedIlms: EsAssetReference[] = [];
if (dataStreamIlmPaths.length > 0) {
const ilmPathDatasets = dataStreams.reduce<IlmPathDataset[]>((acc, dataStream) => {
dataStreamIlmPaths.forEach((path) => {
if (isDatasetIlm(path, dataStream.path)) {
acc.push({ path, dataStream });
}
});
return acc;
}, []);

const ilmRefs = ilmPathDatasets.reduce<EsAssetReference[]>((acc, ilmPathDataset) => {
if (ilmPathDataset) {
acc.push({
id: getIlmNameForInstallation(ilmPathDataset),
type: ElasticsearchAssetType.dataStreamIlmPolicy,
});
}
return acc;
}, []);

await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, ilmRefs);

const ilmInstallations: IlmInstallation[] = ilmPathDatasets.map(
(ilmPathDataset: IlmPathDataset) => {
return {
installationName: getIlmNameForInstallation(ilmPathDataset),
content: getAsset(ilmPathDataset.path).toString('utf-8'),
};
}
);

const installationPromises = ilmInstallations.map(async (ilmInstallation) => {
return handleIlmInstall({ callCluster, ilmInstallation });
});

installedIlms = await Promise.all(installationPromises).then((results) => results.flat());
}

if (previousInstalledIlmEsAssets.length > 0) {
const currentInstallation = await getInstallation({
savedObjectsClient,
pkgName: registryPackage.name,
});

// remove the saved object reference
await deleteIlmRefs(
savedObjectsClient,
currentInstallation?.installed_es || [],
registryPackage.name,
previousInstalledIlmEsAssets.map((asset) => asset.id),
installedIlms.map((installed) => installed.id)
);
}
return installedIlms;
};

async function handleIlmInstall({
callCluster,
ilmInstallation,
}: {
callCluster: CallESAsCurrentUser;
ilmInstallation: IlmInstallation;
}): Promise<EsAssetReference> {
await callCluster('transport.request', {
method: 'PUT',
path: `/_ilm/policy/${ilmInstallation.installationName}`,
body: ilmInstallation.content,
});

return { id: ilmInstallation.installationName, type: ElasticsearchAssetType.dataStreamIlmPolicy };
}

const isDataStreamIlm = (path: string) => {
return new RegExp('(?<package>.*)/data_stream/(?<dataset>.*)/elasticsearch/ilm/*.*').test(path);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Do we need the ?<package> or the ?<dataset> ? I believe those are so we can extract out those specific fields.

Do we need to escape all the /?
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions

Similarly, if you're writing a regular expression literal and need to match a slash ("/"), you need to escape that (otherwise, it terminates the pattern). For instance, to search for the string "/example/" followed by one or more alphabetic characters, you'd use //example/[a-z]+/i—the backslashes before each slash make them literal.

I was playing around with it here:
https://regex101.com/r/GLnjJN/1
https://regex101.com/r/GLnjJN/2

I think we might need one of these:

(?<package>.*)\/data_stream\/(?<dataset>.*)\/elasticsearch\/ilm\/.*
.*\/data_stream\/.*\/elasticsearch\/ilm\/.*

Copy link
Member

Choose a reason for hiding this comment

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

Do we need? No, probably not. (?<package>.*) vs .* is adding a named capture group.

It doesn't look like we access or use the groups, so that's unnecessary. Adding the label, though, somewhat acts as in-regex-documentation of what we're looking at. I do like that.

We do not need to escape the slashes here because it is a new Regexp constructor using a string. Not using slash-literals.

};

const isDatasetIlm = (path: string, datasetName: string) => {
return new RegExp(`(?<package>.*)/data_stream\\/${datasetName}/elasticsearch/ilm/*.*`).test(path);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same questions about groups and escaping as above

};

const getIlmNameForInstallation = (ilmPathDataset: IlmPathDataset) => {
const filename = ilmPathDataset?.path.split('/')?.pop()?.split('.')[0];
return `${ilmPathDataset.dataStream.type}-${ilmPathDataset.dataStream.package}.${ilmPathDataset.dataStream.path}-${filename}`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { SavedObjectsClientContract } from 'kibana/server';
import { CallESAsCurrentUser, ElasticsearchAssetType, EsAssetReference } from '../../../../types';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants';

export const deleteIlms = async (callCluster: CallESAsCurrentUser, ilmPolicyIds: string[]) => {
await Promise.all(
ilmPolicyIds.map(async (ilmPolicyId) => {
await callCluster('transport.request', {
method: 'DELETE',
path: `_ilm/policy/${ilmPolicyId}`,
ignore: [404, 400],
});
})
);
};

export const deleteIlmRefs = async (
savedObjectsClient: SavedObjectsClientContract,
installedEsAssets: EsAssetReference[],
pkgName: string,
installedEsIdToRemove: string[],
currentInstalledEsIlmIds: string[]
) => {
const seen = new Set<string>();
const filteredAssets = installedEsAssets.filter(({ type, id }) => {
if (type !== ElasticsearchAssetType.dataStreamIlmPolicy) return true;
const add =
(currentInstalledEsIlmIds.includes(id) || !installedEsIdToRemove.includes(id)) &&
!seen.has(id);
seen.add(id);
return add;
});
return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, {
installed_es: filteredAssets,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ export async function installTemplate({
pipelineName,
packageName,
composedOfTemplates,
ilmPolicy: dataStream.ilm_policy,
});

// TODO: Check return values for errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,24 @@ export function getTemplate({
pipelineName,
packageName,
composedOfTemplates,
ilmPolicy,
}: {
type: string;
templateName: string;
mappings: IndexTemplateMappings;
pipelineName?: string | undefined;
packageName: string;
composedOfTemplates: string[];
ilmPolicy?: string | undefined;
}): IndexTemplate {
const template = getBaseTemplate(type, templateName, mappings, packageName, composedOfTemplates);
const template = getBaseTemplate(
type,
templateName,
mappings,
packageName,
composedOfTemplates,
ilmPolicy
);
if (pipelineName) {
template.template.settings.index.default_pipeline = pipelineName;
}
Expand Down Expand Up @@ -253,7 +262,8 @@ function getBaseTemplate(
templateName: string,
mappings: IndexTemplateMappings,
packageName: string,
composedOfTemplates: string[]
composedOfTemplates: string[],
ilmPolicy?: string | undefined
): IndexTemplate {
// Meta information to identify Ingest Manager's managed templates and indices
const _meta = {
Expand All @@ -277,7 +287,7 @@ function getBaseTemplate(
index: {
// ILM Policy must be added here, for now point to the default global ILM policy name
lifecycle: {
name: type,
name: ilmPolicy ? ilmPolicy : type,
},
// What should be our default for the compression?
codec: 'best_compression',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { updateCurrentWriteIndices } from '../elasticsearch/template/template';
import { deleteKibanaSavedObjectsAssets } from './remove';
import { installTransform } from '../elasticsearch/transform/install';
import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install';
import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install';
Copy link
Contributor

Choose a reason for hiding this comment

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

is this file missing from the commit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, updated

import { saveArchiveEntries } from '../archive/storage';

// this is only exported for testing
Expand Down Expand Up @@ -131,6 +132,13 @@ export async function _installPackage({
// per data stream and we should then save them
await installILMPolicy(paths, callCluster);

const installedDataStreamIlm = await installIlmForDataStream(
packageInfo,
paths,
callCluster,
savedObjectsClient
);

// installs versionized pipelines without removing currently installed ones
const installedPipelines = await installPipelines(
packageInfo,
Expand Down Expand Up @@ -209,6 +217,7 @@ export async function _installPackage({
return [
...installedKibanaAssetsRefs,
...installedPipelines,
...installedDataStreamIlm,
...installedTemplateRefs,
...installedTransforms,
];
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/fleet/server/services/epm/packages/remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { deleteTransforms } from '../elasticsearch/transform/remove';
import { packagePolicyService, appContextService } from '../..';
import { splitPkgKey } from '../registry';
import { deletePackageCache } from '../archive';
import { deleteIlms } from '../elasticsearch/datastream_ilm/remove';
Copy link
Contributor

Choose a reason for hiding this comment

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

same here, is this file missing from the commit?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, updated

import { removeArchiveEntries } from '../archive/storage';

export async function removeInstallation(options: {
Expand Down Expand Up @@ -93,6 +94,8 @@ function deleteESAssets(installedObjects: EsAssetReference[], callCluster: CallE
return deleteTemplate(callCluster, id);
} else if (assetType === ElasticsearchAssetType.transform) {
return deleteTransforms(callCluster, [id]);
} else if (assetType === ElasticsearchAssetType.dataStreamIlmPolicy) {
return deleteIlms(callCluster, [id]);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,14 @@ const expectAssetsInstalled = ({
},
],
installed_es: [
{
id: 'logs-all_assets.test_logs-all_assets',
type: 'data_stream_ilm_policy',
},
{
id: 'metrics-all_assets.test_metrics-all_assets',
type: 'data_stream_ilm_policy',
},
{
id: 'logs-all_assets.test_logs',
type: 'index_template',
Expand Down Expand Up @@ -496,6 +504,7 @@ const expectAssetsInstalled = ({
{ id: '96c6eb85-fe2e-56c6-84be-5fda976796db', type: 'epm-packages-assets' },
{ id: '2d73a161-fa69-52d0-aa09-1bdc691b95bb', type: 'epm-packages-assets' },
{ id: '0a00c2d2-ce63-5b9c-9aa0-0cf1938f7362', type: 'epm-packages-assets' },
{ id: '691f0505-18c5-57a6-9f40-06e8affbdf7a', type: 'epm-packages-assets' },
{ id: 'b36e6dd0-58f7-5dd0-a286-8187e4019274', type: 'epm-packages-assets' },
{ id: 'f839c76e-d194-555a-90a1-3265a45789e4', type: 'epm-packages-assets' },
{ id: '9af7bbb3-7d8a-50fa-acc9-9dde6f5efca2', type: 'epm-packages-assets' },
Expand Down
4 changes: 4 additions & 0 deletions x-pack/test/fleet_api_integration/apis/epm/update_assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ export default function (providerContext: FtrProviderContext) {
},
],
installed_es: [
{
id: 'logs-all_assets.test_logs-all_assets',
type: 'data_stream_ilm_policy',
},
{
id: 'logs-all_assets.test_logs-0.2.0',
type: 'ingest_pipeline',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_size": "50gb",
"max_age": "30d"
}
}
}
}
}
}