[7.15] Kibana plugin development - Import saved objects to specific space

Hi there,

my team and I are developing a custom Kibana plugin and during the initialization phase we'd like to import some custom Saved Objects into a new space.

Basically, we managed to create the space during the initialization phase, requiring the spaces x-pack plugin in the kibana.json file and adding the following lines:

let namespacesResponse =  await this.externalPluginsService.getExternalPlugins().spaces?.spacesService?.createSpacesClient(request).getAll();
const namespaces = namespacesResponse?.map((namespace: GetSpaceResult) => namespace.id) as string[];
if (!namespaces.includes('my_custom_space')) await this.externalPluginsService.getExternalPlugins().spaces?.spacesService?.createSpacesClient(request).create(INIT_QUERIES.INIT_EPI_SPACE);

Now we'd like to import what's inside a ndjson file into that new space, basically the equivalent of switching to that space in the Kibana console, go to Management>Saved Object and import a given ndjson file or of making the following cURL:

curl -XPOST "http://localhost:5601/ayo/s/my_custom_space/api/saved_objects/_import" -H "kbn-xsrf: true" --form file=@./server/services/my_custom_savedobjects.ndjson -u elastic

Is there a straightforward way to achieve such a result?

What we had to do was to install a new module (fs-ndjson), use its readFile method to read from a given path, and use the getImporter method as follows:

const importer = await context.core.savedObjects.getImporter(context.core.savedObjects.getClient());
importer.import({
    overwrite: true,
    createNewCopies: false,
    readStream: this.createReadableStreamFromArray(epi_array)
})

Unfortunately, it seems to only work with the Default space. In fact, if we try to add the namespace option to the import method (defined as optional in the SavedObjectsImportOptions interface), it always returns the Spaces currently determines the namespaces error, thrown by the throwErrorIfNamespaceSpecified of the checkConflicts function, defined (in the x-pack/plugins/spaces/server/saved_objects/spaces_saved_objects_client.ts file) as follows

/**
* Check what conflicts will result when creating a given array of saved objects. This includes "unresolvable conflicts", which are
* multi-namespace objects that exist in a different namespace; such conflicts cannot be resolved/overwritten.
*
* @param objects
* @param options
*/
public async checkConflicts(
objects: SavedObjectsCheckConflictsObject[] = [],
options: SavedObjectsBaseOptions = {}
) {
throwErrorIfNamespaceSpecified(options);

return await this.client.checkConflicts(objects, {
    ...options,
    namespace: spaceIdToNamespace(this.spaceId),
});
}

Why is there a namespace option - which perfectly makes sense in order to pick the space you want to import your saved objects to - but you cannot use it without throwing that error? What are we doing wrong?

Thank you in advance!

UPDATES

It seems I managed to make it work replacing this

const importer = await context.core.savedObjects.getImporter(context.core.savedObjects.getClient());

with this

const importer = await context.core.savedObjects.getImporter(context.core.savedObjects.getClient({excludedWrappers: ['spaces']}));

that is adding the excludeWrappers option in the getClient method. That way the wrapper that raises an error if the namespace is specified is ignored and I can then proceeed with the import with a specific space as follows:

importer.import({
    overwrite: true,
    createNewCopies: false,
    namespace: 'my_custom_space',
    readStream: this.createReadableStreamFromArray(epi_array)
})

It seems to work flawlessy. Is it the correct way to achieve this result?

Also, when in dev mode I can read the ndjson file using the relative path, as follows:

await fsNdjson.readFile('plugins/my_plugin_folder/server/services/my_custom_savedobjects.ndjson')

Though, that path is not valid once I build the plugin. Any ideas about a possible path?

Thank you!

EDIT:
Guess I sorted out the path problem, using the ${__dirname}/${filepath} sintax when reading the file. It works in both dev env and testing env (built plugin).

1 Like

I guess Spaces plugin tries to prevent you from accessing internal API. Have you tried to use this experimental API created for this purpose Copy saved objects to space API | Kibana Guide [7.16] | Elastic?

Yes, that's the correct way to do it, and that's what we do for the Copy-to-Spaces feature:

Yeah, the Spaces plugin doesn't want you to try to set the namespace when the Spaces SOC wrapper already does that based on the request path. But that's why we have the ability to exclude SOC wrappers, for edge cases like this!

1 Like

First of all thank you both for your replies.

That's the API I took inspiration from, but I do not want to merely call the API (e.g. axios), I'd rather go with a cleaner approach using something offered by Kibana developers.

Thank you so much for your confirmation.

As for now, I changed my code to avoid the re-definition of your createReadableStreamFromArray method and the manual creation of a json array from a ndjson file.
This way I avoided the installation of further dependencies (i.e. fs-ndjson and ndjson) and I simply went with something like the following

private async initCUSTOMSpace(context: RequestHandlerContext, request: KibanaRequest) {
    let namespacesResponse =  await this.externalPluginsService.getExternalPlugins().spaces?.spacesService?.createSpacesClient(request).getAll();
    const namespaces = namespacesResponse?.map((namespace: GetSpaceResult) => namespace.id) as string[];
    if (!namespaces.includes(INIT_PARAMS.INIT_CUSTOM_NAMESPACE_ID)) await this.externalPluginsService.getExternalPlugins().spaces?.spacesService?.createSpacesClient(request).create(INIT_PARAMS.INIT_CUSTOM_SPACE);
    
    const custom_savedobjects_stream = fs.createReadStream(`${__dirname}/${INIT_PARAMS.INIT_CUSTOM_SO_FILEPATH}`);
    const importer = context.core.savedObjects.getImporter(context.core.savedObjects.getClient({excludedWrappers: ['spaces']}));
    importer.import({
        overwrite: true,
        createNewCopies: false,
        namespace: INIT_PARAMS.INIT_CUSTOM_NAMESPACE_ID,
        readStream: await createSavedObjectsStreamFromNdJson(custom_savedobjects_stream)
    })
}

Guess it's the cleanest approach I could use to achieve what I wanted and it seems to work perfectly.

Thank you

1 Like

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.