Skip to content
64 changes: 63 additions & 1 deletion x-pack/plugins/snapshot_restore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,66 @@ To run ES with plugins:
1. Run `yarn es snapshot` from the Kibana directory like normal, then exit out of process.
2. `cd .es/8.0.0`
3. `bin/elasticsearch-plugin install https://snapshots.elastic.co/downloads/elasticsearch-plugins/repository-hdfs/repository-hdfs-8.0.0-SNAPSHOT.zip`
4. Run `bin/elasticsearch` from the `.es/8.0.0` directory. Otherwise, starting ES with `yarn es snapshot` would overwrite the plugins you just installed.
4. Run `bin/elasticsearch` from the `.es/8.0.0` directory. Otherwise, starting ES with `yarn es snapshot` would overwrite the plugins you just installed.

### Cloud-managed repositories

Cloud-managed repositories can be imitated when Kibana is running locally by following the steps below:

1. Add the file system path you want to use to elasticsearch.yml or as part of starting up ES. Note that this path should point to a directory that exists.

```
path:
repo: /tmp/es-backups
```

or

```
yarn es snapshot --license=trial -E path.repo=/tmp/es-backups
```

2. Use Console to add the `cluster.metadata.managed_repository` and `cluster.metadata.managed_policies` settings:

```
PUT /_cluster/settings
{
"persistent": {
"cluster.metadata.managed_repository": "found-snapshots",
"cluster.metadata.managed_policies": ["managed-policy"]
}
}
```

3. Use Console or UI to create a repository with the same name as your setting value (`found-snapshots`). Use the file system path from the first step as the `location` setting:

```
PUT /_snapshot/found-snapshots
{
"type": "fs",
"settings": {
"location": "/tmp/es-backups"
}
}
```

4. Use Console or UI to create a policy with the same name as your setting value (`managed-policy`)

```
PUT _slm/policy/managed-policy
{
"name": "managed-snap",
"schedule": "0 30 1 * * ?",
"repository": "found-snapshots",
"config": {
"include_global_state": true,
"feature_states": []
}
}
```

5. Execute the created policy to create a managed snapshot:

