Skip to content

Commit 420d182

Browse files
qn895kibanamachineelasticmachine
authored
[Product docs] Add new update_all endpoint for product docs management (elastic#231884)
## Summary This PR fixes elastic#231450. It adds a check upon Kibana start to get previously installed product docs and their Inference IDs, then perform upgrade task for each. This means upon Kibana start: - If previous version has ELSER installed → ELSER product docs are updated - If previous version has E5 installed → E5 product docs are updated This PR also exposes out a new API that will allow users to update product_docs on command. ### To update Passing `forceUpdate: true` will uninstall and install the product docs to the latest compatible version, even if docs for the same Kibana version are already installed. This is helpful if we need to repair installation or simply upgrade/update docs to the newest possible version even for the same Kibana major-minor version. Because the operation is expensive, by default, `forceUpdate` is set to false unless user explicitly wants to do that with the API. To force update all previously installed Inference IDs: ``` POST kbn://internal/product_doc_base/update_all { # Will force update to latest compatible, even for same Kibana version "forceUpdate": true } ``` Optionally, you can specify specific Inference IDs to update: ``` POST kbn://internal/product_doc_base/update_all { # Will only update to latest compatible only if Kibana version is different "forceUpdate": false, "inferenceIds": [ ".multilingual-e5-small-elasticsearch", ] } ``` Omit `inferenceIds` if you want to update all previously installed docs, regardless of which Inference ID. - If ELSER was installed, but not E5 → ELSER docs will be updated - If E5 was installed, but not ELSER → E5 docs will be updated - If ELSER was installed, E5 was installed → ELSER & E5 docs will be updated ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [ ] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 16ee897 commit 420d182

18 files changed

Lines changed: 613 additions & 21 deletions

File tree

docs/extend/plugin-list.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ mapped_pages:
188188
| [onechat](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/onechat/README.md) | Home of the workchat framework. |
189189
| [osquery](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/osquery/README.md) | This plugin adds extended support to Security Solution Fleet Osquery integration |
190190
| [painlessLab](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/private/painless_lab/README.md) | This plugin helps users learn how to use the Painless scripting language. |
191-
| [productDocBase](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/ai_infra/product_doc_base/README.md) | This plugin contains the product documentation base service. |
191+
| [productDocBase](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/ai_infra/product_doc_base/README.md) | We expose several APIs to install, update, and uninstall product docs artifacts. Currently, for every Kibana version, we support product docs for 2 Inference IDs: .multilingual-e5-small-elasticsearch \| .elser-2-elasticsearch. |
192192
| [productIntercept](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/private/product_intercept/README.md) | This is a standalone plugin that leverages the intercept plugin to display product intercept used to gather information that is turn used to compute CSAT about user's experience of Kibana. |
193193
| [profiling](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/profiling/README.md) | Universal Profiling provides fleet-wide, whole-system, continuous profiling with zero instrumentation. Get a comprehensive understanding of what lines of code are consuming compute resources throughout your entire fleet by visualizing your data in Kibana using the flamegraph, stacktraces, and top functions views. |
194194
| [profilingDataAccess](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/profiling_data_access) | WARNING: Missing or empty README. |

packages/kbn-optimizer/limits.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ pageLoadAssetSize:
121121
painlessLab: 6299
122122
presentationPanel: 11484
123123
presentationUtil: 9000
124-
productDocBase: 3074
124+
productDocBase: 3083
125125
productIntercept: 9860
126126
profiling: 20716
127127
reindexService: 3601
Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,63 @@
11
# Product documentation base plugin
22

3-
This plugin contains the product documentation base service.
3+
4+
## Kibana API
5+
We expose several APIs to install, update, and uninstall product docs artifacts. Currently, for every Kibana version, we support product docs for 2 Inference IDs: .multilingual-e5-small-elasticsearch | .elser-2-elasticsearch.
6+
7+
### To install
8+
```
9+
POST kbn://internal/product_doc_base/install
10+
{
11+
"inferenceId": ".multilingual-e5-small-elasticsearch"
12+
}
13+
```
14+
### To update
15+
16+
Passing `forceUpdate: true` will uninstall and install the product docs to the latest compatible version, even if docs for the same Kibana version are already installed. This is helpful if we need to repair installation or simply upgrade/update docs to the newest possible version even for the same Kibana major-minor version.
17+
18+
Because the operation is expensive, by default, `forceUpdate` is set to false unless user explicitly wants to do that with the API.
19+
20+
To force update all previously installed Inference IDs:
21+
```
22+
POST kbn://internal/product_doc_base/update_all
23+
{
24+
# Will force update to latest compatible, even for same Kibana version
25+
"forceUpdate": true
26+
}
27+
```
28+
29+
Optionally, you can specify specific Inference IDs to update:
30+
```
31+
POST kbn://internal/product_doc_base/update_all
32+
{
33+
# Will only update to latest compatible only if Kibana version is different
34+
"forceUpdate": false,
35+
"inferenceIds": [
36+
".multilingual-e5-small-elasticsearch",
37+
]
38+
}
39+
```
40+
Omit `inferenceIds` if you want to update all previously installed docs, regardless of which Inference ID.
41+
- If ELSER was installed, but not E5 → ELSER docs will be updated
42+
- If E5 was installed, but not ELSER → E5 docs will be updated
43+
- If ELSER was installed, E5 was installed → ELSER & E5 docs will be updated
44+
45+
### To uninstall
46+
```
47+
POST kbn://internal/product_doc_base/uninstall
48+
{
49+
"inferenceId": ".elser-2-elasticsearch"
50+
}
51+
```
52+
53+
54+
## Run tests
55+
56+
Set up test server
57+
```
58+
node scripts/functional_tests_server.js --config x-pack/platform/test/functional_gen_ai/inference/config.ts
59+
```
60+
Run tests on different terminal
61+
```
62+
node scripts/functional_tests_server.js --config x-pack/platform/test/functional_gen_ai/inference/config.ts --grep='product docs base'
63+
```

x-pack/platform/plugins/shared/ai_infra/product_doc_base/common/http_api/installation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { ProductInstallState, InstallationStatus } from '../install_status'
1111
export const INSTALLATION_STATUS_API_PATH = '/internal/product_doc_base/status';
1212
export const INSTALL_ALL_API_PATH = '/internal/product_doc_base/install';
1313
export const UNINSTALL_ALL_API_PATH = '/internal/product_doc_base/uninstall';
14+
export const UPDATE_ALL_API_PATH = '/internal/product_doc_base/update_all';
1415

1516
export interface InstallationStatusResponse {
1617
inferenceId: string;
@@ -23,6 +24,11 @@ export interface PerformInstallResponse {
2324
failureReason?: string;
2425
}
2526

27+
export interface PerformUpdateResponse {
28+
installed: boolean;
29+
failureReason?: string;
30+
}
31+
2632
export interface UninstallResponse {
2733
success: boolean;
2834
}

x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
INSTALLATION_STATUS_API_PATH,
1212
INSTALL_ALL_API_PATH,
1313
UNINSTALL_ALL_API_PATH,
14+
UPDATE_ALL_API_PATH,
1415
} from '../../../common/http_api/installation';
1516
import { defaultInferenceEndpoints } from '@kbn/inference-common';
1617

@@ -95,6 +96,55 @@ describe('InstallationService', () => {
9596
);
9697
});
9798
});
99+
describe('#updateAll', () => {
100+
it('calls the endpoint with the forceUpdate as false by default', async () => {
101+
await service.updateAll();
102+
expect(http.post).toHaveBeenCalledTimes(1);
103+
expect(http.post).toHaveBeenCalledWith(UPDATE_ALL_API_PATH, {
104+
body: JSON.stringify({
105+
forceUpdate: false,
106+
}),
107+
});
108+
});
109+
it('calls the endpoint with the forceUpdate ', async () => {
110+
await service.updateAll({ forceUpdate: true });
111+
expect(http.post).toHaveBeenCalledTimes(1);
112+
expect(http.post).toHaveBeenCalledWith(UPDATE_ALL_API_PATH, {
113+
body: JSON.stringify({
114+
forceUpdate: true,
115+
}),
116+
});
117+
});
118+
it('calls the endpoint with the forceUpdate and Inference Ids if provided', async () => {
119+
await service.updateAll({
120+
forceUpdate: false,
121+
inferenceIds: [defaultInferenceEndpoints.ELSER],
122+
});
123+
expect(http.post).toHaveBeenCalledTimes(1);
124+
expect(http.post).toHaveBeenCalledWith(UPDATE_ALL_API_PATH, {
125+
body: JSON.stringify({
126+
forceUpdate: false,
127+
inferenceIds: [defaultInferenceEndpoints.ELSER],
128+
}),
129+
});
130+
});
131+
132+
it('returns the value from the server', async () => {
133+
const expected = {
134+
'.elser-2-elasticsearch': {
135+
installing: true,
136+
},
137+
'.multilingual-e5-small-elasticsearch': {
138+
installing: true,
139+
},
140+
};
141+
http.post.mockResolvedValue(expected);
142+
143+
const response = await service.updateAll();
144+
expect(response).toEqual(expected);
145+
});
146+
});
147+
98148
describe('#uninstall', () => {
99149
it('calls the endpoint with the right parameters', async () => {
100150
await service.uninstall({ inferenceId });

x-pack/platform/plugins/shared/ai_infra/product_doc_base/public/services/installation/installation_service.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ import { defaultInferenceEndpoints } from '@kbn/inference-common';
1010
import type {
1111
InstallationStatusResponse,
1212
PerformInstallResponse,
13+
PerformUpdateResponse,
1314
UninstallResponse,
1415
} from '../../../common/http_api/installation';
1516
import {
1617
INSTALLATION_STATUS_API_PATH,
1718
INSTALL_ALL_API_PATH,
1819
UNINSTALL_ALL_API_PATH,
20+
UPDATE_ALL_API_PATH,
1921
} from '../../../common/http_api/installation';
2022

2123
export class InstallationService {
@@ -44,7 +46,7 @@ export class InstallationService {
4446
body: JSON.stringify({ inferenceId }),
4547
});
4648

47-
if (!response.installed) {
49+
if (!response?.installed) {
4850
throw new Error(
4951
`Installation did not complete successfully.${
5052
response.failureReason ? `\n${response.failureReason}` : ''
@@ -63,4 +65,24 @@ export class InstallationService {
6365

6466
return response;
6567
}
68+
69+
/**
70+
* Update all product documentation to the latest version.
71+
*
72+
* @param forceUpdate - If true, the docs with the same version majorMinor version will be forced to updated regardless
73+
* @param inferenceIds - If provided, only the product docs for the given inference IDs will be updated. If not, all previously installed inference IDs will be updated.
74+
* @returns
75+
*/
76+
async updateAll(params?: {
77+
forceUpdate?: boolean;
78+
inferenceIds?: string[];
79+
}): Promise<PerformUpdateResponse> {
80+
const forceUpdate = params?.forceUpdate ?? false;
81+
const inferenceIds = params?.inferenceIds ?? [];
82+
const response = await this.http.post<PerformUpdateResponse>(UPDATE_ALL_API_PATH, {
83+
body: JSON.stringify({ forceUpdate, ...(inferenceIds.length > 0 ? { inferenceIds } : {}) }),
84+
});
85+
86+
return response;
87+
}
6688
}

x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/plugin.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe('ProductDocBasePlugin', () => {
5050
update: jest.fn().mockResolvedValue({}),
5151
uninstall: jest.fn().mockResolvedValue({}),
5252
getStatus: jest.fn().mockResolvedValue({}),
53+
getStatuses: jest.fn().mockResolvedValue({}),
5354
updateAll: jest.fn().mockResolvedValue({}),
5455
});
5556
});
@@ -89,6 +90,7 @@ describe('ProductDocBasePlugin', () => {
8990
expect(startContract).toEqual({
9091
management: {
9192
getStatus: expect.any(Function),
93+
getStatuses: expect.any(Function),
9294
install: expect.any(Function),
9395
uninstall: expect.any(Function),
9496
update: expect.any(Function),

x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ export class ProductDocBasePlugin
112112
licensing,
113113
taskManager,
114114
};
115-
116115
documentationManager.updateAll().catch((err) => {
117116
this.logger.error(`Error scheduling product documentation updateAll task: ${err.message}`);
118117
});
@@ -123,6 +122,7 @@ export class ProductDocBasePlugin
123122
updateAll: documentationManager.updateAll.bind(documentationManager),
124123
uninstall: documentationManager.uninstall.bind(documentationManager),
125124
getStatus: documentationManager.getStatus.bind(documentationManager),
125+
getStatuses: documentationManager.getStatuses.bind(documentationManager),
126126
},
127127
search: searchService.search.bind(searchService),
128128
};

x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/routes/installation.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ import { defaultInferenceEndpoints } from '@kbn/inference-common';
1212
import type {
1313
InstallationStatusResponse,
1414
PerformInstallResponse,
15+
PerformUpdateResponse,
1516
UninstallResponse,
1617
} from '../../common/http_api/installation';
1718
import {
1819
INSTALLATION_STATUS_API_PATH,
1920
INSTALL_ALL_API_PATH,
2021
UNINSTALL_ALL_API_PATH,
22+
UPDATE_ALL_API_PATH,
2123
} from '../../common/http_api/installation';
2224
import type { InternalServices } from '../types';
23-
import type { ProductInstallState } from '../../common/install_status';
2425

2526
export const registerInstallationRoutes = ({
2627
router,
@@ -104,10 +105,8 @@ export const registerInstallationRoutes = ({
104105
let failureReason = null;
105106
if (status === 'error' && installStatus) {
106107
failureReason = Object.values(installStatus)
107-
.filter(
108-
(product: ProductInstallState) => product.status === 'error' && product.failureReason
109-
)
110-
.map((product: ProductInstallState) => product.failureReason)
108+
.filter((product) => product.status === 'error' && product.failureReason)
109+
.map((product) => product.failureReason)
111110
.join('\n');
112111
}
113112
return res.ok<PerformInstallResponse>({
@@ -119,6 +118,46 @@ export const registerInstallationRoutes = ({
119118
}
120119
);
121120

121+
router.post(
122+
{
123+
path: UPDATE_ALL_API_PATH,
124+
validate: {
125+
body: schema.object({
126+
forceUpdate: schema.boolean({ defaultValue: false }),
127+
inferenceIds: schema.maybe(schema.arrayOf(schema.string())),
128+
}),
129+
},
130+
options: {
131+
access: 'internal',
132+
timeout: { idleSocket: 20 * 60 * 1000 }, // install can take time.
133+
},
134+
security: {
135+
authz: {
136+
requiredPrivileges: [ApiPrivileges.manage('llm_product_doc')],
137+
},
138+
},
139+
},
140+
async (ctx, req, res) => {
141+
const { forceUpdate } = req.body ?? {};
142+
143+
const { documentationManager } = getServices();
144+
145+
const updated = await documentationManager.updateAll({
146+
request: req,
147+
forceUpdate,
148+
// If inferenceIds is provided, use it, otherwise use all previously installed inference IDs
149+
inferenceIds: req.body.inferenceIds ?? [],
150+
});
151+
152+
// check status after installation in case of failure
153+
const statuses: Record<string, PerformUpdateResponse> =
154+
await documentationManager.getStatuses({ inferenceIds: updated.inferenceIds });
155+
return res.ok<Record<string, PerformUpdateResponse>>({
156+
body: statuses,
157+
});
158+
}
159+
);
160+
122161
router.post(
123162
{
124163
path: UNINSTALL_ALL_API_PATH,

x-pack/platform/plugins/shared/ai_infra/product_doc_base/server/services/doc_manager/doc_manager.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,35 @@ describe('DocumentationManager', () => {
212212
});
213213
});
214214

215+
describe('#updateAll', () => {
216+
beforeEach(() => {
217+
getTaskStatusMock.mockResolvedValue('not_scheduled');
218+
219+
docInstallClient.getInstallationStatus.mockResolvedValue({
220+
kibana: { status: 'uninstalled' },
221+
} as Awaited<ReturnType<ProductDocInstallClient['getInstallationStatus']>>);
222+
});
223+
224+
it('calls `scheduleEnsureUpToDateTask` for each inferenceId', async () => {
225+
await docManager.updateAll();
226+
227+
expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledTimes(2);
228+
expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledWith({
229+
taskManager,
230+
logger,
231+
inferenceId: defaultInferenceEndpoints.MULTILINGUAL_E5_SMALL,
232+
});
233+
234+
expect(scheduleEnsureUpToDateTaskMock).toHaveBeenCalledWith({
235+
taskManager,
236+
logger,
237+
inferenceId: defaultInferenceEndpoints.ELSER,
238+
});
239+
240+
expect(waitUntilTaskCompletedMock).not.toHaveBeenCalled();
241+
});
242+
});
243+
215244
describe('#uninstall', () => {
216245
beforeEach(() => {
217246
getTaskStatusMock.mockResolvedValue('not_scheduled');

0 commit comments

Comments
 (0)