Skip to content

Commit

Permalink
Implements downward compatible update to support mixed kibana versions
Browse files Browse the repository at this point in the history
  • Loading branch information
TinaHeiligers committed Jun 27, 2023
1 parent c0047a1 commit b24b41d
Show file tree
Hide file tree
Showing 14 changed files with 1,400 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
*/

import type { PublicMethodsOf } from '@kbn/utility-types';
import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal';
import {
isNotFoundFromUnsupportedServer,
isSupportedEsServer,
} from '@kbn/core-elasticsearch-server-internal';
import type {
ISavedObjectTypeRegistry,
ISavedObjectsSerializer,
Expand Down Expand Up @@ -105,6 +108,69 @@ export class PreflightCheckHelper {
return bulkGetMultiNamespaceDocsResponse;
}

/**
* Pre-flight check to ensure that a multi-namespace object exists in the current namespace.
checkDocResult
unsafeRawDoc
* if a multi-namespace type isn't found it might still exist in another namespace
*/
public async preflightGetDoc({
type,
id,
namespace,
}: PreflightGetDocParams): Promise<PreflightGetDocResult> {
const { body, statusCode, headers } = await this.client.get<SavedObjectsRawDocSource>(
{
id: this.serializer.generateRawId(namespace, type, id),
index: this.getIndexForType(type),
},
{ ignore: [404], meta: true }
);
const indexFound = statusCode !== 404;

if (!indexFound && !isSupportedEsServer(headers)) {
// checking if the 404 is from Elasticsearch
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}

if (!isFoundGetResponse(body) || !indexFound) {
return {
checkDocFound: 'not_found',
};
}

return {
checkDocFound: 'found',
unsafeRawDoc: body,
};
}

/**
* Pre-flight check to ensure that a multi-namespace object exists in the current namespace for update API.
*/
public async preflightCheckNamespacesForUpdate({
type,
id,
namespace,
initialNamespaces,
rawDocSource,
}: PreflightCheckNamespacesParams): Promise<PreflightCheckNamespacesResult> {
const namespaces = initialNamespaces ?? [SavedObjectsUtils.namespaceIdToString(namespace)];
if (rawDocSource && isFoundGetResponse(rawDocSource)) {
if (!rawDocExistsInNamespaces(this.registry, rawDocSource, namespaces)) {
return { checkResult: 'found_outside_namespace' };
}
return {
checkResult: 'found_in_namespace',
savedObjectNamespaces:
initialNamespaces ?? getSavedObjectNamespaces(namespace, rawDocSource),
};
}
return {
checkResult: 'not_found',
savedObjectNamespaces: initialNamespaces ?? getSavedObjectNamespaces(namespace),
};
}
/**
* Pre-flight check to ensure that a multi-namespace object exists in the current namespace.
*/
Expand Down Expand Up @@ -174,6 +240,43 @@ export class PreflightCheckHelper {
// any other error from this check does not matter
}
}
/**
* @internal
*/
export interface PreflightGetDocParams {
/** The object type to fetch */
type: string;
/** The object ID to fetch */
id: string;
/** The current space */
namespace: string | undefined;
}

/**
* @internal
*/
export interface PreflightGetDocResult {
/** If the object exists, and whether or not it exists in the current space */
checkDocFound: 'not_found' | 'found';
/** The source of the raw document, if the object already exists in the server's version (unsafe to use) */
unsafeRawDoc?: GetResponseFound<SavedObjectsRawDocSource>;
}

/**
* @internal
*/
export interface PreflightCheckNamespacesParams {
/** The object type to fetch */
type: string;
/** The object ID to fetch */
id: string;
/** The current space */
namespace: string | undefined;
/** Optional; for an object that is being created, this specifies the initial namespace(s) it will exist in (overriding the current space) */
initialNamespaces?: string[];
/** Optional; for a pre-fetched object */
rawDocSource?: GetResponseFound<SavedObjectsRawDocSource>;
}

