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 00c7d705c1f44..68b2ac59d2a19 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 c91ee3e2a1c06..a203a434bb21a 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 3e1577d8033ba..eb17402a46950 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 5af8807f2dec8..df5d6e2f80c15 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 0000000000000..b379cb3956a02
--- /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 4d20db4018740..0ad2d923117f4 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 2274efda426ad..0000000000000
--- 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 0000000000000..f901bfcf4d49d
--- /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 0000000000000..d18a63d34c101
--- /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 0000000000000..694f26abafe1d
--- /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 dc3d8a640e682..7d512936290af 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;
}