Skip to content

Commit

Permalink
[6.x] Delete objects belonging to removed space (#23640) (#24033)
Browse files Browse the repository at this point in the history
Backports the following commits to 6.x:
 - Delete objects belonging to removed space  (#23640)
  • Loading branch information
legrego authored Oct 15, 2018
1 parent 2f7797b commit 4a4c1e5
Show file tree
Hide file tree
Showing 15 changed files with 400 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SavedObjectsRepository #deleteByNamespace requires namespace to be a string 1`] = `"namespace is required, and must be a string"`;

exports[`SavedObjectsRepository #deleteByNamespace requires namespace to be defined 1`] = `"namespace is required, and must be a string"`;
34 changes: 33 additions & 1 deletion src/server/saved_objects/service/lib/repository.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/

import { omit } from 'lodash';
import { getRootType } from '../../../mappings';
import { getRootType, getRootPropertiesObjects } from '../../../mappings';
import { getSearchDsl } from './search_dsl';
import { includedFields } from './included_fields';
import { decorateEsError } from './decorate_es_error';
Expand Down Expand Up @@ -245,6 +245,38 @@ export class SavedObjectsRepository {
);
}

/**
* Deletes all objects from the provided namespace.
*
* @param {string} namespace
* @returns {promise} - { took, timed_out, total, deleted, batches, version_conflicts, noops, retries, failures }
*/
async deleteByNamespace(namespace) {

if (!namespace || typeof namespace !== 'string') {
throw new TypeError(`namespace is required, and must be a string`);
}

const allTypes = Object.keys(getRootPropertiesObjects(this._mappings));

const typesToDelete = allTypes.filter(type => !this._schema.isNamespaceAgnostic(type));

const esOptions = {
index: this._index,
ignore: [404],
refresh: 'wait_for',
body: {
conflicts: 'proceed',
...getSearchDsl(this._mappings, this._schema, {
namespace,
type: typesToDelete,
})
}
};

return await this._writeToCluster('deleteByQuery', esOptions);
}

/**
* @param {object} [options={}]
* @property {(string|Array<string>)} [options.type]
Expand Down
66 changes: 66 additions & 0 deletions src/server/saved_objects/service/lib/repository.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,21 @@ describe('SavedObjectsRepository', () => {
}
};

const deleteByQueryResults = {
took: 27,
timed_out: false,
total: 23,
deleted: 23,
batches: 1,
version_conflicts: 0,
noops: 0,
retries: { bulk: 0, search: 0 },
throttled_millis: 0,
requests_per_second: -1,
throttled_until_millis: 0,
failures: []
};

const mappings = {
doc: {
properties: {
Expand All @@ -171,6 +186,20 @@ describe('SavedObjectsRepository', () => {
type: 'keyword'
}
}
},
'dashboard': {
properties: {
otherField: {
type: 'keyword'
}
}
},
'globaltype': {
properties: {
yetAnotherField: {
type: 'keyword'
}
}
}
}
}
Expand Down Expand Up @@ -708,6 +737,43 @@ describe('SavedObjectsRepository', () => {
});
});

describe('#deleteByNamespace', () => {
it('requires namespace to be defined', async () => {
callAdminCluster.returns(deleteByQueryResults);
expect(savedObjectsRepository.deleteByNamespace()).rejects.toThrowErrorMatchingSnapshot();
sinon.assert.notCalled(callAdminCluster);
sinon.assert.notCalled(onBeforeWrite);
});

it('requires namespace to be a string', async () => {
callAdminCluster.returns(deleteByQueryResults);
expect(savedObjectsRepository.deleteByNamespace(['namespace-1', 'namespace-2'])).rejects.toThrowErrorMatchingSnapshot();
sinon.assert.notCalled(callAdminCluster);
sinon.assert.notCalled(onBeforeWrite);
});

it('constructs a deleteByQuery call using all types that are namespace aware', async () => {
callAdminCluster.returns(deleteByQueryResults);
const result = await savedObjectsRepository.deleteByNamespace('my-namespace');

expect(result).toEqual(deleteByQueryResults);
sinon.assert.calledOnce(callAdminCluster);
sinon.assert.calledOnce(onBeforeWrite);

sinon.assert.calledWithExactly(getSearchDsl, mappings, schema, {
namespace: 'my-namespace',
type: ['index-pattern', 'dashboard']
});

sinon.assert.calledWithExactly(callAdminCluster, 'deleteByQuery', {
body: { conflicts: 'proceed' },
ignore: [404],
index: '.kibana-test',
refresh: 'wait_for'
});
});
});

describe('#find', () => {
it('waits until migrations are complete before proceeding', async () => {
migrator.awaitMigration = sinon.spy(async () => sinon.assert.notCalled(callAdminCluster));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,52 +2,92 @@

exports[`ConfirmDeleteModal renders as expected 1`] = `
<EuiOverlayMask>
<EuiConfirmModal
buttonColor="danger"
cancelButtonText="Cancel"
confirmButtonText="Delete space"
defaultFocusedButton="cancel"
onCancel={[MockFunction]}
onConfirm={[Function]}
title="Delete space 'My Space'"
<EuiModal
className="euiModal--confirmation"
maxWidth={true}
onClose={[MockFunction]}
>
<p>
Deleting a space permanently removes the space and all of its contents. You can't undo this action.
</p>
<EuiFormRow
describedByIds={Array []}
error="Space names do not match."
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Confirm space name"
>
<EuiFieldText
compressed={false}
fullWidth={false}
isLoading={false}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<EuiCallOut
color="warning"
size="m"
>
<EuiModalHeader>
<EuiModalHeaderTitle
data-test-subj="confirmModalTitleText"
>
Delete space
'My Space'
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText
data-test-subj="confirmModalBodyText"
grow={true}
>
You are about to delete your current space
<span>
(
<p>
Deleting a space permanently removes the space and
<strong>
My Space
all of its contents
</strong>
)
</span>
. You will be redirected to choose a different space if you continue.
. You can't undo this action.
</p>
<EuiFormRow
describedByIds={Array []}
error="Space names do not match."
fullWidth={false}
hasEmptyLabelSpace={false}
isInvalid={false}
label="Confirm space name"
>
<EuiFieldText
compressed={false}
disabled={false}
fullWidth={false}
isLoading={false}
onChange={[Function]}
value=""
/>
</EuiFormRow>
<EuiCallOut
color="warning"
size="m"
>
<EuiText
grow={true}
>
You are about to delete your current space
<span>
(
<strong>
My Space
</strong>
)
</span>
. You will be redirected to choose a different space if you continue.
</EuiText>
</EuiCallOut>
</EuiText>
</EuiCallOut>
</EuiConfirmModal>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty
color="primary"
data-test-subj="confirmModalCancelButton"
iconSide="left"
isDisabled={false}
onClick={[MockFunction]}
type="button"
>
Cancel
</EuiButtonEmpty>
<EuiButton
color="danger"
data-test-subj="confirmModalConfirmButton"
fill={true}
iconSide="left"
isLoading={false}
onClick={[Function]}
type="button"
>
Delete space and all contents
</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
`;
Loading

0 comments on commit 4a4c1e5

Please sign in to comment.