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

[EPM] /packages/{package} endpoint to support upgrades #63629

Merged
9 changes: 9 additions & 0 deletions x-pack/plugins/ingest_manager/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,10 +256,19 @@ export enum DefaultPackages {
endpoint = 'endpoint',
}

export interface IndexTemplateMappings {
properties: any;
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 really any? e.g. number, string, etc?

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 actually just renamed Mappings to IndexTemplateMappings that @skh created. Trying to type all the mappings and settings is going to be pretty time consuming.

}

export interface IndexTemplate {
order: number;
index_patterns: string[];
settings: any;
mappings: object;
aliases: object;
}

export interface TemplateRef {
templateName: string;
indexTemplate: IndexTemplate;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import {
AssetReference,
Dataset,
RegistryPackage,
IngestAssetType,
ElasticsearchAssetType,
} from '../../../../types';
import { Dataset, RegistryPackage, ElasticsearchAssetType, TemplateRef } from '../../../../types';
import { CallESAsCurrentUser } from '../../../../types';
import { Field, loadFieldsFromYaml, processFields } from '../../fields/field';
import { getPipelineNameForInstallation } from '../ingest_pipeline/install';
Expand All @@ -22,15 +16,15 @@ export const installTemplates = async (
callCluster: CallESAsCurrentUser,
pkgName: string,
pkgVersion: string
) => {
): Promise<TemplateRef[]> => {
// install any pre-built index template assets,
// atm, this is only the base package's global template
installPreBuiltTemplates(pkgName, pkgVersion, callCluster);

// build templates per dataset from yml files
const datasets = registryPackage.datasets;
if (datasets) {
const templates = datasets.reduce<Array<Promise<AssetReference>>>((acc, dataset) => {
const installTemplatePromises = datasets.reduce<Array<Promise<TemplateRef>>>((acc, dataset) => {
acc.push(
installTemplateForDataset({
pkg: registryPackage,
Expand All @@ -40,7 +34,9 @@ export const installTemplates = async (
);
return acc;
}, []);
return Promise.all(templates).then(results => results.flat());

const res = await Promise.all(installTemplatePromises);
return res.flat();
}
return [];
};
Expand Down Expand Up @@ -84,7 +80,7 @@ export async function installTemplateForDataset({
pkg: RegistryPackage;
callCluster: CallESAsCurrentUser;
dataset: Dataset;
}): Promise<AssetReference> {
}): Promise<TemplateRef> {
const fields = await loadFieldsFromYaml(pkg, dataset.path);
return installTemplate({
callCluster,
Expand All @@ -104,7 +100,7 @@ export async function installTemplate({
fields: Field[];
dataset: Dataset;
packageVersion: string;
}): Promise<AssetReference> {
}): Promise<TemplateRef> {
const mappings = generateMappings(processFields(fields));
const templateName = generateTemplateName(dataset);
let pipelineName;
Expand All @@ -122,6 +118,8 @@ export async function installTemplate({
body: template,
});

// The id of a template is its name
return { id: templateName, type: IngestAssetType.IndexTemplate };
return {
templateName,
indexTemplate: template,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,30 @@
*/

import { Field, Fields } from '../../fields/field';
import { Dataset, IndexTemplate } from '../../../../types';
import {
Dataset,
CallESAsCurrentUser,
TemplateRef,
IndexTemplate,
IndexTemplateMappings,
} from '../../../../types';
import { getDatasetAssetBaseName } from '../index';

interface Properties {
[key: string]: any;
}
interface Mappings {
properties: any;
}

interface Mapping {
[key: string]: any;
}

interface MultiFields {
[key: string]: object;
}

export interface IndexTemplateMapping {
[key: string]: any;
}
export interface CurrentIndex {
indexName: string;
indexTemplate: IndexTemplate;
}
const DEFAULT_SCALING_FACTOR = 1000;
const DEFAULT_IGNORE_ABOVE = 1024;

Expand All @@ -34,7 +40,7 @@ const DEFAULT_IGNORE_ABOVE = 1024;
export function getTemplate(
type: string,
templateName: string,
mappings: Mappings,
mappings: IndexTemplateMappings,
pipelineName?: string | undefined
): IndexTemplate {
const template = getBaseTemplate(type, templateName, mappings);
Expand All @@ -52,7 +58,7 @@ export function getTemplate(
*
* @param fields
*/
export function generateMappings(fields: Field[]): Mappings {
export function generateMappings(fields: Field[]): IndexTemplateMappings {
const props: Properties = {};
// TODO: this can happen when the fields property in fields.yml is present but empty
// Maybe validation should be moved to fields/field.ts
Expand Down Expand Up @@ -140,8 +146,8 @@ function generateMultiFields(fields: Fields): MultiFields {
return multiFields;
}

function generateKeywordMapping(field: Field): Mapping {
const mapping: Mapping = {
function generateKeywordMapping(field: Field): IndexTemplateMapping {
const mapping: IndexTemplateMapping = {
ignore_above: DEFAULT_IGNORE_ABOVE,
};
if (field.ignore_above) {
Expand All @@ -150,8 +156,8 @@ function generateKeywordMapping(field: Field): Mapping {
return mapping;
}

function generateTextMapping(field: Field): Mapping {
const mapping: Mapping = {};
function generateTextMapping(field: Field): IndexTemplateMapping {
const mapping: IndexTemplateMapping = {};
if (field.analyzer) {
mapping.analyzer = field.analyzer;
}
Expand Down Expand Up @@ -200,7 +206,11 @@ export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record
return patterns;
}

function getBaseTemplate(type: string, templateName: string, mappings: Mappings): IndexTemplate {
function getBaseTemplate(
type: string,
templateName: string,
mappings: IndexTemplateMappings
): IndexTemplate {
return {
// We need to decide which order we use for the templates
order: 1,
Expand Down Expand Up @@ -234,10 +244,6 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings)
},
},
mappings: {
// To be filled with interesting information about this specific index
_meta: {
package: 'foo',
},
// All the dynamic field mappings
dynamic_templates: [
// This makes sure all mappings are keywords by default
Expand All @@ -261,3 +267,94 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings)
aliases: {},
};
}

export const updateCurrentWriteIndices = async (
callCluster: CallESAsCurrentUser,
templates: TemplateRef[]
): Promise<void> => {
if (!templates) return;

const allIndices = await queryIndicesFromTemplates(callCluster, templates);
return updateAllIndices(allIndices, callCluster);
};

const queryIndicesFromTemplates = async (
callCluster: CallESAsCurrentUser,
templates: TemplateRef[]
): Promise<CurrentIndex[]> => {
const indexPromises = templates.map(template => {
return createIndexFromNamespace(callCluster, template);
});
const indexObjects = await Promise.all(indexPromises);
return indexObjects.filter(item => item !== undefined).flat();
};

const createIndexFromNamespace = async (
callCluster: CallESAsCurrentUser,
template: TemplateRef
): Promise<CurrentIndex[] | undefined> => {
const { templateName, indexTemplate } = template;
const res = await callCluster('search', getIndexByNamespaceQuery(templateName));
const namespaces: any[] = res?.aggregations?.streams.buckets;
if (namespaces) {
return namespaces.map(namespace => ({
indexName: `${templateName}-${namespace.key}`,
indexTemplate,
}));
}
};

const updateAllIndices = async (
indexNameWithTemplates: CurrentIndex[],
callCluster: CallESAsCurrentUser
): Promise<void> => {
const updateIndexPromises = indexNameWithTemplates.map(({ indexName, indexTemplate }) => {
return updateExistingIndex({ indexName, callCluster, indexTemplate });
});
await Promise.all(updateIndexPromises);
};
const updateExistingIndex = async ({
indexName,
callCluster,
indexTemplate,
}: {
indexName: string;
callCluster: CallESAsCurrentUser;
indexTemplate: IndexTemplate;
}) => {
const { settings, mappings } = indexTemplate;
// try to update the mappings first
// for now we assume updates are compatible
try {
await callCluster('indices.putMapping', {
index: indexName,
body: mappings,
});
} catch (err) {
throw new Error('incompatible mappings update');
}
// update settings after mappings was successful to ensure
// pointing to theme new pipeline is safe
// for now, only update the pipeline
if (!settings.index.default_pipeline) return;
try {
await callCluster('indices.putSettings', {
index: indexName,
body: { index: { default_pipeline: settings.index.default_pipeline } },
});
} catch (err) {
throw new Error('incompatible settings update');
}
};

const getIndexByNamespaceQuery = (templateName: string) => ({
index: `${templateName}-*`,
size: 1,
body: {
aggs: {
streams: {
terms: { field: 'fields.stream.namespace' },
},
},
},
});
Loading