[streams] content packs endpoints (#213910)

Creates basic routes to download and upload content packs associated to
a stream.
Only dashboard assets will be exported and linked to a stream.

The endpoints are currently a proxy to the savedObjects
importer/exporter interfaces:
- download exports the dashboard linked to a stream
- upload imports a content pack file and link the dashboards to the
targeted stream. Dashboards are imported as-is with no index pattern
replacement performed, this will be implemented separately

### Testing
- download `curl -XPOST -H "x-elastic-internal-origin: 'kibana'" -H
"kbn-xsrf: true"
http://elastic:changeme@localhost:5601/pat/api/streams/logs/content/export
--output content.json`
- upload `curl -XPOST -H "kbn-xsrf: true"
http://elastic:changeme@localhost:5601/pat/api/streams/logs.foo/content/import
-F 'content=@content.json'`

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Kevin Lacabane 2025-03-18 13:22:18 +01:00 committed by GitHub
parent e1f094d1f5
commit e84f6de3f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 514 additions and 3 deletions

View file

@ -48880,6 +48880,108 @@
"x-state": "Technical Preview"
}
},
"/api/streams/{name}/content/export": {
"post": {
"description": "Exports the content associated to a stream.",
"operationId": "post-streams-name-content-export",
"parameters": [
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
},
{
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"additionalProperties": false,
"properties": {},
"type": "object"
},
{
"enum": [
"null"
],
"nullable": true
},
{
"not": {}
}
]
}
}
}
},
"responses": {},
"summary": "Export stream content",
"tags": [
"streams"
]
}
},
"/api/streams/{name}/content/import": {
"post": {
"description": "Links content objects to a stream.",
"operationId": "post-streams-name-content-import",
"parameters": [
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
},
{
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"additionalProperties": false,
"properties": {
"content": {}
},
"required": [
"content"
],
"type": "object"
}
}
}
},
"responses": {},
"summary": "Import content into a stream",
"tags": [
"streams"
]
}
},
"/api/streams/{name}/dashboards": {
"get": {
"description": "Fetches all dashboards linked to a stream that are visible to the current user in the current space.",

View file

@ -48471,6 +48471,108 @@
"x-state": "Technical Preview"
}
},
"/api/streams/{name}/content/export": {
"post": {
"description": "Exports the content associated to a stream.",
"operationId": "post-streams-name-content-export",
"parameters": [
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
},
{
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"additionalProperties": false,
"properties": {},
"type": "object"
},
{
"enum": [
"null"
],
"nullable": true
},
{
"not": {}
}
]
}
}
}
},
"responses": {},
"summary": "Export stream content",
"tags": [
"streams"
]
}
},
"/api/streams/{name}/content/import": {
"post": {
"description": "Links content objects to a stream.",
"operationId": "post-streams-name-content-import",
"parameters": [
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
},
{
"in": "path",
"name": "name",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"additionalProperties": false,
"properties": {
"content": {}
},
"required": [
"content"
],
"type": "object"
}
}
}
},
"responses": {},
"summary": "Import content into a stream",
"tags": [
"streams"
]
}
},
"/api/streams/{name}/dashboards": {
"get": {
"description": "Fetches all dashboards linked to a stream that are visible to the current user in the current space.",

View file

@ -43447,6 +43447,72 @@ paths:
- streams
x-state: Technical Preview
x-beta: true
/api/streams/{name}/content/export:
post:
description: Exports the content associated to a stream.
operationId: post-streams-name-content-export
parameters:
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: name
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
anyOf:
- additionalProperties: false
type: object
properties: {}
- enum:
- 'null'
nullable: true
- not: {}
responses: {}
summary: Export stream content
tags:
- streams
x-beta: true
/api/streams/{name}/content/import:
post:
description: Links content objects to a stream.
operationId: post-streams-name-content-import
parameters:
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: name
required: true
schema:
type: string
requestBody:
content:
multipart/form-data:
schema:
additionalProperties: false
type: object
properties:
content: {}
required:
- content
responses: {}
summary: Import content into a stream
tags:
- streams
x-beta: true
/api/streams/{name}/dashboards:
get:
description: Fetches all dashboards linked to a stream that are visible to the current user in the current space.

View file

@ -46515,6 +46515,70 @@ paths:
tags:
- streams
x-state: Technical Preview
/api/streams/{name}/content/export:
post:
description: Exports the content associated to a stream.
operationId: post-streams-name-content-export
parameters:
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: name
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
anyOf:
- additionalProperties: false
type: object
properties: {}
- enum:
- 'null'
nullable: true
- not: {}
responses: {}
summary: Export stream content
tags:
- streams
/api/streams/{name}/content/import:
post:
description: Links content objects to a stream.
operationId: post-streams-name-content-import
parameters:
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
- in: path
name: name
required: true
schema:
type: string
requestBody:
content:
multipart/form-data:
schema:
additionalProperties: false
type: object
properties:
content: {}
required:
- content
responses: {}
summary: Import content into a stream
tags:
- streams
/api/streams/{name}/dashboards:
get:
description: Fetches all dashboards linked to a stream that are visible to the current user in the current space.

View file

@ -0,0 +1,18 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { z } from '@kbn/zod';
interface ContentPack {
content: string;
}
const contentPackSchema: z.Schema<ContentPack> = z.object({
content: z.string(),
});
export { contentPackSchema, type ContentPack };

View file

@ -6,9 +6,9 @@
*/
export * from './ingest';
export * from './api';
export * from './core';
export * from './helpers';
export * from './group';
export * from './record_types';
export * from './content';

View file

@ -0,0 +1,157 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { Readable } from 'stream';
import { z } from '@kbn/zod';
import {
createConcatStream,
createListStream,
createMapStream,
createPromiseFromStreams,
} from '@kbn/utils';
import { createSavedObjectsStreamFromNdJson } from '@kbn/core-saved-objects-server-internal/src/routes/utils';
import { ContentPack, contentPackSchema } from '@kbn/streams-schema';
import { createServerRoute } from '../create_server_route';
import { StatusError } from '../../lib/streams/errors/status_error';
const exportContentRoute = createServerRoute({
endpoint: 'POST /api/streams/{name}/content/export 2023-10-31',
options: {
access: 'public',
summary: 'Export stream content',
description: 'Exports the content associated to a stream.',
},
params: z.object({
path: z.object({
name: z.string(),
}),
}),
security: {
authz: {
enabled: false,
reason:
'This API delegates security to the currently logged in user and their Elasticsearch permissions.',
},
},
async handler({ params, request, response, getScopedClients, context }) {
const { assetClient, soClient, streamsClient } = await getScopedClients({ request });
await streamsClient.ensureStream(params.path.name);
const dashboards = await assetClient
.getAssets({ entityId: params.path.name, entityType: 'stream' })
.then((assets) => assets.filter(({ assetType }) => assetType === 'dashboard'));
if (dashboards.length === 0) {
throw new StatusError(`No dashboards are linked to [${params.path.name}] stream`, 400);
}
const exporter = (await context.core).savedObjects.getExporter(soClient);
const exportStream = await exporter.exportByObjects({
request,
objects: dashboards.map((dashboard) => ({ id: dashboard.assetId, type: 'dashboard' })),
includeReferencesDeep: true,
});
const savedObjects: string[] = await createPromiseFromStreams([
exportStream,
createMapStream((savedObject) => {
return JSON.stringify(savedObject);
}),
createConcatStream([]),
]);
return response.ok({
body: { content: savedObjects.join('\n') },
headers: {
'Content-Disposition': `attachment; filename="content.json"`,
'Content-Type': 'application/json',
},
});
},
});
const importContentRoute = createServerRoute({
endpoint: 'POST /api/streams/{name}/content/import 2023-10-31',
options: {
access: 'public',
summary: 'Import content into a stream',
description: 'Links content objects to a stream.',
body: {
accepts: 'multipart/form-data',
output: 'stream',
},
},
params: z.object({
path: z.object({
name: z.string(),
}),
body: z.object({
content: z.instanceof(Readable),
}),
}),
security: {
authz: {
enabled: false,
reason:
'This API delegates security to the currently logged in user and their Elasticsearch permissions.',
},
},
async handler({ params, request, getScopedClients, context }) {
const { assetClient, soClient, streamsClient } = await getScopedClients({ request });
await streamsClient.ensureStream(params.path.name);
const body: ContentPack = await new Promise((resolve, reject) => {
let data = '';
params.body.content.on('data', (chunk) => (data += chunk));
params.body.content.on('end', () => {
try {
const parsed = JSON.parse(data);
resolve(contentPackSchema.parse(parsed));
} catch (err) {
reject(new StatusError('Invalid content pack format', 400));
}
});
params.body.content.on('error', (error) => reject(error));
});
const updatedSavedObjectsStream = await createPromiseFromStreams([
await createSavedObjectsStreamFromNdJson(Readable.from(body.content)),
createConcatStream([]),
]);
const importer = (await context.core).savedObjects.getImporter(soClient);
const { successResults, errors } = await importer.import({
readStream: createListStream(updatedSavedObjectsStream),
createNewCopies: true,
overwrite: true,
});
const createdAssets = (successResults ?? [])
.filter((savedObject) => savedObject.type === 'dashboard')
.map((dashboard) => ({
assetType: 'dashboard' as const,
assetId: dashboard.destinationId ?? dashboard.id,
}));
if (createdAssets.length > 0) {
await assetClient.bulk(
{ entityId: params.path.name, entityType: 'stream' },
createdAssets.map((asset) => ({
index: { asset },
}))
);
}
return { errors, created: createdAssets };
},
});
export const contentRoutes = {
...exportContentRoute,
...importContentRoute,
};

View file

@ -121,8 +121,6 @@ const linkDashboardRoute = createServerRoute({
}),
handler: async ({ params, request, getScopedClients }): Promise<LinkDashboardResponse> => {
const { assetClient, streamsClient } = await getScopedClients({ request });
await streamsClient.ensureStream(params.path.name);
const {
path: { dashboardId, name: streamName },
} = params;

View file

@ -15,6 +15,7 @@ import { internalProcessingRoutes } from './internal/streams/processing/route';
import { ingestRoutes } from './streams/ingest/route';
import { internalLifecycleRoutes } from './internal/streams/lifecycle/route';
import { groupRoutes } from './streams/group/route';
import { contentRoutes } from './content/route';
import { internalDashboardRoutes } from './internal/dashboards/route';
import { internalCrudRoutes } from './internal/streams/crud/route';
import { internalManagementRoutes } from './internal/streams/management/route';
@ -35,6 +36,7 @@ export const streamsRouteRepository = {
...managementRoutes,
...ingestRoutes,
...groupRoutes,
...contentRoutes,
};
export type StreamsRouteRepository = typeof streamsRouteRepository;

View file

@ -40,6 +40,8 @@
"@kbn/traced-es-client",
"@kbn/es-query",
"@kbn/core-elasticsearch-client-server-internal",
"@kbn/utils",
"@kbn/core-saved-objects-server-internal",
"@kbn/core-analytics-server"
]
}