diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts index 00c7d705c1f44a..68b2ac59d2a190 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts @@ -195,3 +195,41 @@ export const POLICY_WITH_NODE_ROLE_ALLOCATION: PolicyFromES = { }, name: POLICY_NAME, }; + +export const POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS = ({ + version: 1, + modified_date: Date.now().toString(), + policy: { + foo: 'bar', + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + unknown_setting: 123, + max_size: '50gb', + }, + }, + }, + warm: { + actions: { + my_unfollow_action: {}, + set_priority: { + priority: 22, + unknown_setting: true, + }, + }, + }, + delete: { + wait_for_snapshot: { + policy: SNAPSHOT_POLICY_NAME, + }, + delete: { + delete_searchable_snapshot: true, + }, + }, + }, + name: POLICY_NAME, + }, + name: POLICY_NAME, +} as any) as PolicyFromES; diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index c91ee3e2a1c068..a203a434bb21a4 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -19,6 +19,7 @@ import { POLICY_WITH_INCLUDE_EXCLUDE, POLICY_WITH_NODE_ATTR_AND_OFF_ALLOCATION, POLICY_WITH_NODE_ROLE_ALLOCATION, + POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS, getDefaultHotPhasePolicy, } from './constants'; @@ -31,6 +32,70 @@ describe('', () => { server.restore(); }); + describe('serialization', () => { + /** + * We assume that policies that populate this form are loaded directly from ES and so + * are valid according to ES. There may be settings in the policy created through the ILM + * API that the UI does not cater for, like the unfollow action. We do not want to overwrite + * the configuration for these actions in the UI. + */ + it('preserves policy settings it did not configure', async () => { + httpRequestsMockHelpers.setLoadPolicies([POLICY_WITH_KNOWN_AND_UNKNOWN_FIELDS]); + httpRequestsMockHelpers.setLoadSnapshotPolicies([]); + httpRequestsMockHelpers.setListNodes({ + nodesByRoles: {}, + nodesByAttributes: { test: ['123'] }, + isUsingDeprecatedDataRoleConfig: false, + }); + + await act(async () => { + testBed = await setup(); + }); + + const { component, actions } = testBed; + component.update(); + + // Set max docs to test whether we keep the unknown fields in that object after serializing + await actions.hot.setMaxDocs('1000'); + // Remove the delete phase to ensure that we also correctly remove data + await actions.delete.enable(false); + await actions.savePolicy(); + + const latestRequest = server.requests[server.requests.length - 1]; + const entirePolicy = JSON.parse(JSON.parse(latestRequest.requestBody).body); + + expect(entirePolicy).toEqual({ + foo: 'bar', // Made up value + name: 'my_policy', + phases: { + hot: { + actions: { + rollover: { + max_docs: 1000, + max_size: '50gb', + unknown_setting: 123, // Made up setting that should stay preserved + }, + set_priority: { + priority: 100, + }, + }, + min_age: '0ms', + }, + warm: { + actions: { + my_unfollow_action: {}, // Made up action + set_priority: { + priority: 22, + unknown_setting: true, + }, + }, + min_age: '0ms', + }, + }, + }); + }); + }); + describe('hot phase', () => { describe('serialization', () => { beforeEach(async () => { diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index 3e1577d8033ba7..eb17402a469501 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -298,12 +298,12 @@ describe('edit policy', () => { phases: { hot: { actions: { - set_priority: { - priority: 100, - }, rollover: { - max_size: '50gb', max_age: '30d', + max_size: '50gb', + }, + set_priority: { + priority: 100, }, }, min_age: '0ms', diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts index 5af8807f2dec82..df5d6e2f80c15b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer.ts @@ -22,13 +22,11 @@ export const deserializer = (policy: SerializedPolicy): FormInternal => { const _meta: FormInternal['_meta'] = { hot: { useRollover: Boolean(hot?.actions?.rollover), - forceMergeEnabled: Boolean(hot?.actions?.forcemerge), bestCompression: hot?.actions?.forcemerge?.index_codec === 'best_compression', }, warm: { enabled: Boolean(warm), warmPhaseOnRollover: Boolean(warm?.min_age === '0ms'), - forceMergeEnabled: Boolean(warm?.actions?.forcemerge), bestCompression: warm?.actions?.forcemerge?.index_codec === 'best_compression', dataTierAllocationType: determineDataTierAllocationType(warm?.actions), }, diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts new file mode 100644 index 00000000000000..b379cb3956a022 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/deserializer_and_serializer.test.ts @@ -0,0 +1,201 @@ +/* + * 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 { setAutoFreeze } from 'immer'; +import { cloneDeep } from 'lodash'; +import { SerializedPolicy } from '../../../../../common/types'; +import { deserializer } from './deserializer'; +import { createSerializer } from './serializer'; +import { FormInternal } from '../types'; + +const isObject = (v: unknown): v is { [key: string]: any } => + Object.prototype.toString.call(v) === '[object Object]'; + +const unknownValue = { some: 'value' }; + +const populateWithUnknownEntries = (v: unknown) => { + if (isObject(v)) { + for (const key of Object.keys(v)) { + if (['require', 'include', 'exclude'].includes(key)) continue; // this will generate an invalid policy + populateWithUnknownEntries(v[key]); + } + v.unknown = unknownValue; + return; + } + if (Array.isArray(v)) { + v.forEach(populateWithUnknownEntries); + } +}; + +const originalPolicy: SerializedPolicy = { + name: 'test', + phases: { + hot: { + actions: { + rollover: { + max_age: '1d', + max_size: '10gb', + max_docs: 1000, + }, + forcemerge: { + index_codec: 'best_compression', + max_num_segments: 22, + }, + set_priority: { + priority: 1, + }, + }, + min_age: '12ms', + }, + warm: { + min_age: '12ms', + actions: { + shrink: { number_of_shards: 12 }, + allocate: { + number_of_replicas: 3, + }, + set_priority: { + priority: 10, + }, + migrate: { enabled: false }, + }, + }, + cold: { + min_age: '30ms', + actions: { + allocate: { + number_of_replicas: 12, + require: { test: 'my_value' }, + include: { test: 'my_value' }, + exclude: { test: 'my_value' }, + }, + freeze: {}, + set_priority: { + priority: 12, + }, + }, + }, + delete: { + min_age: '33ms', + actions: { + delete: { + delete_searchable_snapshot: true, + }, + wait_for_snapshot: { + policy: 'test', + }, + }, + }, + }, +}; + +describe('deserializer and serializer', () => { + let policy: SerializedPolicy; + let serializer: ReturnType; + let formInternal: FormInternal; + + // So that we can modify produced form objects + beforeAll(() => setAutoFreeze(false)); + // This is the default in dev, so change back to true (https://github.com/immerjs/immer/blob/master/docs/freezing.md) + afterAll(() => setAutoFreeze(true)); + + beforeEach(() => { + policy = cloneDeep(originalPolicy); + formInternal = deserializer(policy); + // Because the policy object is not deepCloned by the form lib we + // clone here so that we can mutate the policy and preserve the + // original reference in the createSerializer + serializer = createSerializer(cloneDeep(policy)); + }); + + it('preserves any unknown policy settings', () => { + const thisTestPolicy = cloneDeep(originalPolicy); + // We populate all levels of the policy with entries our UI does not know about + populateWithUnknownEntries(thisTestPolicy); + serializer = createSerializer(thisTestPolicy); + + const copyOfThisTestPolicy = cloneDeep(thisTestPolicy); + + expect(serializer(deserializer(thisTestPolicy))).toEqual(thisTestPolicy); + + // Assert that the policy we passed in is unaltered after deserialization and serialization + expect(thisTestPolicy).not.toBe(copyOfThisTestPolicy); + expect(thisTestPolicy).toEqual(copyOfThisTestPolicy); + }); + + it('removes all phases if they were disabled in the form', () => { + formInternal._meta.warm.enabled = false; + formInternal._meta.cold.enabled = false; + formInternal._meta.delete.enabled = false; + + expect(serializer(formInternal)).toEqual({ + name: 'test', + phases: { + hot: policy.phases.hot, // We expect to see only the hot phase + }, + }); + }); + + it('removes the forcemerge action if it is disabled in the form', () => { + delete formInternal.phases.hot!.actions.forcemerge; + delete formInternal.phases.warm!.actions.forcemerge; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + expect(result.phases.warm!.actions.forcemerge).toBeUndefined(); + }); + + it('removes set priority if it is disabled in the form', () => { + delete formInternal.phases.hot!.actions.set_priority; + delete formInternal.phases.warm!.actions.set_priority; + delete formInternal.phases.cold!.actions.set_priority; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.set_priority).toBeUndefined(); + expect(result.phases.warm!.actions.set_priority).toBeUndefined(); + expect(result.phases.cold!.actions.set_priority).toBeUndefined(); + }); + + it('removes freeze setting in the cold phase if it is disabled in the form', () => { + formInternal._meta.cold.freezeEnabled = false; + + const result = serializer(formInternal); + + expect(result.phases.cold!.actions.freeze).toBeUndefined(); + }); + + it('removes node attribute allocation when it is not selected in the form', () => { + // Change from 'node_attrs' to 'node_roles' + formInternal._meta.cold.dataTierAllocationType = 'node_roles'; + + const result = serializer(formInternal); + + expect(result.phases.cold!.actions.allocate!.number_of_replicas).toBe(12); + expect(result.phases.cold!.actions.allocate!.require).toBeUndefined(); + expect(result.phases.cold!.actions.allocate!.include).toBeUndefined(); + expect(result.phases.cold!.actions.allocate!.exclude).toBeUndefined(); + }); + + it('removes forcemerge and rollover config when rollover is disabled in hot phase', () => { + formInternal._meta.hot.useRollover = false; + + const result = serializer(formInternal); + + expect(result.phases.hot!.actions.rollover).toBeUndefined(); + expect(result.phases.hot!.actions.forcemerge).toBeUndefined(); + }); + + it('removes min_age from warm when rollover is enabled', () => { + formInternal._meta.hot.useRollover = true; + formInternal._meta.warm.warmPhaseOnRollover = true; + + const result = serializer(formInternal); + + expect(result.phases.warm!.min_age).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index 4d20db40187409..0ad2d923117f45 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -23,7 +23,7 @@ import { i18nTexts } from '../i18n_texts'; const { emptyField, numberGreaterThanField } = fieldValidators; const serializers = { - stringToNumber: (v: string): any => (v ? parseInt(v, 10) : undefined), + stringToNumber: (v: string): any => (v != null ? parseInt(v, 10) : undefined), }; export const schema: FormSchema = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts deleted file mode 100644 index 2274efda426ad1..00000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - * 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 { isEmpty, isNumber } from 'lodash'; - -import { SerializedPolicy, SerializedActionWithAllocation } from '../../../../../common/types'; - -import { FormInternal, DataAllocationMetaFields } from '../types'; - -const serializeAllocateAction = ( - { dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields, - newActions: SerializedActionWithAllocation = {}, - originalActions: SerializedActionWithAllocation = {} -): SerializedActionWithAllocation => { - const { allocate, migrate, ...rest } = newActions; - // First copy over all non-allocate and migrate actions. - const actions: SerializedActionWithAllocation = { allocate, migrate, ...rest }; - - switch (dataTierAllocationType) { - case 'node_attrs': - if (allocationNodeAttribute) { - const [name, value] = allocationNodeAttribute.split(':'); - actions.allocate = { - // copy over any other allocate details like "number_of_replicas" - ...actions.allocate, - require: { - [name]: value, - }, - }; - } else { - // The form has been configured to use node attribute based allocation but no node attribute - // was selected. We fall back to what was originally selected in this case. This might be - // migrate.enabled: "false" - actions.migrate = originalActions.migrate; - } - - // copy over the original include and exclude values until we can set them in the form. - if (!isEmpty(originalActions?.allocate?.include)) { - actions.allocate = { - ...actions.allocate, - include: { ...originalActions?.allocate?.include }, - }; - } - - if (!isEmpty(originalActions?.allocate?.exclude)) { - actions.allocate = { - ...actions.allocate, - exclude: { ...originalActions?.allocate?.exclude }, - }; - } - break; - case 'none': - actions.migrate = { enabled: false }; - break; - default: - } - return actions; -}; - -export const createSerializer = (originalPolicy?: SerializedPolicy) => ( - data: FormInternal -): SerializedPolicy => { - const { _meta, ...policy } = data; - - if (!policy.phases || !policy.phases.hot) { - policy.phases = { hot: { actions: {} } }; - } - - /** - * HOT PHASE SERIALIZATION - */ - if (policy.phases.hot) { - policy.phases.hot.min_age = originalPolicy?.phases.hot?.min_age ?? '0ms'; - } - - if (policy.phases.hot?.actions) { - if (policy.phases.hot.actions?.rollover && _meta.hot.useRollover) { - if (policy.phases.hot.actions.rollover.max_age) { - policy.phases.hot.actions.rollover.max_age = `${policy.phases.hot.actions.rollover.max_age}${_meta.hot.maxAgeUnit}`; - } - - if (policy.phases.hot.actions.rollover.max_size) { - policy.phases.hot.actions.rollover.max_size = `${policy.phases.hot.actions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; - } - - if (_meta.hot.bestCompression && policy.phases.hot.actions?.forcemerge) { - policy.phases.hot.actions.forcemerge.index_codec = 'best_compression'; - } - } else { - delete policy.phases.hot.actions?.rollover; - } - } - - /** - * WARM PHASE SERIALIZATION - */ - if (policy.phases.warm) { - // If warm phase on rollover is enabled, delete min age field - // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time - // They are mutually exclusive - if (_meta.hot.useRollover && _meta.warm.warmPhaseOnRollover) { - delete policy.phases.warm.min_age; - } else if ( - (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && - policy.phases.warm.min_age - ) { - policy.phases.warm.min_age = `${policy.phases.warm.min_age}${_meta.warm.minAgeUnit}`; - } - - policy.phases.warm.actions = serializeAllocateAction( - _meta.warm, - policy.phases.warm.actions, - originalPolicy?.phases.warm?.actions - ); - - if ( - policy.phases.warm.actions.allocate && - !policy.phases.warm.actions.allocate.require && - !isNumber(policy.phases.warm.actions.allocate.number_of_replicas) && - isEmpty(policy.phases.warm.actions.allocate.include) && - isEmpty(policy.phases.warm.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete policy.phases.warm.actions.allocate; - } - - if (_meta.warm.bestCompression && policy.phases.warm.actions?.forcemerge) { - policy.phases.warm.actions.forcemerge.index_codec = 'best_compression'; - } - } - - /** - * COLD PHASE SERIALIZATION - */ - if (policy.phases.cold) { - if (policy.phases.cold.min_age) { - policy.phases.cold.min_age = `${policy.phases.cold.min_age}${_meta.cold.minAgeUnit}`; - } - - policy.phases.cold.actions = serializeAllocateAction( - _meta.cold, - policy.phases.cold.actions, - originalPolicy?.phases.cold?.actions - ); - - if ( - policy.phases.cold.actions.allocate && - !policy.phases.cold.actions.allocate.require && - !isNumber(policy.phases.cold.actions.allocate.number_of_replicas) && - isEmpty(policy.phases.cold.actions.allocate.include) && - isEmpty(policy.phases.cold.actions.allocate.exclude) - ) { - // remove allocate action if it does not define require or number of nodes - // and both include and exclude are empty objects (ES will fail to parse if we don't) - delete policy.phases.cold.actions.allocate; - } - - if (_meta.cold.freezeEnabled) { - policy.phases.cold.actions.freeze = {}; - } - } - - /** - * DELETE PHASE SERIALIZATION - */ - if (policy.phases.delete) { - if (policy.phases.delete.min_age) { - policy.phases.delete.min_age = `${policy.phases.delete.min_age}${_meta.delete.minAgeUnit}`; - } - - if (originalPolicy?.phases.delete?.actions) { - const { wait_for_snapshot: __, ...rest } = originalPolicy.phases.delete.actions; - policy.phases.delete.actions = { - ...policy.phases.delete.actions, - ...rest, - }; - } - } - - return policy; -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/index.ts new file mode 100644 index 00000000000000..f901bfcf4d49d3 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { createSerializer } from './serializer'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts new file mode 100644 index 00000000000000..d18a63d34c101e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serialize_migrate_and_allocate_actions.ts @@ -0,0 +1,73 @@ +/* + * 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 { isEmpty } from 'lodash'; + +import { SerializedActionWithAllocation } from '../../../../../../common/types'; + +import { DataAllocationMetaFields } from '../../types'; + +export const serializeMigrateAndAllocateActions = ( + { dataTierAllocationType, allocationNodeAttribute }: DataAllocationMetaFields, + newActions: SerializedActionWithAllocation = {}, + originalActions: SerializedActionWithAllocation = {} +): SerializedActionWithAllocation => { + const { allocate, migrate, ...otherActions } = newActions; + + // First copy over all non-allocate and migrate actions. + const actions: SerializedActionWithAllocation = { ...otherActions }; + + // The UI only knows about include, exclude and require, so copy over all other values. + if (allocate) { + const { include, exclude, require, ...otherSettings } = allocate; + if (!isEmpty(otherSettings)) { + actions.allocate = { ...otherSettings }; + } + } + + switch (dataTierAllocationType) { + case 'node_attrs': + if (allocationNodeAttribute) { + const [name, value] = allocationNodeAttribute.split(':'); + actions.allocate = { + // copy over any other allocate details like "number_of_replicas" + ...actions.allocate, + require: { + [name]: value, + }, + }; + } else { + // The form has been configured to use node attribute based allocation but no node attribute + // was selected. We fall back to what was originally selected in this case. This might be + // migrate.enabled: "false" + actions.migrate = originalActions.migrate; + } + + // copy over the original include and exclude values until we can set them in the form. + if (!isEmpty(originalActions?.allocate?.include)) { + actions.allocate = { + ...actions.allocate, + include: { ...originalActions?.allocate?.include }, + }; + } + + if (!isEmpty(originalActions?.allocate?.exclude)) { + actions.allocate = { + ...actions.allocate, + exclude: { ...originalActions?.allocate?.exclude }, + }; + } + break; + case 'none': + actions.migrate = { + ...originalActions?.migrate, + enabled: false, + }; + break; + default: + } + return actions; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts new file mode 100644 index 00000000000000..694f26abafe1d0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/serializer/serializer.ts @@ -0,0 +1,161 @@ +/* + * 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 { produce } from 'immer'; + +import { merge } from 'lodash'; + +import { SerializedPolicy } from '../../../../../../common/types'; + +import { defaultPolicy } from '../../../../constants'; + +import { FormInternal } from '../../types'; + +import { serializeMigrateAndAllocateActions } from './serialize_migrate_and_allocate_actions'; + +export const createSerializer = (originalPolicy?: SerializedPolicy) => ( + data: FormInternal +): SerializedPolicy => { + const { _meta, ...updatedPolicy } = data; + + if (!updatedPolicy.phases || !updatedPolicy.phases.hot) { + updatedPolicy.phases = { hot: { actions: {} } }; + } + + return produce(originalPolicy ?? defaultPolicy, (draft) => { + // Copy over all updated fields + merge(draft, updatedPolicy); + + // Next copy over all meta fields and delete any fields that have been removed + // by fields exposed in the form. It is very important that we do not delete + // data that the form does not control! E.g., unfollow action in hot phase. + + /** + * HOT PHASE SERIALIZATION + */ + if (draft.phases.hot) { + draft.phases.hot.min_age = draft.phases.hot.min_age ?? '0ms'; + } + + if (draft.phases.hot?.actions) { + const hotPhaseActions = draft.phases.hot.actions; + if (hotPhaseActions.rollover && _meta.hot.useRollover) { + if (hotPhaseActions.rollover.max_age) { + hotPhaseActions.rollover.max_age = `${hotPhaseActions.rollover.max_age}${_meta.hot.maxAgeUnit}`; + } + + if (hotPhaseActions.rollover.max_size) { + hotPhaseActions.rollover.max_size = `${hotPhaseActions.rollover.max_size}${_meta.hot.maxStorageSizeUnit}`; + } + + if (!updatedPolicy.phases.hot!.actions?.forcemerge) { + delete hotPhaseActions.forcemerge; + } else if (_meta.hot.bestCompression) { + hotPhaseActions.forcemerge!.index_codec = 'best_compression'; + } + + if (_meta.hot.bestCompression && hotPhaseActions.forcemerge) { + hotPhaseActions.forcemerge.index_codec = 'best_compression'; + } + } else { + delete hotPhaseActions.rollover; + delete hotPhaseActions.forcemerge; + } + + if (!updatedPolicy.phases.hot!.actions?.set_priority) { + delete hotPhaseActions.set_priority; + } + } + + /** + * WARM PHASE SERIALIZATION + */ + if (_meta.warm.enabled) { + const warmPhase = draft.phases.warm!; + // If warm phase on rollover is enabled, delete min age field + // An index lifecycle switches to warm phase when rollover occurs, so you cannot specify a warm phase time + // They are mutually exclusive + if ( + (!_meta.hot.useRollover || !_meta.warm.warmPhaseOnRollover) && + updatedPolicy.phases.warm!.min_age + ) { + warmPhase.min_age = `${updatedPolicy.phases.warm!.min_age}${_meta.warm.minAgeUnit}`; + } else { + delete warmPhase.min_age; + } + + warmPhase.actions = serializeMigrateAndAllocateActions( + _meta.warm, + warmPhase.actions, + originalPolicy?.phases.warm?.actions + ); + + if (!updatedPolicy.phases.warm!.actions?.forcemerge) { + delete warmPhase.actions.forcemerge; + } else if (_meta.warm.bestCompression) { + warmPhase.actions.forcemerge!.index_codec = 'best_compression'; + } + + if (!updatedPolicy.phases.warm!.actions?.set_priority) { + delete warmPhase.actions.set_priority; + } + + if (!updatedPolicy.phases.warm!.actions?.shrink) { + delete warmPhase.actions.shrink; + } + } else { + delete draft.phases.warm; + } + + /** + * COLD PHASE SERIALIZATION + */ + if (_meta.cold.enabled) { + const coldPhase = draft.phases.cold!; + + if (updatedPolicy.phases.cold!.min_age) { + coldPhase.min_age = `${updatedPolicy.phases.cold!.min_age}${_meta.cold.minAgeUnit}`; + } + + coldPhase.actions = serializeMigrateAndAllocateActions( + _meta.cold, + coldPhase.actions, + originalPolicy?.phases.cold?.actions + ); + + if (_meta.cold.freezeEnabled) { + coldPhase.actions.freeze = coldPhase.actions.freeze ?? {}; + } else { + delete coldPhase.actions.freeze; + } + + if (!updatedPolicy.phases.cold!.actions?.set_priority) { + delete coldPhase.actions.set_priority; + } + } else { + delete draft.phases.cold; + } + + /** + * DELETE PHASE SERIALIZATION + */ + if (_meta.delete.enabled) { + const deletePhase = draft.phases.delete!; + if (updatedPolicy.phases.delete!.min_age) { + deletePhase.min_age = `${updatedPolicy.phases.delete!.min_age}${_meta.delete.minAgeUnit}`; + } + + if ( + !updatedPolicy.phases.delete!.actions?.wait_for_snapshot && + deletePhase.actions.wait_for_snapshot + ) { + delete deletePhase.actions.wait_for_snapshot; + } + } else { + delete draft.phases.delete; + } + }); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts index dc3d8a640e682d..7d512936290af0 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/types.ts @@ -18,7 +18,6 @@ export interface MinAgeField { } export interface ForcemergeFields { - forceMergeEnabled: boolean; bestCompression: boolean; }