[Dashboard] Make Dashboard Saved Objects multiple-isolated (#115817)

* Make Dashboard SO multiple-isolated

* Fix integration tests

* Fix Saved Objects API Integration Tests

* Fix more tests

* Fix even more tests

Co-authored-by: Joe Portner <5295965+jportner@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Corey Robertson 2021-10-25 19:21:02 -04:00 committed by GitHub
parent 6d6cb5c836
commit edc43c0ff2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 107 additions and 61 deletions

View file

@ -19,7 +19,8 @@ export const createDashboardSavedObjectType = ({
}): SavedObjectsType => ({
name: 'dashboard',
hidden: false,
namespaceType: 'single',
namespaceType: 'multiple-isolated',
convertToMultiNamespaceTypeVersion: '8.0.0',
management: {
icon: 'dashboardApp',
defaultSearchField: 'title',

View file

@ -202,7 +202,7 @@ export default function ({ getService }: FtrProviderContext) {
path: '/app/dashboards#/view/b70c7ae0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'dashboard.show',
},
namespaceType: 'single',
namespaceType: 'multiple-isolated',
});
}));

View file

@ -301,7 +301,7 @@ export default function ({ getService }: FtrProviderContext) {
path: '/app/dashboards#/view/b70c7ae0-3224-11e8-a572-ffca06da1357',
uiCapabilitiesPath: 'dashboard.show',
},
namespaceType: 'single',
namespaceType: 'multiple-isolated',
hiddenType: false,
},
},

View file

