[6.4] Fix bug caused by importing saved objects with missing index patterns (#20379, #20379, #22068, #22029) (#22432)

* Add detection of invalid JSON searchSource to saved_object and dashboard (#20379)

* Reenable import objects tests (#21250)

* Reenable import objects tests. Refine their assertions. Add primary callout to indicate completion of import process when the user has opted to not import anything.

* fixing importing saved objects when there's a missing index pattern (#22068)

* fixing issue with importing vis with missing saved search (#22029)
This commit is contained in:
CJ Cenizal 2018-08-28 08:31:39 -07:00 committed by GitHub
parent 4114120a70
commit 11e2fa1b6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 2897 additions and 54 deletions

View file

@ -28,7 +28,7 @@ import dashboardTemplate from './dashboard_app.html';
import dashboardListingTemplate from './listing/dashboard_listing_ng_wrapper.html';
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
import { SavedObjectNotFound } from 'ui/errors';
import { InvalidJSONProperty, SavedObjectNotFound } from 'ui/errors';
import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { recentlyAccessed } from 'ui/persisted_log';
@ -114,6 +114,13 @@ uiRoutes
return savedDashboard;
})
.catch((error) => {
// A corrupt dashboard was detected (e.g. with invalid JSON properties)
if (error instanceof InvalidJSONProperty) {
toastNotifications.addDanger(error.message);
kbnUrl.redirect(DashboardConstants.LANDING_PAGE_PATH);
return;
}
// Preserve BWC of v5.3.0 links for new, unsaved dashboards.
// See https://github.com/elastic/kibana/issues/10951 for more context.
if (error instanceof SavedObjectNotFound && id === 'create') {

File diff suppressed because one or more lines are too long

View file

@ -136,6 +136,7 @@ export class Flyout extends Component {
conflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs,
importedObjectCount,
failedImports,
} = await resolveSavedObjects(
contents,
isOverwriteAllChecked,
@ -166,6 +167,7 @@ export class Flyout extends Component {
conflictedIndexPatterns,
conflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs,
failedImports,
conflicts,
importCount: importedObjectCount,
isLoading: false,
@ -198,6 +200,7 @@ export class Flyout extends Component {
isOverwriteAllChecked,
conflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs,
failedImports
} = this.state;
const { services, indexPatterns } = this.props;
@ -237,6 +240,13 @@ export class Flyout extends Component {
indexPatterns,
isOverwriteAllChecked
);
this.setState({
loadingMessage: 'Retrying failed objects...',
});
importCount += await saveObjects(
failedImports.map(({ obj }) => obj),
isOverwriteAllChecked
);
} catch (e) {
this.setState({
error: e.message,
@ -373,6 +383,7 @@ export class Flyout extends Component {
isOverwriteAllChecked,
wasImportSuccessful,
importCount,
failedImports = [],
} = this.state;
if (isLoading) {
@ -389,9 +400,41 @@ export class Flyout extends Component {
);
}
if (wasImportSuccessful) {
if (failedImports.length && !this.hasConflicts) {
return (
<EuiCallOut title="Import successful" color="success" iconType="check">
<EuiCallOut
title="Import failed"
color="warning"
iconType="help"
>
<p>
Failed to import {failedImports.length} of {importCount + failedImports.length} objects.
</p>
<p>
{failedImports.map(({ error }) => error.message || '').join(' ')}
</p>
</EuiCallOut>
);
}
if (wasImportSuccessful) {
if (importCount === 0) {
return (
<EuiCallOut
data-test-subj="importSavedObjectsSuccessNoneImported"
title="No objects imported"
color="primary"
/>
);
}
return (
<EuiCallOut
data-test-subj="importSavedObjectsSuccess"
title="Import successful"
color="success"
iconType="check"
>
<p>Successfully imported {importCount} objects.</p>
</EuiCallOut>
);

View file

@ -163,6 +163,7 @@ export async function resolveSavedObjects(
// Keep track of how many we actually import because the user
// can cancel an override
let importedObjectCount = 0;
// Keep a record of any objects which fail to import for unknown reasons.
const failedImports = [];
// Start with the index patterns since everything is dependent on them
await awaitEachItemInParallel(
@ -199,13 +200,15 @@ export async function resolveSavedObjects(
if (await importDocument(obj, searchDoc, overwriteAll)) {
importedObjectCount++;
}
} catch (err) {
if (err instanceof SavedObjectNotFound) {
if (err.savedObjectType === 'index-pattern') {
} catch (error) {
if (error instanceof SavedObjectNotFound) {
if (error.savedObjectType === 'index-pattern') {
conflictedIndexPatterns.push({ obj, doc: searchDoc });
} else {
conflictedSearchDocs.push(searchDoc);
}
} else {
failedImports.push({ obj, error });
}
}
});
@ -217,15 +220,20 @@ export async function resolveSavedObjects(
if (await importDocument(obj, otherDoc, overwriteAll)) {
importedObjectCount++;
}
} catch (err) {
if (err instanceof SavedObjectNotFound) {
if (err.savedObjectType === 'index-pattern') {
} catch (error) {
if (error instanceof SavedObjectNotFound) {
if (error.savedObjectType === 'search') {
failedImports.push({ obj, error });
}
if (error.savedObjectType === 'index-pattern') {
if (obj.savedSearchId) {
conflictedSavedObjectsLinkedToSavedSearches.push(obj);
} else {
conflictedIndexPatterns.push({ obj, doc: otherDoc });
}
}
} else {
failedImports.push({ obj, error });
}
}
});
@ -235,5 +243,6 @@ export async function resolveSavedObjects(
conflictedSavedObjectsLinkedToSavedSearches,
conflictedSearchDocs,
importedObjectCount,
failedImports,
};
}

View file

@ -0,0 +1,87 @@
@-webkit-keyframes euiAnimFadeIn {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@keyframes euiAnimFadeIn {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-webkit-keyframes euiGrow {
0% {
opacity: 0; }
1% {
opacity: 0;
-webkit-transform: scale(0);
transform: scale(0); }
100% {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1); } }
@keyframes euiGrow {
0% {
opacity: 0; }
1% {
opacity: 0;
-webkit-transform: scale(0);
transform: scale(0); }
100% {
opacity: 1;
-webkit-transform: scale(1);
transform: scale(1); } }
/**
* Text truncation
*
* Prevent text from wrapping onto multiple lines, and truncate with an
* ellipsis.
*
* 1. Ensure that the node has a maximum width after which truncation can
* occur.
* 2. Fix for IE 8/9 if `word-wrap: break-word` is in effect on ancestor
* nodes.
*/
/**
* Set scroll bar appearance on Chrome.
*/
/**
* Specifically target IE11, but not Edge.
*/
@-webkit-keyframes focusRingAnimate {
0% {
-webkit-box-shadow: 0 0 0 6px rgba(0, 121, 165, 0);
box-shadow: 0 0 0 6px rgba(0, 121, 165, 0); }
100% {
-webkit-box-shadow: 0 0 0 2px rgba(0, 121, 165, 0.3);
box-shadow: 0 0 0 2px rgba(0, 121, 165, 0.3); } }
@keyframes focusRingAnimate {
0% {
-webkit-box-shadow: 0 0 0 6px rgba(0, 121, 165, 0);
box-shadow: 0 0 0 6px rgba(0, 121, 165, 0); }
100% {
-webkit-box-shadow: 0 0 0 2px rgba(0, 121, 165, 0.3);
box-shadow: 0 0 0 2px rgba(0, 121, 165, 0.3); } }
@-webkit-keyframes focusRingAnimateLarge {
0% {
-webkit-box-shadow: 0 0 0 10px rgba(0, 121, 165, 0);
box-shadow: 0 0 0 10px rgba(0, 121, 165, 0); }
100% {
-webkit-box-shadow: 0 0 0 4px rgba(0, 121, 165, 0.3);
box-shadow: 0 0 0 4px rgba(0, 121, 165, 0.3); } }
@keyframes focusRingAnimateLarge {
0% {
-webkit-box-shadow: 0 0 0 10px rgba(0, 121, 165, 0);
box-shadow: 0 0 0 10px rgba(0, 121, 165, 0); }
100% {
-webkit-box-shadow: 0 0 0 4px rgba(0, 121, 165, 0.3);
box-shadow: 0 0 0 4px rgba(0, 121, 165, 0.3); } }
.stsPage {
min-height: 100vh; }
/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL25vZGVfbW9kdWxlcy9AZWxhc3RpYy9ldWkvc3JjL2dsb2JhbF9zdHlsaW5nL3ZhcmlhYmxlcy9fYW5pbWF0aW9ucy5zY3NzIiwiLi4vLi4vLi4vLi4vbm9kZV9tb2R1bGVzL0BlbGFzdGljL2V1aS9zcmMvZ2xvYmFsX3N0eWxpbmcvbWl4aW5zL190eXBvZ3JhcGh5LnNjc3MiLCIuLi8uLi8uLi8uLi9ub2RlX21vZHVsZXMvQGVsYXN0aWMvZXVpL3NyYy9nbG9iYWxfc3R5bGluZy9taXhpbnMvX2hlbHBlcnMuc2NzcyIsIi4uLy4uLy4uLy4uL25vZGVfbW9kdWxlcy9AZWxhc3RpYy9ldWkvc3JjL2dsb2JhbF9zdHlsaW5nL21peGlucy9fc3RhdGVzLnNjc3MiLCIuLi8uLi8uLi8uLi9ub2RlX21vZHVsZXMvQGVsYXN0aWMvZXVpL3NyYy9nbG9iYWxfc3R5bGluZy92YXJpYWJsZXMvX2NvbG9ycy5zY3NzIiwiaW5kZXguc2NzcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFXQTtFQUNFO0lBQ0UsV0FBVSxFQUFBO0VBRVo7SUFDRSxXQUFVLEVBQUEsRUFBQTs7QUFMZDtFQUNFO0lBQ0UsV0FBVSxFQUFBO0VBRVo7SUFDRSxXQUFVLEVBQUEsRUFBQTs7QUFJZDtFQUNFO0lBQ0UsV0FBVSxFQUFBO0VBRVo7SUFDRSxXQUFVO0lBQ1YsNEJBQW1CO1lBQW5CLG9CQUFtQixFQUFBO0VBRXJCO0lBQ0UsV0FBVTtJQUNWLDRCQUFtQjtZQUFuQixvQkFBbUIsRUFBQSxFQUFBOztBQVZ2QjtFQUNFO0lBQ0UsV0FBVSxFQUFBO0VBRVo7SUFDRSxXQUFVO0lBQ1YsNEJBQW1CO1lBQW5CLG9CQUFtQixFQUFBO0VBRXJCO0lBQ0UsV0FBVTtJQUNWLDRCQUFtQjtZQUFuQixvQkFBbUIsRUFBQSxFQUFBOztBQ3lEdkI7Ozs7Ozs7Ozs7R0FVRztBQ2xFSDs7R0FFRztBQW9CSDs7R0FFRztBQ3ZDSDtFQUNFO0lBQ0UsbURDaEJxQjtZRGdCckIsMkNDaEJxQixFQUFBO0VEa0J2QjtJQUNFLHFEQ25CcUI7WURtQnJCLDZDQ25CcUIsRUFBQSxFQUFBO0FEY3pCO0VBQ0U7SUFDRSxtRENoQnFCO1lEZ0JyQiwyQ0NoQnFCLEVBQUE7RURrQnZCO0lBQ0UscURDbkJxQjtZRG1CckIsNkNDbkJxQixFQUFBLEVBQUE7O0FEdUJ6QjtFQUNFO0lBQ0Usb0RDekJxQjtZRHlCckIsNENDekJxQixFQUFBO0VEMkJ2QjtJQUNFLHFEQzVCcUI7WUQ0QnJCLDZDQzVCcUIsRUFBQSxFQUFBOztBRHVCekI7RUFDRTtJQUNFLG9EQ3pCcUI7WUR5QnJCLDRDQ3pCcUIsRUFBQTtFRDJCdkI7SUFDRSxxREM1QnFCO1lENEJyQiw2Q0M1QnFCLEVBQUEsRUFBQTs7QUNDekI7RUFDRSxrQkFBaUIsRUFDbEIiLCJmaWxlIjoidG8uY3NzIn0= */

View file

@ -25,8 +25,8 @@ import BluebirdPromise from 'bluebird';
import { SavedObjectProvider } from '../saved_object';
import { IndexPatternProvider } from '../../../index_patterns/_index_pattern';
import { SavedObjectsClientProvider } from '../../../saved_objects';
import { StubIndexPatternsApiClientModule } from '../../../index_patterns/__tests__/stub_index_patterns_api_client';
import { InvalidJSONProperty } from '../../../errors';
describe('Saved Object', function () {
require('test_utils/no_digest_promises').activateForSuite();
@ -300,6 +300,25 @@ describe('Saved Object', function () {
});
});
it('throws error invalid JSON is detected', async function () {
const savedObject = await createInitializedSavedObject({ type: 'dashboard', searchSource: true });
const response = {
found: true,
_source: {
kibanaSavedObjectMeta: {
searchSourceJSON: '\"{\\n \\\"filter\\\": []\\n}\"'
}
}
};
try {
await savedObject.applyESResp(response);
throw new Error('applyESResp should have failed, but did not.');
} catch (err) {
expect(err instanceof InvalidJSONProperty).to.be(true);
}
});
it('preserves original defaults if not overridden', function () {
const id = 'anid';
const preserveMeValue = 'here to stay!';

View file

@ -31,7 +31,7 @@
import angular from 'angular';
import _ from 'lodash';
import { SavedObjectNotFound } from '../../errors';
import { InvalidJSONProperty, SavedObjectNotFound } from '../../errors';
import MappingSetupProvider from '../../utils/mapping_setup';
import { SearchSourceProvider } from '../search_source';
@ -117,7 +117,15 @@ export function SavedObjectProvider(Promise, Private, Notifier, confirmModalProm
try {
searchSourceValues = JSON.parse(searchSourceJson);
} catch (e) {
searchSourceValues = {};
throw new InvalidJSONProperty(
`Invalid JSON in ${esType} "${this.id}". ${e.message} JSON: ${searchSourceJson}`
);
}
// This detects a scenario where documents with invalid JSON properties have been imported into the saved object index.
// (This happened in issue #20308)
if (!searchSourceValues || typeof searchSourceValues !== 'object') {
throw new InvalidJSONProperty(`Invalid searchSourceJSON in ${esType} "${this.id}".`);
}
const searchSourceFields = this.searchSource.getFields();

View file

@ -230,6 +230,17 @@ export class PersistedStateError extends KbnError {
}
}
/**
* This error is for scenarios where a saved object is detected that has invalid JSON properties.
* There was a scenario where we were importing objects with double-encoded JSON, and the system
* was silently failing. This error is now thrown in those scenarios.
*/
export class InvalidJSONProperty extends KbnError {
constructor(message) {
super(message);
}
}
/**
* UI Errors
*/

View file

@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const esArchiver = getService('esArchiver');
const PageObjects = getPageObjects(['common', 'settings', 'header']);
const testSubjects = getService('testSubjects');
describe('import objects', function describeIndexTests() {
beforeEach(async function () {
@ -53,7 +54,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects-conflicts.json'));
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.setImportIndexFieldOption(2);
await PageObjects.settings.clickConfirmConflicts();
await PageObjects.settings.clickConfirmChanges();
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.clickImportDone();
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
@ -62,56 +63,40 @@ export default function ({ getService, getPageObjects }) {
expect(isSavedObjectImported).to.be(true);
});
// Test should be testing that user is warned when saved object will override another because of an id collision
// This is not the case. Instead the test just loads a saved object with an index pattern conflict
// Disabling until the issue is resolved since the test does not test the intended behavior
it.skip('should allow for overrides', async function () {
it('should allow the user to override duplicate saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
// Put in data which already exists
// This data has already been loaded by the "visualize" esArchive. We'll load it again
// so that we can override the existing visualization.
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
// Wait for all the saves to happen
await PageObjects.header.waitUntilLoadingHasFinished();
// Interact with the conflict modal
await PageObjects.settings.setImportIndexFieldOption(2);
await PageObjects.settings.clickConfirmConflicts();
// Now confirm we want to override
await PageObjects.common.clickConfirmOnModal();
// Wait for all the saves to happen
await PageObjects.header.waitUntilLoadingHasFinished();
// Finish the flyout
await PageObjects.settings.clickImportDone();
// Wait...
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
expect(objects.length).to.be(2);
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.setImportIndexFieldOption(2);
await PageObjects.settings.clickConfirmChanges();
// Override the visualization.
await PageObjects.common.clickConfirmOnModal();
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccess');
expect(isSuccessful).to.be(true);
});
// Test should be testing that user is warned when saved object will overrides another because of an id collision
// This is not the case. Instead the test just loads a saved object with an index pattern conflict
// Disabling until the issue is resolved since the test does not test the intended behavior
it.skip('should allow for cancelling overrides', async function () {
it('should allow the user to cancel overriding duplicate saved objects', async function () {
await PageObjects.settings.clickKibanaSavedObjects();
// Put in data which already exists
// This data has already been loaded by the "visualize" esArchive. We'll load it again
// so that we can be prompted to override the existing visualization.
await PageObjects.settings.importFile(path.join(__dirname, 'exports', '_import_objects_exists.json'), false);
// Wait for all the saves to happen
await PageObjects.header.waitUntilLoadingHasFinished();
// Interact with the conflict modal
await PageObjects.settings.setImportIndexFieldOption(2);
await PageObjects.settings.clickConfirmConflicts();
// Now cancel the override
await PageObjects.common.clickCancelOnModal();
// Wait for all saves to happen
await PageObjects.header.waitUntilLoadingHasFinished();
// Finish the flyout
await PageObjects.settings.clickImportDone();
// Wait for table to refresh
await PageObjects.settings.waitUntilSavedObjectsTableIsNotLoading();
const objects = await PageObjects.settings.getSavedObjectsInTable();
expect(objects.length).to.be(2);
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.settings.setImportIndexFieldOption(2);
await PageObjects.settings.clickConfirmChanges();
// *Don't* override the visualization.
await PageObjects.common.clickCancelOnModal();
const isSuccessful = await testSubjects.exists('importSavedObjectsSuccessNoneImported');
expect(isSuccessful).to.be(true);
});
it('should import saved objects linked to saved searches', async function () {

View file

@ -578,7 +578,7 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await testSubjects.click('importSavedObjectsDoneBtn');
}
async clickConfirmConflicts() {
async clickConfirmChanges() {
await testSubjects.click('importSavedObjectsConfirmBtn');
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long