diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts
index 7c095256bd10f..28d4ad5aceb2d 100644
--- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts
+++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts
@@ -124,7 +124,8 @@ describe('', () => {
const { snapshotName } = POLICY_EDIT;
// Complete step 1, change snapshot name
- form.setInputValue('snapshotNameInput', `${snapshotName}-edited`);
+ const editedSnapshotName = `${snapshotName}-edited`;
+ form.setInputValue('snapshotNameInput', editedSnapshotName);
actions.clickNextButton();
// Complete step 2, enable ignore unavailable indices switch
@@ -143,20 +144,24 @@ describe('', () => {
const latestRequest = server.requests[server.requests.length - 1];
+ const { name, isManagedPolicy, schedule, repository, retention } = POLICY_EDIT;
+
const expected = {
- ...POLICY_EDIT,
- ...{
- config: {
- ignoreUnavailable: true,
- },
- retention: {
- ...POLICY_EDIT.retention,
- expireAfterValue: Number(EXPIRE_AFTER_VALUE),
- expireAfterUnit: EXPIRE_AFTER_UNIT,
- },
- snapshotName: `${POLICY_EDIT.snapshotName}-edited`,
+ name,
+ isManagedPolicy,
+ schedule,
+ repository,
+ config: {
+ ignoreUnavailable: true,
+ },
+ retention: {
+ ...retention,
+ expireAfterValue: Number(EXPIRE_AFTER_VALUE),
+ expireAfterUnit: EXPIRE_AFTER_UNIT,
},
+ snapshotName: editedSnapshotName,
};
+
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
});
@@ -180,10 +185,25 @@ describe('', () => {
const latestRequest = server.requests[server.requests.length - 1];
+ const {
+ name,
+ isManagedPolicy,
+ schedule,
+ repository,
+ retention,
+ config,
+ snapshotName,
+ } = POLICY_EDIT;
+
const expected = {
- ...POLICY_EDIT,
+ name,
+ isManagedPolicy,
+ schedule,
+ repository,
+ config,
+ snapshotName,
retention: {
- ...POLICY_EDIT.retention,
+ ...retention,
expireAfterValue: Number(EXPIRE_AFTER_VALUE),
expireAfterUnit: TIME_UNITS.DAY, // default value
},
diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx
index 7af663b29957d..a119c96e0a1ec 100644
--- a/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx
+++ b/x-pack/plugins/snapshot_restore/public/application/sections/policy_edit/policy_edit.tsx
@@ -64,8 +64,22 @@ export const PolicyEdit: React.FunctionComponent {
- if (policyData && policyData.policy) {
- setPolicy(policyData.policy);
+ if (policyData?.policy) {
+ const { policy: policyToEdit } = policyData;
+
+ // The policy response includes data not pertinent to the form
+ // that we need to remove, e.g., lastSuccess, lastFailure, stats
+ const policyFormData: SlmPolicyPayload = {
+ name: policyToEdit.name,
+ snapshotName: policyToEdit.snapshotName,
+ schedule: policyToEdit.schedule,
+ repository: policyToEdit.repository,
+ config: policyToEdit.config,
+ retention: policyToEdit.retention,
+ isManagedPolicy: policyToEdit.isManagedPolicy,
+ };
+
+ setPolicy(policyFormData);
}
}, [policyData]);
diff --git a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts
index e5df0ec33db0b..7a13b4ac27caa 100644
--- a/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts
+++ b/x-pack/plugins/snapshot_restore/server/routes/api/validate_schemas.ts
@@ -26,20 +26,12 @@ const snapshotRetentionSchema = schema.object({
export const policySchema = schema.object({
name: schema.string(),
- version: schema.maybe(schema.number()),
- modifiedDate: schema.maybe(schema.string()),
- modifiedDateMillis: schema.maybe(schema.number()),
snapshotName: schema.string(),
schedule: schema.string(),
repository: schema.string(),
- nextExecution: schema.maybe(schema.string()),
- nextExecutionMillis: schema.maybe(schema.number()),
config: schema.maybe(snapshotConfigSchema),
retention: schema.maybe(snapshotRetentionSchema),
isManagedPolicy: schema.boolean(),
- stats: schema.maybe(schema.object({}, { unknowns: 'allow' })),
- lastFailure: schema.maybe(schema.object({}, { unknowns: 'allow' })),
- lastSuccess: schema.maybe(schema.object({}, { unknowns: 'allow' })),
});
const fsRepositorySettings = schema.object({
diff --git a/x-pack/test/api_integration/apis/management/index.js b/x-pack/test/api_integration/apis/management/index.js
index 5afb9cfba9f5f..7b6deb0c3892b 100644
--- a/x-pack/test/api_integration/apis/management/index.js
+++ b/x-pack/test/api_integration/apis/management/index.js
@@ -13,5 +13,6 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./index_management'));
loadTestFile(require.resolve('./index_lifecycle_management'));
loadTestFile(require.resolve('./ingest_pipelines'));
+ loadTestFile(require.resolve('./snapshot_restore'));
});
}
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts
new file mode 100644
index 0000000000000..f0eea0f960b4b
--- /dev/null
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/index.ts
@@ -0,0 +1,12 @@
+/*
+ * 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 { FtrProviderContext } from '../../../ftr_provider_context';
+
+export default function ({ loadTestFile }: FtrProviderContext) {
+ describe('Snapshot and Restore', () => {
+ loadTestFile(require.resolve('./snapshot_restore'));
+ });
+}
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts
new file mode 100644
index 0000000000000..932df405dde12
--- /dev/null
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/elasticsearch.ts
@@ -0,0 +1,89 @@
+/*
+ * 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 { FtrProviderContext } from '../../../../ftr_provider_context';
+
+interface SlmPolicy {
+ name: string;
+ snapshotName: string;
+ schedule: string;
+ repository: string;
+ isManagedPolicy: boolean;
+ config?: {
+ indices?: string | string[];
+ ignoreUnavailable?: boolean;
+ includeGlobalState?: boolean;
+ partial?: boolean;
+ metadata?: Record;
+ };
+ retention?: {
+ expireAfterValue?: number | '';
+ expireAfterUnit?: string;
+ maxCount?: number | '';
+ minCount?: number | '';
+ };
+}
+
+/**
+ * Helpers to create and delete SLM policies on the Elasticsearch instance
+ * during our tests.
+ * @param {ElasticsearchClient} es The Elasticsearch client instance
+ */
+export const registerEsHelpers = (getService: FtrProviderContext['getService']) => {
+ let policiesCreated: string[] = [];
+
+ const es = getService('legacyEs');
+
+ const createRepository = (repoName: string) => {
+ return es.snapshot.createRepository({
+ repository: repoName,
+ body: {
+ type: 'fs',
+ settings: {
+ location: '/tmp/',
+ },
+ },
+ verify: false,
+ });
+ };
+
+ const createPolicy = (policy: SlmPolicy, cachePolicy?: boolean) => {
+ if (cachePolicy) {
+ policiesCreated.push(policy.name);
+ }
+
+ return es.sr.updatePolicy({
+ name: policy.name,
+ body: policy,
+ });
+ };
+
+ const getPolicy = (policyName: string) => {
+ return es.sr.policy({
+ name: policyName,
+ human: true,
+ });
+ };
+
+ const deletePolicy = (policyName: string) => es.sr.deletePolicy({ name: policyName });
+
+ const cleanupPolicies = () =>
+ Promise.all(policiesCreated.map(deletePolicy))
+ .then(() => {
+ policiesCreated = [];
+ })
+ .catch((err) => {
+ // eslint-disable-next-line no-console
+ console.log(`[Cleanup error] Error deleting ES resources: ${err.message}`);
+ });
+
+ return {
+ createRepository,
+ createPolicy,
+ deletePolicy,
+ cleanupPolicies,
+ getPolicy,
+ };
+};
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/index.ts
new file mode 100644
index 0000000000000..66ea0fe40c4ce
--- /dev/null
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/lib/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 { registerEsHelpers } from './elasticsearch';
diff --git a/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts
new file mode 100644
index 0000000000000..575da0db2a759
--- /dev/null
+++ b/x-pack/test/api_integration/apis/management/snapshot_restore/snapshot_restore.ts
@@ -0,0 +1,234 @@
+/*
+ * 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 '../../../ftr_provider_context';
+import { registerEsHelpers } from './lib';
+
+const API_BASE_PATH = '/api/snapshot_restore';
+const REPO_NAME = 'test_repo';
+
+export default function ({ getService }: FtrProviderContext) {
+ const supertest = getService('supertest');
+
+ const {
+ createRepository,
+ createPolicy,
+ deletePolicy,
+ cleanupPolicies,
+ getPolicy,
+ } = registerEsHelpers(getService);
+
+ describe('Snapshot Lifecycle Management', function () {
+ before(async () => {
+ try {
+ await createRepository(REPO_NAME);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log('[Setup error] Error creating repository');
+ throw err;
+ }
+ });
+
+ after(async () => {
+ await cleanupPolicies();
+ });
+
+ describe('Create', () => {
+ const POLICY_NAME = 'test_create_policy';
+ const REQUIRED_FIELDS_POLICY_NAME = 'test_create_required_fields_policy';
+
+ after(async () => {
+ // Clean up any policies created in test cases
+ await Promise.all([POLICY_NAME, REQUIRED_FIELDS_POLICY_NAME].map(deletePolicy)).catch(
+ (err) => {
+ // eslint-disable-next-line no-console
+ console.log(`[Cleanup error] Error deleting policies: ${err.message}`);
+ throw err;
+ }
+ );
+ });
+
+ it('should create a SLM policy', async () => {
+ const { body } = await supertest
+ .post(`${API_BASE_PATH}/policies`)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ name: POLICY_NAME,
+ snapshotName: 'my_snapshot',
+ schedule: '0 30 1 * * ?',
+ repository: REPO_NAME,
+ config: {
+ indices: ['my_index'],
+ ignoreUnavailable: true,
+ partial: false,
+ metadata: {
+ meta: 'my_meta',
+ },
+ },
+ retention: {
+ expireAfterValue: 1,
+ expireAfterUnit: 'd',
+ maxCount: 10,
+ minCount: 5,
+ },
+ isManagedPolicy: false,
+ })
+ .expect(200);
+
+ expect(body).to.eql({
+ acknowledged: true,
+ });
+
+ const policyFromEs = await getPolicy(POLICY_NAME);
+ expect(policyFromEs[POLICY_NAME]).to.be.ok();
+ expect(policyFromEs[POLICY_NAME].policy).to.eql({
+ name: 'my_snapshot',
+ schedule: '0 30 1 * * ?',
+ repository: REPO_NAME,
+ config: {
+ indices: ['my_index'],
+ ignore_unavailable: true,
+ partial: false,
+ metadata: {
+ meta: 'my_meta',
+ },
+ },
+ retention: {
+ expire_after: '1d',
+ max_count: 10,
+ min_count: 5,
+ },
+ });
+ });
+
+ it('should create a policy with only required fields', async () => {
+ const { body } = await supertest
+ .post(`${API_BASE_PATH}/policies`)
+ .set('kbn-xsrf', 'xxx')
+ // Exclude config and retention
+ .send({
+ name: REQUIRED_FIELDS_POLICY_NAME,
+ snapshotName: 'my_snapshot',
+ repository: REPO_NAME,
+ schedule: '0 30 1 * * ?',
+ isManagedPolicy: false,
+ })
+ .expect(200);
+
+ expect(body).to.eql({
+ acknowledged: true,
+ });
+
+ const policyFromEs = await getPolicy(REQUIRED_FIELDS_POLICY_NAME);
+ expect(policyFromEs[REQUIRED_FIELDS_POLICY_NAME]).to.be.ok();
+ expect(policyFromEs[REQUIRED_FIELDS_POLICY_NAME].policy).to.eql({
+ name: 'my_snapshot',
+ repository: REPO_NAME,
+ schedule: '0 30 1 * * ?',
+ });
+ });
+ });
+
+ describe('Update', () => {
+ const POLICY_NAME = 'test_update_policy';
+ const POLICY = {
+ name: POLICY_NAME,
+ snapshotName: 'my_snapshot',
+ schedule: '0 30 1 * * ?',
+ repository: REPO_NAME,
+ config: {
+ indices: ['my_index'],
+ ignoreUnavailable: true,
+ partial: false,
+ metadata: {
+ meta: 'my_meta',
+ },
+ },
+ retention: {
+ expireAfterValue: 1,
+ expireAfterUnit: 'd',
+ maxCount: 10,
+ minCount: 5,
+ },
+ isManagedPolicy: false,
+ };
+
+ before(async () => {
+ // Create SLM policy that can be used to test PUT request
+ try {
+ await createPolicy(POLICY, true);
+ } catch (err) {
+ // eslint-disable-next-line no-console
+ console.log('[Setup error] Error creating policy');
+ throw err;
+ }
+ });
+
+ it('should allow an existing policy to be updated', async () => {
+ const uri = `${API_BASE_PATH}/policies/${POLICY_NAME}`;
+
+ const { body } = await supertest
+ .put(uri)
+ .set('kbn-xsrf', 'xxx')
+ .send({
+ ...POLICY,
+ schedule: '0 0 0 ? * 7',
+ })
+ .expect(200);
+
+ expect(body).to.eql({
+ acknowledged: true,
+ });
+
+ const policyFromEs = await getPolicy(POLICY_NAME);
+ expect(policyFromEs[POLICY_NAME]).to.be.ok();
+ expect(policyFromEs[POLICY_NAME].policy).to.eql({
+ name: 'my_snapshot',
+ schedule: '0 0 0 ? * 7',
+ repository: REPO_NAME,
+ config: {
+ indices: ['my_index'],
+ ignore_unavailable: true,
+ partial: false,
+ metadata: {
+ meta: 'my_meta',
+ },
+ },
+ retention: {
+ expire_after: '1d',
+ max_count: 10,
+ min_count: 5,
+ },
+ });
+ });
+
+ it('should allow optional fields to be removed', async () => {
+ const uri = `${API_BASE_PATH}/policies/${POLICY_NAME}`;
+ const { retention, config, ...requiredFields } = POLICY;
+
+ const { body } = await supertest
+ .put(uri)
+ .set('kbn-xsrf', 'xxx')
+ .send(requiredFields)
+ .expect(200);
+
+ expect(body).to.eql({
+ acknowledged: true,
+ });
+
+ const policyFromEs = await getPolicy(POLICY_NAME);
+ expect(policyFromEs[POLICY_NAME]).to.be.ok();
+ expect(policyFromEs[POLICY_NAME].policy).to.eql({
+ name: 'my_snapshot',
+ schedule: '0 30 1 * * ?',
+ repository: REPO_NAME,
+ });
+ });
+ });
+ });
+}
diff --git a/x-pack/test/api_integration/services/legacy_es.js b/x-pack/test/api_integration/services/legacy_es.js
index 04b991151034a..c184a87365977 100644
--- a/x-pack/test/api_integration/services/legacy_es.js
+++ b/x-pack/test/api_integration/services/legacy_es.js
@@ -10,6 +10,7 @@ import * as legacyElasticsearch from 'elasticsearch';
import { elasticsearchClientPlugin as securityEsClientPlugin } from '../../../plugins/security/server/elasticsearch/elasticsearch_client_plugin';
import { elasticsearchJsPlugin as indexManagementEsClientPlugin } from '../../../plugins/index_management/server/client/elasticsearch';
+import { elasticsearchJsPlugin as snapshotRestoreEsClientPlugin } from '../../../plugins/snapshot_restore/server/client/elasticsearch_sr';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { DEFAULT_API_VERSION } from '../../../../src/core/server/elasticsearch/elasticsearch_config';
@@ -20,6 +21,6 @@ export function LegacyEsProvider({ getService }) {
apiVersion: DEFAULT_API_VERSION,
host: formatUrl(config.get('servers.elasticsearch')),
requestTimeout: config.get('timeouts.esRequestTimeout'),
- plugins: [securityEsClientPlugin, indexManagementEsClientPlugin],
+ plugins: [securityEsClientPlugin, indexManagementEsClientPlugin, snapshotRestoreEsClientPlugin],
});
}