```
POST _slm/policy/managed-policy/_execute
```
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import { pageHelpers, getRandomString } from './helpers';
*/
jest.mock('../../public/application/services/http', () => ({
useLoadSnapshots: jest.fn(),
useLastSuccessfulManagedSnapshot: () => {
return { data: undefined };
},
setUiMetricServiceSnapshot: () => {},
setUiMetricService: () => {},
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,6 @@ import { SnapshotListParams, SortDirection, SortField } from '../../../../lib';
import { DataPlaceholder, FormattedDateTime, SnapshotDeleteProvider } from '../../../../components';
import { SnapshotSearchBar } from './snapshot_search_bar';

const getLastSuccessfulManagedSnapshot = (
snapshots: SnapshotDetails[]
): SnapshotDetails | undefined => {
const successfulSnapshots = snapshots
.filter(
({ state, repository, managedRepository }) =>
repository === managedRepository && state === 'SUCCESS'
)
.sort((a, b) => {
return +new Date(b.endTime) - +new Date(a.endTime);
});

return successfulSnapshots[0];
};

interface Props {
snapshots: SnapshotDetails[];
repositories: string[];
Expand All @@ -54,6 +39,7 @@ interface Props {
setListParams: (listParams: SnapshotListParams) => void;
totalItemCount: number;
isLoading: boolean;
lastSuccessfulManagedSnapshot?: SnapshotDetails;
}

export const SnapshotTable: React.FunctionComponent<Props> = (props: Props) => {
Expand All @@ -66,12 +52,11 @@ export const SnapshotTable: React.FunctionComponent<Props> = (props: Props) => {
setListParams,
totalItemCount,
isLoading,
lastSuccessfulManagedSnapshot,
} = props;
const { i18n, uiMetricService, history } = useServices();
const [selectedItems, setSelectedItems] = useState<SnapshotDetails[]>([]);

const lastSuccessfulManagedSnapshot = getLastSuccessfulManagedSnapshot(snapshots);

const columns = [
{
field: 'snapshot',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
useExecutionContext,
} from '../../../../shared_imports';
import { BASE_PATH, UIM_SNAPSHOT_LIST_LOAD } from '../../../constants';
import { useLoadSnapshots } from '../../../services/http';
import { useLoadSnapshots, useLastSuccessfulManagedSnapshot } from '../../../services/http';
import { linkToRepositories } from '../../../services/navigation';
import { useAppContext, useServices } from '../../../app_context';
import { useDecodedParams, SnapshotListParams, DEFAULT_SNAPSHOT_LIST_PARAMS } from '../../../lib';
Expand All @@ -42,6 +42,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
history,
}) => {
const { repositoryName, snapshotId } = useDecodedParams<MatchParams>();
const lastSuccessfulManagedSnapshot = useLastSuccessfulManagedSnapshot().data;
const [listParams, setListParams] = useState<SnapshotListParams>(DEFAULT_SNAPSHOT_LIST_PARAMS);
const {
error,
Expand Down Expand Up @@ -197,6 +198,7 @@ export const SnapshotList: React.FunctionComponent<RouteComponentProps<MatchPara
setListParams={setListParams}
totalItemCount={totalSnapshotsCount}
isLoading={isLoading}
lastSuccessfulManagedSnapshot={lastSuccessfulManagedSnapshot}
/>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,9 @@ export const deleteSnapshots = async (
);
return result;
};

export const useLastSuccessfulManagedSnapshot = () =>
useRequest({
path: `${API_BASE_PATH}snapshots/last_successful_managed_snapshot`,
method: 'get',
});
52 changes: 52 additions & 0 deletions x-pack/plugins/snapshot_restore/server/routes/api/snapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,4 +267,56 @@ export function registerSnapshotsRoutes({
}
})
);

// GET last successful managed snapshot
router.get(
{
path: addBasePath('snapshots/last_successful_managed_snapshot'),
validate: false,
},
license.guardApiRoute(async (ctx, req, res) => {
const { client: clusterClient } = (await ctx.core).elasticsearch;
const managedRepository = await getManagedRepositoryName(clusterClient.asCurrentUser);

if (managedRepository === undefined) {
return res.ok({
body: undefined,
});
}

try {
const response = await clusterClient.asCurrentUser.snapshot.get({
repository: managedRepository,
snapshot: '_all',
ignore_unavailable: true,
sort: 'start_time',
order: 'desc',
});

const { snapshots: managedSnapshotsList } = response;

if (!managedSnapshotsList || managedSnapshotsList.length === 0) {
return res.ok({
body: undefined,
});
}

const successfulManagedSnapshots = managedSnapshotsList.filter(
({ state }) => state === 'SUCCESS'
) as SnapshotDetailsEs[];

if (successfulManagedSnapshots.length === 0) {
return res.ok({
body: undefined,
});
}

return res.ok({
body: deserializeSnapshotDetails(successfulManagedSnapshots[0]),
});
} catch (e) {
return handleEsError({ error: e, response: res });
}
})
);
}
1 change: 1 addition & 0 deletions x-pack/test/functional/apps/snapshot_restore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ export default ({ loadTestFile }: FtrProviderContext) => {
this.tags('skipCloud');
loadTestFile(require.resolve('./home_page'));
loadTestFile(require.resolve('./snapshot_restore'));
loadTestFile(require.resolve('./snapshot_list'));
});
};
122 changes: 122 additions & 0 deletions x-pack/test/functional/apps/snapshot_restore/snapshot_list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';

export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'snapshotRestore', 'header']);
const testSubjects = getService('testSubjects');
const browser = getService('browser');
const es = getService('es');
const security = getService('security');

