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

[Snapshot Restore] Fix logic for finding last successful managed snapshot #159324

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();
});
});
});
};
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