/**
* @internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { performFind } from './find';
export { performBulkGet } from './bulk_get';
export { performGet } from './get';
export { performUpdate } from './update';
// export { performBWCUpdate } from './update_bwc';
export { performBulkUpdate } from './bulk_update';
export { performRemoveReferencesTo } from './remove_references_to';
export { performOpenPointInTime } from './open_point_in_time';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ export {
type InternalBulkResolveParams,
type InternalSavedObjectsBulkResolveResponse,
} from './internal_bulk_resolve';
export {
type InternalUpsertParams,
type SavedObjectInternalUpsertResponse,
// internalUpsert,
} from './internal_upsert';
export { type InternalUpdateParams, internalUpdate } from './internal_update';
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { isNotFoundFromUnsupportedServer } from '@kbn/core-elasticsearch-server-internal';
import { MutatingOperationRefreshSetting } from '@kbn/core-saved-objects-api-server/src/apis';
import { decodeRequestVersion } from '@kbn/core-saved-objects-base-server-internal';
import {
type ISavedObjectsSerializer,
SavedObjectSanitizedDoc,
SavedObjectsErrorHelpers,
SavedObjectReference,
SavedObject,
} from '@kbn/core-saved-objects-server';
import { cloneDeep } from 'lodash';
import type { RepositoryEsClient } from '../../repository_es_client';

import type { RepositoryHelpers } from '../helpers';
import { getCurrentTime } from '../utils';

export interface InternalUpdateParams<T = unknown> {
// so info
type: string;
id: string;
namespace?: string;
references?: SavedObjectReference[];
migrationVersionCompatibility?: 'compatible' | 'raw';
version?: string;
// apiContext
helpers: RepositoryHelpers;
client: RepositoryEsClient;
serializer: ISavedObjectsSerializer;
// update request & options. attributes are the ones we want to replace in the original doc
attributes: T;
refresh: MutatingOperationRefreshSetting;
// doc we retrieved from type & id, migrated to this kibana version
migrated: SavedObject<T>;
}

export const internalUpdate = async <T>({
namespace,
references,
migrationVersionCompatibility,
version,
helpers,
client,
serializer,
type,
id,
attributes,
refresh,
migrated,
}: InternalUpdateParams): Promise<SavedObjectSanitizedDoc<T>> => {
const {
common: commonHelper,
// validation: validationHelper,
encryption: encryptionHelper,
migration: migrationHelper,
} = helpers;

const time = getCurrentTime();
const originalMigratedDoc = cloneDeep(migrated!) as SavedObject<T>; // `canUpdate` assures we have a doc to work with

const updatedDoc = {
...migrated,
attributes: {
...originalMigratedDoc.attributes,
...(attributes as T),
},
};
const doc = migrationHelper.migrateInputDocument({
...updatedDoc,
attributes: {
...(await encryptionHelper.optionallyEncryptAttributes(
type,
id,
namespace,
updatedDoc.attributes
)),
},
updated_at: time,
...(Array.isArray(references) && { references }),
});
// try {
// validationHelper.validateObjectForCreate(type, doc as SavedObjectSanitizedDoc<T>);
// } catch (err) {
// swollow error: `coreMigrationVersion` belongs to higher kibana version than current (2.0.0)
// kibana version hardcoded to 2.0.0 in repo setup for api unit tests
// I can't get around that, even though we ignore the validation for now.
// }

const raw = serializer.savedObjectToRaw(doc as SavedObjectSanitizedDoc<T>);

const updatedDocRequestParams = {
id: raw._id,
index: commonHelper.getIndexForType(type),
refresh,
body: raw._source,
...(version ? decodeRequestVersion(version) : {}),
require_alias: true,
};

const {
body: indexBody,
statusCode: indexStatusCode,
headers: indexHeaders,
} = await client.index(updatedDocRequestParams, { meta: true });

if (isNotFoundFromUnsupportedServer({ statusCode: indexStatusCode, headers: indexHeaders })) {
throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(id, type);
}
// console.log('indexBody', JSON.stringify(indexBody));
return serializer.rawToSavedObject<T>(
{ ...raw, ...indexBody },
{ migrationVersionCompatibility }
);
};
Loading

0 comments on commit b24b41d

Please sign in to comment.