@ -134,6 +134,7 @@
"uiStateJSON": "{}",
"version": 1
},
"namespaces": ["default"],
"type": "dashboard",
"updated_at": "2017-09-21T18:57:40.826Z"
},
@ -205,7 +206,7 @@
{
"type": "doc",
"value": {
"id": "space_1:dashboard:space1-dashboard-id",
"id": "dashboard:space1-dashboard-id",
"index": ".kibana",
"source": {
"dashboard": {
@ -228,7 +229,7 @@
"uiStateJSON": "{}",
"version": 1
},
"namespace": "space_1",
"namespaces": ["space_1"],
"type": "dashboard",
"updated_at": "2017-09-21T18:57:40.826Z"
},
@ -300,7 +301,7 @@
{
"type": "doc",
"value": {
"id": "space_2:dashboard:space2-dashboard-id",
"id": "dashboard:space2-dashboard-id",
"index": ".kibana",
"source": {
"dashboard": {
@ -323,7 +324,7 @@
"uiStateJSON": "{}",
"version": 1
},
"namespace": "space_2",
"namespaces": ["space_2"],
"type": "dashboard",
"updated_at": "2017-09-21T18:57:40.826Z"
},

View file

@ -52,7 +52,7 @@ export const TEST_CASES: Record<string, ImportTestCase> = Object.freeze({
expectedNewId: `${CID}3`,
}),
CONFLICT_4_OBJ: Object.freeze({ type: 'sharedtype', id: `${CID}4`, expectedNewId: `${CID}4a` }),
NEW_SINGLE_NAMESPACE_OBJ: Object.freeze({ type: 'dashboard', id: 'new-dashboard-id' }),
NEW_SINGLE_NAMESPACE_OBJ: Object.freeze({ type: 'isolatedtype', id: 'new-isolatedtype-id' }),
NEW_MULTI_NAMESPACE_OBJ: Object.freeze({ type: 'sharedtype', id: 'new-sharedtype-id' }),
NEW_NAMESPACE_AGNOSTIC_OBJ: Object.freeze({ type: 'globaltype', id: 'new-globaltype-id' }),
});

View file

@ -130,7 +130,6 @@ export default function ({ getService }: FtrProviderContext) {
spaceId,
singleRequest,
responseBodyOverride: expectSavedObjectForbidden([
'dashboard',
'globaltype',
'isolatedtype',
'sharedtype',
@ -152,11 +151,7 @@ export default function ({ getService }: FtrProviderContext) {
overwrite,
spaceId,
singleRequest,
responseBodyOverride: expectSavedObjectForbidden([
'dashboard',
'globaltype',
'isolatedtype',
]),
responseBodyOverride: expectSavedObjectForbidden(['globaltype', 'isolatedtype']),
}),
createTestDefinitions(group2, true, { overwrite, spaceId, singleRequest }),
createTestDefinitions(group3, true, { overwrite, spaceId, singleRequest }),

View file

@ -56,15 +56,11 @@
{
"type": "_doc",
"value": {
"id": "space_2:dashboard:my_dashboard",
"id": "isolatedtype:my_isolated_object",
"index": ".kibana",
"source": {
"dashboard": {
"description": "Space 2",
"title": "This is the second test space"
},
"namespace": "space_2",
"type": "dashboard",
"type": "isolatedtype",
"updated_at": "2017-09-21T18:49:16.270Z"
},
"type": "_doc"
@ -74,15 +70,11 @@
{
"type": "_doc",
"value": {
"id": "space_1:dashboard:my_dashboard",
"id": "isolatedtype:my_isolated_object",
"index": ".kibana",
"source": {
"dashboard": {
"description": "Space 1",
"title": "This is the second test space"
},
"namespace": "space_1",
"type": "dashboard",
"type": "isolatedtype",
"updated_at": "2017-09-21T18:49:16.270Z"
},
"type": "_doc"
@ -92,14 +84,10 @@
{
"type": "_doc",
"value": {
"id": "dashboard:my_dashboard",
"id": "isolatedtype:my_isolated_object",
"index": ".kibana",
"source": {
"dashboard": {
"description": "Default Space",
"title": "This is the default test space"
},
"type": "dashboard",
"type": "isolatedtype",
"updated_at": "2017-09-21T18:49:16.270Z"
},
"type": "_doc"
@ -109,9 +97,10 @@
{
"type": "_doc",
"value": {
"id": "dashboard:cts_dashboard",
"id": "dashboard:cts_dashboard_default",
"index": ".kibana",
"source": {
"originId": "cts_dashboard",
"dashboard": {
"description": "Copy to Space Dashboard from the default space",
"title": "This is the default test space CTS dashboard"
@ -130,7 +119,8 @@
"name": "CTS Vis 3"
}],
"type": "dashboard",
"updated_at": "2017-09-21T18:49:16.270Z"
"updated_at": "2017-09-21T18:49:16.270Z",
"namespaces": ["default"]
},
"type": "_doc"
}
@ -227,9 +217,10 @@
{
"type": "_doc",
"value": {
"id": "space_1:dashboard:cts_dashboard",
"id": "dashboard:cts_dashboard_space_1",
"index": ".kibana",
"source": {
"originId": "cts_dashboard",
"dashboard": {
"description": "Copy to Space Dashboard from space_1 space",
"title": "This is the space_1 test space CTS dashboard"
@ -253,7 +244,7 @@
],
"type": "dashboard",
"updated_at": "2017-09-21T18:49:16.270Z",
"namespace": "space_1"
"namespaces": ["space_1"]
},
"type": "_doc"
}

View file

@ -29,6 +29,23 @@ export class Plugin {
},
},
});
core.savedObjects.registerType({
name: 'isolatedtype',
hidden: false,
namespaceType: 'single',
management: {
icon: 'beaker',
importableAndExportable: true,
getTitle(obj) {
return obj.attributes.title;
},
},
mappings: {
properties: {
title: { type: 'text' },
},
},
});
}
public start() {

View file

@ -64,9 +64,8 @@ interface SpaceBucket {
}
const INITIAL_COUNTS: Record<string, Record<string, number>> = {
[DEFAULT_SPACE_ID]: { dashboard: 2, visualization: 3, 'index-pattern': 1 },
space_1: { dashboard: 2, visualization: 3, 'index-pattern': 1 },
space_2: { dashboard: 1 },
[DEFAULT_SPACE_ID]: { dashboard: 1, visualization: 3, 'index-pattern': 1 },
space_1: { dashboard: 1, visualization: 3, 'index-pattern': 1 },
};
const UUID_PATTERN = new RegExp(
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
@ -148,18 +147,23 @@ export function copyToSpaceTestSuiteFactory(
(spaceId: string, destination: string, expectedDashboardCount: number) =>
async (resp: TestResponse) => {
const result = resp.body as CopyResponse;
const dashboardDestinationId = result[destination].successResults![0].destinationId;
expect(dashboardDestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID
expect(result).to.eql({
[destination]: {
success: true,
successCount: 1,
successResults: [
{
id: 'cts_dashboard',
id: `cts_dashboard_${spaceId}`,
type: 'dashboard',
meta: {
title: `This is the ${spaceId} test space CTS dashboard`,
icon: 'dashboardApp',
},
destinationId: dashboardDestinationId,
},
],
},
@ -172,7 +176,7 @@ export function copyToSpaceTestSuiteFactory(
};
const expectNoConflictsWithoutReferencesResult = (spaceId: string = DEFAULT_SPACE_ID) =>
createExpectNoConflictsWithoutReferencesForSpace(spaceId, getDestinationWithoutConflicts(), 2);
createExpectNoConflictsWithoutReferencesForSpace(spaceId, getDestinationWithoutConflicts(), 1);
const expectNoConflictsForNonExistentSpaceResult = (spaceId: string = DEFAULT_SPACE_ID) =>
createExpectNoConflictsWithoutReferencesForSpace(spaceId, 'non_existent_space', 1);
@ -191,6 +195,8 @@ export function copyToSpaceTestSuiteFactory(
expect(vis2DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID
const vis3DestinationId = result[destination].successResults![3].destinationId;
expect(vis3DestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID
const dashboardDestinationId = result[destination].successResults![4].destinationId;
expect(dashboardDestinationId).to.match(UUID_PATTERN); // this was copied to space 2 and hit an unresolvable conflict, so the object ID was regenerated silently / the destinationId is a UUID
expect(result).to.eql({
[destination]: {
@ -225,12 +231,13 @@ export function copyToSpaceTestSuiteFactory(
destinationId: vis3DestinationId,
},
{
id: 'cts_dashboard',
id: `cts_dashboard_${spaceId}`,
type: 'dashboard',
meta: {
icon: 'dashboardApp',
title: `This is the ${spaceId} test space CTS dashboard`,
},
destinationId: dashboardDestinationId,
},
],
},
@ -238,7 +245,7 @@ export function copyToSpaceTestSuiteFactory(
// Query ES to ensure that we copied everything we expected
await assertSpaceCounts(destination, {
dashboard: 2,
dashboard: 1,
visualization: 3,
'index-pattern': 1,
});
@ -353,13 +360,14 @@ export function copyToSpaceTestSuiteFactory(
destinationId: `cts_vis_3_${destination}`, // this conflicted with another visualization in the destination space because of a shared originId
},
{
id: 'cts_dashboard',
id: `cts_dashboard_${spaceId}`,
type: 'dashboard',
meta: {
icon: 'dashboardApp',
title: `This is the ${spaceId} test space CTS dashboard`,
},
overwrite: true,
destinationId: `cts_dashboard_${destination}`, // this conflicted with another dashboard in the destination space because of a shared originId
},
],
},
@ -367,7 +375,7 @@ export function copyToSpaceTestSuiteFactory(
// Query ES to ensure that we copied everything we expected
await assertSpaceCounts(destination, {
dashboard: 2,
dashboard: 1,
visualization: 5,
'index-pattern': 1,
});
@ -403,8 +411,11 @@ export function copyToSpaceTestSuiteFactory(
];
const expectedErrors = [
{
error: { type: 'conflict' },
id: 'cts_dashboard',
error: {
type: 'conflict',
destinationId: `cts_dashboard_${destination}`, // this conflicted with another dashboard in the destination space because of a shared originId
},
id: `cts_dashboard_${spaceId}`,
title: `This is the ${spaceId} test space CTS dashboard`,
type: 'dashboard',
meta: {
@ -662,7 +673,7 @@ export function copyToSpaceTestSuiteFactory(
)
);
const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' };
const dashboardObject = { type: 'dashboard', id: `cts_dashboard_${spaceId}` };
it(`should return ${tests.noConflictsWithoutReferences.statusCode} when copying to space without conflicts or references`, async () => {
const destination = getDestinationWithoutConflicts();

View file

@ -65,28 +65,28 @@ export function deleteTestSuiteFactory(
const expectedBuckets = [
{
key: 'default',
doc_count: 8,
doc_count: 7,
countByType: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{ key: 'visualization', doc_count: 3 },
{ key: 'dashboard', doc_count: 2 },
{ key: 'space', doc_count: 2 }, // since space objects are namespace-agnostic, they appear in the "default" agg bucket
{ key: 'dashboard', doc_count: 1 },
{ key: 'index-pattern', doc_count: 1 },
// legacy-url-alias objects cannot exist for the default space
],
},
},
{
doc_count: 7,
doc_count: 6,
key: 'space_1',
countByType: {
doc_count_error_upper_bound: 0,
sum_other_doc_count: 0,
buckets: [
{ key: 'visualization', doc_count: 3 },
{ key: 'dashboard', doc_count: 2 },
{ key: 'dashboard', doc_count: 1 },
{ key: 'index-pattern', doc_count: 1 },
{ key: 'legacy-url-alias', doc_count: 1 }, // alias (1)
],

View file

@ -43,7 +43,7 @@ const {
export const TEST_CASE_OBJECTS: Record<string, { type: string; id: string }> = deepFreeze({
SHAREABLE_TYPE: { type: 'sharedtype', id: CASES.EACH_SPACE.id }, // contains references to four other objects
SHAREABLE_TYPE_DOES_NOT_EXIST: { type: 'sharedtype', id: 'does-not-exist' },
NON_SHAREABLE_TYPE: { type: 'dashboard', id: 'my_dashboard' }, // one of these exists in each space
NON_SHAREABLE_TYPE: { type: 'isolatedtype', id: 'my_isolated_object' }, // one of these exists in each space
});
// Expected results for each space are defined here since they are used in multiple test suites
export const EXPECTED_RESULTS: Record<string, SavedObjectReferenceWithContext[]> = {

View file

@ -63,7 +63,7 @@ export function resolveCopyToSpaceConflictsSuite(
};
const getDashboardAtSpace = async (spaceId: string): Promise<SavedObject<any>> => {
return supertestWithAuth
.get(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/cts_dashboard`)
.get(`${getUrlPrefix(spaceId)}/api/saved_objects/dashboard/cts_dashboard_${spaceId}`)
.then((response: any) => response.body);
};
@ -124,12 +124,13 @@ export function resolveCopyToSpaceConflictsSuite(
successCount: 1,
successResults: [
{
id: 'cts_dashboard',
id: `cts_dashboard_${sourceSpaceId}`,
type: 'dashboard',
meta: {
title: `This is the ${sourceSpaceId} test space CTS dashboard`,
icon: 'dashboardApp',
},
destinationId: `cts_dashboard_${destinationSpaceId}`, // this conflicted with another dashboard in the destination space because of a shared originId
overwrite: true,
},
],
@ -204,8 +205,11 @@ export function resolveCopyToSpaceConflictsSuite(
successCount: 0,
errors: [
{
error: { type: 'conflict' },
id: 'cts_dashboard',
error: {
type: 'conflict',
destinationId: `cts_dashboard_${destination}`, // this conflicted with another visualization in the destination space because of a shared originId
},
id: `cts_dashboard_${sourceSpaceId}`,
type: 'dashboard',
title: `This is the ${sourceSpaceId} test space CTS dashboard`,
meta: {
@ -442,7 +446,7 @@ export function resolveCopyToSpaceConflictsSuite(
)
);
const dashboardObject = { type: 'dashboard', id: 'cts_dashboard' };
const dashboardObject = { type: 'dashboard', id: `cts_dashboard_${spaceId}` };
const visualizationObject = { type: 'visualization', id: `cts_vis_3_${spaceId}` };
const indexPatternObject = { type: 'index-pattern', id: `cts_ip_1_${spaceId}` };
@ -514,7 +518,15 @@ export function resolveCopyToSpaceConflictsSuite(
objects: [dashboardObject],
includeReferences: false,
createNewCopies: false,
retries: { [destination]: [{ ...dashboardObject, overwrite: true }] },
retries: {
[destination]: [
{
...dashboardObject,
destinationId: `cts_dashboard_${destination}`,
overwrite: true,
},
],
},
})
.expect(tests.withoutReferencesOverwriting.statusCode)
.then(tests.withoutReferencesOverwriting.response);
@ -530,7 +542,15 @@ export function resolveCopyToSpaceConflictsSuite(
objects: [dashboardObject],
includeReferences: false,
createNewCopies: false,
retries: { [destination]: [{ ...dashboardObject, overwrite: false }] },
retries: {
[destination]: [
{
...dashboardObject,
destinationId: `cts_dashboard_${destination}`,
overwrite: false,
},
],
},
})
.expect(tests.withoutReferencesNotOverwriting.statusCode)
.then(tests.withoutReferencesNotOverwriting.response);
@ -546,7 +566,17 @@ export function resolveCopyToSpaceConflictsSuite(
objects: [dashboardObject],
includeReferences: false,
createNewCopies: false,
retries: { [destination]: [{ ...dashboardObject, overwrite: true }] },
retries: {
[destination]: [
{
...dashboardObject,
destinationId: `cts_dashboard_${destination}`,
// realistically a retry wouldn't use a destinationId, because it wouldn't have an origin conflict with another
// object in a non-existent space, but for the simplicity of testing we'll use this here
overwrite: true,
},
],
},
})
.expect(tests.nonExistentSpace.statusCode)
.then(tests.nonExistentSpace.response);