describe('Snapshot list', function () {
describe('Managed snapshots', function () {
const FIRST_SNAPSHOT_NAME = 'managed-snapshot-1';
const SECOND_SNAPSHOT_NAME = 'managed-snapshot-2';
const MANAGED_REPO_NAME = 'found-snapshots';

before(async () => {
await security.testUser.setRoles(['snapshot_restore_user'], { skipBrowserRefresh: true });
await pageObjects.common.navigateToApp('snapshotRestore');

// Set cluster settings for manager repository
await es.cluster.putSettings({
persistent: {
'cluster.metadata.managed_repository': 'found-snapshots',
},
});

// Create a managed repository
await es.snapshot.createRepository({
name: MANAGED_REPO_NAME,
type: 'fs',
settings: {
location: '/tmp/es-backups/',
compress: true,
},
verify: true,
});

// Create managed snapshots
await es.snapshot.create({
snapshot: FIRST_SNAPSHOT_NAME,
repository: MANAGED_REPO_NAME,
});

await es.snapshot.create({
snapshot: SECOND_SNAPSHOT_NAME,
repository: MANAGED_REPO_NAME,
});

// Wait for snapshots to be ready
await pageObjects.common.sleep(3000);

// Refresh page so that the snapshots show up in the snapshots table
await browser.refresh();
});

it('Last successful managed snapshot is non-deletable', async () => {
const snapshots = await pageObjects.snapshotRestore.getSnapshotList();
const lastSuccessfulSnapshotDeleteButton = snapshots.find(
(snapshot) => snapshot.snapshotName === SECOND_SNAPSHOT_NAME
)?.snapshotDelete;
expect(lastSuccessfulSnapshotDeleteButton).to.not.be(null);

const firstSuccessfulSnapshotDeleteButton = snapshots.find(
(snapshot) => snapshot.snapshotName === FIRST_SNAPSHOT_NAME
)?.snapshotDelete;
expect(firstSuccessfulSnapshotDeleteButton).to.not.be(null);

expect(await lastSuccessfulSnapshotDeleteButton?.isEnabled()).to.be(false);
expect(await firstSuccessfulSnapshotDeleteButton?.isEnabled()).to.be(true);
});

it('Last successful managed snapshot is correct when snapshots are filtered', async () => {
// Filter out last successful managed snapshot
await testSubjects.setValue('snapshotListSearch', FIRST_SNAPSHOT_NAME);
// Wait for filter to be applies
await pageObjects.common.sleep(1000);
const snapshots = await pageObjects.snapshotRestore.getSnapshotList();
const firstSuccessfulSnapshotDeleteButton = snapshots.find(
(snapshot) => snapshot.snapshotName === FIRST_SNAPSHOT_NAME
)?.snapshotDelete;
// Verify that the first successful snapshot is in the list and is deletable
expect(firstSuccessfulSnapshotDeleteButton).to.not.be(null);
expect(await firstSuccessfulSnapshotDeleteButton?.isEnabled()).to.be(true);

// Filter out first successful managed snapshot
await testSubjects.setValue('snapshotListSearch', SECOND_SNAPSHOT_NAME);
// Wait for filter to be applies
await pageObjects.common.sleep(1000);
const newSnapshots = await pageObjects.snapshotRestore.getSnapshotList();
const lastSuccessfulSnapshotDeleteButton = newSnapshots.find(
(snapshot) => snapshot.snapshotName === SECOND_SNAPSHOT_NAME
)?.snapshotDelete;
// Verify that the last successful snapshot is in the list and is non-deletable
expect(lastSuccessfulSnapshotDeleteButton).to.not.be(null);
expect(await lastSuccessfulSnapshotDeleteButton?.isEnabled()).to.be(false);
});

after(async () => {
await es.snapshot.delete({
snapshot: FIRST_SNAPSHOT_NAME,
repository: MANAGED_REPO_NAME,
});
await es.snapshot.delete({
snapshot: SECOND_SNAPSHOT_NAME,
repository: MANAGED_REPO_NAME,
});
await es.snapshot.deleteRepository({
name: MANAGED_REPO_NAME,
});
await security.testUser.restoreDefaults();
});
});
});
};
4 changes: 3 additions & 1 deletion x-pack/test/functional/page_objects/snapshot_restore_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ export function SnapshotRestorePageProvider({ getService }: FtrProviderContext)
return await Promise.all(
rows.map(async (row) => {
return {
snapshotName: await (await row.findByTestSubject('snapshotLink')).getVisibleText(),
snapshotLink: await row.findByTestSubject('snapshotLink'),
repoLink: await row.findByTestSubject('repositoryLink'),
snapshotRestore: row.findByTestSubject('srsnapshotListRestoreActionButton'),
snapshotRestore: await row.findByTestSubject('srsnapshotListRestoreActionButton'),
snapshotDelete: await row.findByTestSubject('srsnapshotListDeleteActionButton'),
};
})
);
Expand Down