From 46dbf514e469ef48690cd258f76c17f4f4247645 Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:46:02 +0200 Subject: [PATCH] [Fleet] using @kbn/config-schema part 2 (outputs and other apis) (#193326) ## Summary Relates https://github.com/elastic/kibana/issues/184685 Added tests to verify response schemas. To verify, go to Fleet settings and try create/update different output types, fleet server hosts, outputs. ``` GET kbn:/api/oas?pathStartsWith=/api/fleet/outputs GET kbn:/api/oas?pathStartsWith=/api/fleet/health_check GET kbn:/api/oas?pathStartsWith=/api/fleet/message_signing_service/rotate_key_pair GET kbn:/api/oas?pathStartsWith=/api/fleet/fleet_server_hosts GET kbn:/api/oas?pathStartsWith=/api/fleet/data_streams # test APIs POST kbn:/api/fleet/health_check { "id": "default-fleet-server" } POST kbn:/api/fleet/message_signing_service/rotate_key_pair GET kbn:/api/fleet/data_streams GET kbn:/api/fleet/check-permissions POST kbn:/api/fleet/service_tokens ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../fleet/common/types/models/output.ts | 8 +- .../edit_output_flyout/use_output_form.tsx | 4 +- .../fleet/server/routes/app/index.test.ts | 81 ++++++ .../plugins/fleet/server/routes/app/index.ts | 36 ++- .../server/routes/data_streams/index.test.ts | 69 +++++ .../fleet/server/routes/data_streams/index.ts | 47 +++- .../server/routes/fleet_proxies/handler.ts | 2 +- .../server/routes/fleet_proxies/index.test.ts | 173 ++++++++++++ .../server/routes/fleet_proxies/index.ts | 91 ++++++- .../routes/fleet_server_hosts/handler.ts | 2 +- .../routes/fleet_server_hosts/index.test.ts | 166 ++++++++++++ .../server/routes/fleet_server_hosts/index.ts | 90 ++++++- .../routes/health_check/handler.test.ts | 31 ++- .../server/routes/health_check/handler.ts | 89 +++++++ .../fleet/server/routes/health_check/index.ts | 102 ++----- .../routes/message_signing_service/index.ts | 22 +- .../fleet/server/routes/output/index.test.ts | 252 ++++++++++++++++++ .../fleet/server/routes/output/index.ts | 122 ++++++++- .../fleet/server/types/models/agent_policy.ts | 8 +- .../fleet/server/types/models/output.ts | 153 ++++++----- .../server/types/models/preconfiguration.ts | 2 +- .../types/rest_spec/check_permissions.ts | 11 + .../server/types/rest_spec/fleet_proxies.ts | 38 ++- .../rest_spec/fleet_server_policy_config.ts | 21 +- .../server/types/rest_spec/health_check.ts | 13 + .../fleet/server/types/rest_spec/output.ts | 33 +++ .../apis/outputs/crud.ts | 58 ++++ 27 files changed, 1508 insertions(+), 216 deletions(-) create mode 100644 x-pack/plugins/fleet/server/routes/app/index.test.ts create mode 100644 x-pack/plugins/fleet/server/routes/data_streams/index.test.ts create mode 100644 x-pack/plugins/fleet/server/routes/fleet_proxies/index.test.ts create mode 100644 x-pack/plugins/fleet/server/routes/fleet_server_hosts/index.test.ts create mode 100644 x-pack/plugins/fleet/server/routes/health_check/handler.ts create mode 100644 x-pack/plugins/fleet/server/routes/output/index.test.ts diff --git a/x-pack/plugins/fleet/common/types/models/output.ts b/x-pack/plugins/fleet/common/types/models/output.ts index 938c4c4cdced07..36471c5e95bfda 100644 --- a/x-pack/plugins/fleet/common/types/models/output.ts +++ b/x-pack/plugins/fleet/common/types/models/output.ts @@ -62,7 +62,7 @@ export interface NewElasticsearchOutput extends NewBaseOutput { export interface NewRemoteElasticsearchOutput extends NewBaseOutput { type: OutputType['RemoteElasticsearch']; - service_token?: string; + service_token?: string | null; secrets?: { service_token?: OutputSecret; }; @@ -110,11 +110,11 @@ export interface KafkaOutput extends NewBaseOutput { compression_level?: number; auth_type?: ValueOf; connection_type?: ValueOf; - username?: string; - password?: string; + username?: string | null; + password?: string | null; sasl?: { mechanism?: ValueOf; - }; + } | null; partition?: ValueOf; random?: { group_events?: number; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx index 78aac64e128303..8526fcdae001aa 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/use_output_form.tsx @@ -394,12 +394,12 @@ export function useOutputForm(onSucess: () => void, output?: Output, defaultOupu ); const kafkaAuthUsernameInput = useInput( - kafkaOutput?.username, + kafkaOutput?.username ?? undefined, kafkaAuthMethodInput.value === kafkaAuthType.Userpass ? validateKafkaUsername : undefined, isDisabled('username') ); const kafkaAuthPasswordInput = useInput( - kafkaOutput?.password, + kafkaOutput?.password ?? undefined, kafkaAuthMethodInput.value === kafkaAuthType.Userpass ? validateKafkaPassword : undefined, isDisabled('password') ); diff --git a/x-pack/plugins/fleet/server/routes/app/index.test.ts b/x-pack/plugins/fleet/server/routes/app/index.test.ts new file mode 100644 index 00000000000000..981bdb5003193e --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/app/index.test.ts @@ -0,0 +1,81 @@ +/* + * 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 { httpServerMock } from '@kbn/core-http-server-mocks'; + +import type { FleetRequestHandlerContext } from '../../types'; +import { CheckPermissionsResponseSchema } from '../../types'; + +import { xpackMocks } from '../../mocks'; + +import { + GenerateServiceTokenResponseSchema, + generateServiceTokenHandler, + getCheckPermissionsHandler, +} from '.'; + +jest.mock('../../services', () => ({ + appContextService: { + getSecurityLicense: jest.fn().mockReturnValue({ isEnabled: jest.fn().mockReturnValue(false) }), + getCloud: jest.fn().mockReturnValue({ isServerlessEnabled: false } as any), + getExperimentalFeatures: jest.fn().mockReturnValue({ subfeaturePrivileges: false }), + getLogger: jest.fn().mockReturnValue({ debug: jest.fn(), error: jest.fn() } as any), + }, +})); + +describe('schema validation', () => { + let context: FleetRequestHandlerContext; + let response: ReturnType; + + beforeEach(() => { + context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; + response = httpServerMock.createResponseFactory(); + }); + + it('check permissions should return valid response', async () => { + const expectedResponse = { + success: false, + error: 'MISSING_SECURITY', + }; + await getCheckPermissionsHandler(context, {} as any, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = CheckPermissionsResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('generate service token should return valid response', async () => { + ( + (await context.core).elasticsearch.client.asCurrentUser.transport.request as jest.Mock + ).mockResolvedValue({ + created: true, + token: { + name: 'token', + value: 'value', + }, + }); + const expectedResponse = { + name: 'token', + value: 'value', + }; + await generateServiceTokenHandler( + context, + { + body: {}, + } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = GenerateServiceTokenResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/app/index.ts b/x-pack/plugins/fleet/server/routes/app/index.ts index 994348f6d99676..ccf0c334e417c5 100644 --- a/x-pack/plugins/fleet/server/routes/app/index.ts +++ b/x-pack/plugins/fleet/server/routes/app/index.ts @@ -7,6 +7,7 @@ import type { RequestHandler, RouteValidationResultFactory } from '@kbn/core/server'; import type { TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { parseExperimentalConfigValue } from '../../../common/experimental_features'; import type { FleetAuthzRouter } from '../../services/security'; @@ -16,9 +17,10 @@ import { appContextService } from '../../services'; import type { CheckPermissionsResponse, GenerateServiceTokenResponse } from '../../../common/types'; import { defaultFleetErrorHandler, GenerateServiceTokenError } from '../../errors'; import type { FleetRequestHandler, GenerateServiceTokenRequestSchema } from '../../types'; -import { CheckPermissionsRequestSchema } from '../../types'; +import { CheckPermissionsRequestSchema, CheckPermissionsResponseSchema } from '../../types'; import { enableSpaceAwarenessMigration } from '../../services/spaces/enable_space_awareness'; import { type FleetConfigType } from '../../config'; +import { genericErrorResponse } from '../schema/errors'; export const getCheckPermissionsHandler: FleetRequestHandler< unknown, @@ -189,6 +191,11 @@ const serviceTokenBodyValidation = (data: any, validationResult: RouteValidation return ok({ remote }); }; +export const GenerateServiceTokenResponseSchema = schema.object({ + name: schema.string(), + value: schema.string(), +}); + export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => { const experimentalFeatures = parseExperimentalConfigValue(config.enableExperimental); @@ -212,11 +219,25 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType router.versioned .get({ path: APP_API_ROUTES.CHECK_PERMISSIONS_PATTERN, + description: `Check permissions`, + options: { + tags: ['oas_tag:Fleet internals'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: CheckPermissionsRequestSchema }, + validate: { + request: CheckPermissionsRequestSchema, + response: { + 200: { + body: () => CheckPermissionsResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getCheckPermissionsHandler ); @@ -244,12 +265,23 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType fleet: { allAgents: true }, }, description: `Create a service token`, + options: { + tags: ['oas_tag:Fleet service tokens'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, validate: { request: { body: serviceTokenBodyValidation }, + response: { + 200: { + body: () => GenerateServiceTokenResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, }, }, generateServiceTokenHandler diff --git a/x-pack/plugins/fleet/server/routes/data_streams/index.test.ts b/x-pack/plugins/fleet/server/routes/data_streams/index.test.ts new file mode 100644 index 00000000000000..134e362acd6308 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/data_streams/index.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { httpServerMock } from '@kbn/core-http-server-mocks'; + +import type { FleetRequestHandlerContext } from '../..'; + +import { xpackMocks } from '../../mocks'; + +import { ListDataStreamsResponseSchema } from '.'; +import { getListHandler } from './handlers'; + +jest.mock('./handlers', () => ({ + getListHandler: jest.fn(), +})); + +const getListHandlerMock = getListHandler as jest.Mock; + +describe('schema validation', () => { + let context: FleetRequestHandlerContext; + let response: ReturnType; + + beforeEach(() => { + context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; + response = httpServerMock.createResponseFactory(); + }); + + it('list data streams should return valid response', async () => { + const expectedResponse = { + data_streams: [ + { + index: 'index', + dataset: 'dataset', + namespace: 'namespace', + type: 'type', + package: 'package', + package_version: '1.0.0', + last_activity_ms: 123, + size_in_bytes: 123, + size_in_bytes_formatted: 123, + dashboards: [ + { + id: 'id', + title: 'title', + }, + ], + serviceDetails: { + environment: 'environment', + serviceName: 'serviceName', + }, + }, + ], + }; + getListHandlerMock.mockImplementation((ctx, request, res) => { + return res.ok({ body: expectedResponse }); + }); + await getListHandler(context, {} as any, response); + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + + const validateResp = ListDataStreamsResponseSchema.validate(expectedResponse); + expect(validateResp).toEqual(expectedResponse); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/data_streams/index.ts b/x-pack/plugins/fleet/server/routes/data_streams/index.ts index cb2af8be110f8b..2b506785f7baf1 100644 --- a/x-pack/plugins/fleet/server/routes/data_streams/index.ts +++ b/x-pack/plugins/fleet/server/routes/data_streams/index.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { schema } from '@kbn/config-schema'; import type { FleetAuthzRouter } from '../../services/security'; @@ -11,8 +12,38 @@ import { API_VERSIONS } from '../../../common/constants'; import { DATA_STREAM_API_ROUTES } from '../../constants'; +import { genericErrorResponse } from '../schema/errors'; + import { getListHandler } from './handlers'; +export const ListDataStreamsResponseSchema = schema.object({ + data_streams: schema.arrayOf( + schema.object({ + index: schema.string(), + dataset: schema.string(), + namespace: schema.string(), + type: schema.string(), + package: schema.string(), + package_version: schema.string(), + last_activity_ms: schema.number(), + size_in_bytes: schema.number(), + size_in_bytes_formatted: schema.oneOf([schema.number(), schema.string()]), + dashboards: schema.arrayOf( + schema.object({ + id: schema.string(), + title: schema.string(), + }) + ), + serviceDetails: schema.nullable( + schema.object({ + environment: schema.string(), + serviceName: schema.string(), + }) + ), + }) + ), +}); + export const registerRoutes = (router: FleetAuthzRouter) => { // List of data streams router.versioned @@ -21,11 +52,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { all: true }, }, + description: `List data streams`, + options: { + tags: ['oas_tag:Data streams'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: false, + validate: { + request: {}, + response: { + 200: { + body: () => ListDataStreamsResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getListHandler ); diff --git a/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts b/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts index a5905f406a6e72..52e469971177d6 100644 --- a/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts +++ b/x-pack/plugins/fleet/server/routes/fleet_proxies/handler.ts @@ -101,7 +101,7 @@ export const putFleetProxyHandler: RequestHandler< > = async (context, request, response) => { try { const proxyId = request.params.itemId; - const coreContext = await await context.core; + const coreContext = await context.core; const soClient = coreContext.savedObjects.client; const esClient = coreContext.elasticsearch.client.asInternalUser; diff --git a/x-pack/plugins/fleet/server/routes/fleet_proxies/index.test.ts b/x-pack/plugins/fleet/server/routes/fleet_proxies/index.test.ts new file mode 100644 index 00000000000000..2a11809f355773 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/fleet_proxies/index.test.ts @@ -0,0 +1,173 @@ +/* + * 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 { httpServerMock } from '@kbn/core-http-server-mocks'; + +import type { FleetRequestHandlerContext } from '../..'; + +import { xpackMocks } from '../../mocks'; +import { ListResponseSchema } from '../schema/utils'; +import { FleetProxySchema, FleetProxyResponseSchema } from '../../types'; +import { + createFleetProxy, + getFleetProxy, + listFleetProxies, + updateFleetProxy, +} from '../../services/fleet_proxies'; + +import { + getAllFleetProxyHandler, + getFleetProxyHandler, + postFleetProxyHandler, + putFleetProxyHandler, +} from './handler'; + +jest.mock('../../services', () => ({ + appContextService: { + getLogger: jest.fn().mockReturnValue({ error: jest.fn() } as any), + }, +})); + +jest.mock('../../services/fleet_proxies', () => ({ + listFleetProxies: jest.fn(), + createFleetProxy: jest.fn(), + updateFleetProxy: jest.fn(), + getFleetProxyRelatedSavedObjects: jest.fn().mockResolvedValue({ + fleetServerHosts: [], + outputs: [], + downloadSources: [], + }), + getFleetProxy: jest.fn(), +})); + +jest.mock('./handler', () => ({ + ...jest.requireActual('./handler'), + bumpRelatedPolicies: jest.fn().mockResolvedValue({}), +})); + +describe('schema validation', () => { + let context: FleetRequestHandlerContext; + let response: ReturnType; + + beforeEach(() => { + context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; + response = httpServerMock.createResponseFactory(); + }); + + it('list fleet proxies should return valid response', async () => { + const expectedResponse = { + items: [ + { + id: 'proxy1', + name: 'proxy1', + url: 'http://proxy1:8080', + is_preconfigured: true, + proxy_headers: { + foo: 'bar', + }, + certificate_authorities: 'ca', + certificate: 'cert', + certificate_key: 'key', + }, + ], + total: 1, + page: 1, + perPage: 20, + }; + (listFleetProxies as jest.Mock).mockResolvedValue(expectedResponse); + await getAllFleetProxyHandler(context, {} as any, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = ListResponseSchema(FleetProxySchema).validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('create fleet proxy should return valid response', async () => { + const expectedResponse = { + item: { + id: 'proxy1', + name: 'proxy1', + url: 'http://proxy1:8080', + is_preconfigured: true, + proxy_headers: { + foo: 'bar', + }, + certificate_authorities: 'ca', + certificate: 'cert', + certificate_key: 'key', + }, + }; + (createFleetProxy as jest.Mock).mockResolvedValue(expectedResponse.item); + await postFleetProxyHandler( + context, + { + body: { + id: 'proxy1', + }, + } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = FleetProxyResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('update fleet proxy should return valid response', async () => { + const expectedResponse = { + item: { + id: 'proxy1', + name: 'proxy1', + url: 'http://proxy1:8080', + is_preconfigured: false, + }, + }; + (updateFleetProxy as jest.Mock).mockResolvedValue(expectedResponse.item); + await putFleetProxyHandler( + context, + { body: {}, params: { itemId: 'proxy1' } } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = FleetProxyResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('get fleet proxy should return valid response', async () => { + const expectedResponse = { + item: { + id: 'proxy1', + name: 'proxy1', + url: 'http://proxy1:8080', + is_preconfigured: false, + proxy_headers: null, + certificate_authorities: null, + certificate: null, + certificate_key: null, + }, + }; + (getFleetProxy as jest.Mock).mockResolvedValue(expectedResponse.item); + await getFleetProxyHandler( + context, + { body: {}, params: { itemId: 'proxy1' } } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = FleetProxyResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts b/x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts index 5100ae77c23771..1adc8355a0f48a 100644 --- a/x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts +++ b/x-pack/plugins/fleet/server/routes/fleet_proxies/index.ts @@ -4,16 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { schema } from '@kbn/config-schema'; + import type { FleetAuthzRouter } from '../../services/security'; import { API_VERSIONS } from '../../../common/constants'; import { FLEET_PROXY_API_ROUTES } from '../../../common/constants'; import { + FleetProxyResponseSchema, + FleetProxySchema, GetOneFleetProxyRequestSchema, PostFleetProxyRequestSchema, PutFleetProxyRequestSchema, } from '../../types'; +import { genericErrorResponse } from '../schema/errors'; + +import { ListResponseSchema } from '../schema/utils'; + import { getAllFleetProxyHandler, postFleetProxyHandler, @@ -29,11 +37,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { readSettings: true }, }, + description: `List proxies`, + options: { + tags: ['oas_tag:Fleet proxies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: false, + validate: { + request: {}, + response: { + 200: { + body: () => ListResponseSchema(FleetProxySchema), + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getAllFleetProxyHandler ); @@ -44,11 +66,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: `Create proxy`, + options: { + tags: ['oas_tag:Fleet proxies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PostFleetProxyRequestSchema }, + validate: { + request: PostFleetProxyRequestSchema, + response: { + 200: { + body: () => FleetProxyResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, postFleetProxyHandler ); @@ -59,11 +95,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: `Update proxy by ID`, + options: { + tags: ['oas_tag:Fleet proxies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PutFleetProxyRequestSchema }, + validate: { + request: PutFleetProxyRequestSchema, + response: { + 200: { + body: () => FleetProxyResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, putFleetProxyHandler ); @@ -74,11 +124,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { readSettings: true }, }, + description: `Get proxy by ID`, + options: { + tags: ['oas_tag:Fleet proxies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetOneFleetProxyRequestSchema }, + validate: { + request: GetOneFleetProxyRequestSchema, + response: { + 200: { + body: () => FleetProxyResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getFleetProxyHandler ); @@ -89,11 +153,28 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: `Delete proxy by ID`, + options: { + tags: ['oas_tag:Fleet proxies'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetOneFleetProxyRequestSchema }, + validate: { + request: GetOneFleetProxyRequestSchema, + response: { + 200: { + body: () => + schema.object({ + id: schema.string(), + }), + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, deleteFleetProxyHandler ); diff --git a/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.ts b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.ts index f7159e6b5f4988..8ad69d585ffc73 100644 --- a/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.ts +++ b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/handler.ts @@ -135,7 +135,7 @@ export const putFleetServerHostHandler: RequestHandler< TypeOf > = async (context, request, response) => { try { - const coreContext = await await context.core; + const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asInternalUser; const soClient = coreContext.savedObjects.client; diff --git a/x-pack/plugins/fleet/server/routes/fleet_server_hosts/index.test.ts b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/index.test.ts new file mode 100644 index 00000000000000..198fb6e8a6bcc5 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/index.test.ts @@ -0,0 +1,166 @@ +/* + * 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 { httpServerMock } from '@kbn/core-http-server-mocks'; + +import type { FleetRequestHandlerContext } from '../..'; +import { xpackMocks } from '../../mocks'; +import { ListResponseSchema } from '../schema/utils'; +import { FleetServerHostSchema, FleetServerHostResponseSchema } from '../../types'; +import { + createFleetServerHost, + getFleetServerHost, + listFleetServerHosts, + updateFleetServerHost, +} from '../../services/fleet_server_host'; + +import { + getAllFleetServerHostsHandler, + getFleetServerHostHandler, + postFleetServerHost, + putFleetServerHostHandler, +} from './handler'; + +jest.mock('../../services', () => ({ + appContextService: { + getLogger: jest.fn().mockReturnValue({ error: jest.fn() } as any), + getCloud: jest.fn().mockReturnValue({ isServerlessEnabled: false } as any), + }, + agentPolicyService: { + bumpAllAgentPolicies: jest.fn().mockResolvedValue({}), + }, +})); + +jest.mock('../../services/fleet_server_host', () => ({ + listFleetServerHosts: jest.fn(), + createFleetServerHost: jest.fn(), + updateFleetServerHost: jest.fn(), + getFleetServerHost: jest.fn(), +})); + +describe('schema validation', () => { + let context: FleetRequestHandlerContext; + let response: ReturnType; + + beforeEach(() => { + context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; + response = httpServerMock.createResponseFactory(); + }); + + it('list fleet server hosts should return valid response', async () => { + const expectedResponse = { + items: [ + { + id: 'host1', + name: 'host1', + host_urls: ['http://host1:8080'], + is_preconfigured: true, + is_default: true, + is_internal: true, + proxy_id: 'proxy1', + }, + ], + total: 1, + page: 1, + perPage: 20, + }; + (listFleetServerHosts as jest.Mock).mockResolvedValue(expectedResponse); + await getAllFleetServerHostsHandler(context, {} as any, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = ListResponseSchema(FleetServerHostSchema).validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('create fleet server host should return valid response', async () => { + const expectedResponse = { + item: { + id: 'host1', + name: 'host1', + host_urls: ['http://host1:8080'], + is_preconfigured: true, + is_default: true, + proxy_id: 'proxy1', + }, + }; + (createFleetServerHost as jest.Mock).mockResolvedValue(expectedResponse.item); + await postFleetServerHost( + context, + { + body: { + id: 'host1', + }, + } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = FleetServerHostResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('update fleet server host should return valid response', async () => { + const expectedResponse = { + item: { + id: 'host1', + name: 'host1', + host_urls: ['http://host1:8080'], + is_preconfigured: true, + is_default: true, + is_internal: true, + proxy_id: null, + }, + }; + (updateFleetServerHost as jest.Mock).mockResolvedValue(expectedResponse.item); + await putFleetServerHostHandler( + context, + { + body: { + host_urls: [], + }, + params: { itemId: 'host1' }, + } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = FleetServerHostResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('get fleet server host should return valid response', async () => { + const expectedResponse = { + item: { + id: 'host1', + name: 'host1', + host_urls: ['http://host1:8080'], + is_preconfigured: true, + is_default: true, + is_internal: true, + proxy_id: null, + }, + }; + (getFleetServerHost as jest.Mock).mockResolvedValue(expectedResponse.item); + await getFleetServerHostHandler( + context, + { body: {}, params: { itemId: 'host1' } } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = FleetServerHostResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/fleet_server_hosts/index.ts b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/index.ts index 05fd0ab90f9e89..e9cf5cd55a6f48 100644 --- a/x-pack/plugins/fleet/server/routes/fleet_server_hosts/index.ts +++ b/x-pack/plugins/fleet/server/routes/fleet_server_hosts/index.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { schema } from '@kbn/config-schema'; import type { FleetAuthzRouter } from '../../services/security'; @@ -11,12 +12,18 @@ import { API_VERSIONS } from '../../../common/constants'; import { FLEET_SERVER_HOST_API_ROUTES } from '../../../common/constants'; import { + FleetServerHostResponseSchema, + FleetServerHostSchema, GetAllFleetServerHostRequestSchema, GetOneFleetServerHostRequestSchema, PostFleetServerHostRequestSchema, PutFleetServerHostRequestSchema, } from '../../types'; +import { genericErrorResponse } from '../schema/errors'; + +import { ListResponseSchema } from '../schema/utils'; + import { deleteFleetServerHostHandler, getAllFleetServerHostsHandler, @@ -32,11 +39,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: (authz) => { return authz.fleet.addAgents || authz.fleet.addFleetServers || authz.fleet.readSettings; }, + description: `List Fleet Server hosts`, + options: { + tags: ['oas_tag:Fleet Server hosts'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetAllFleetServerHostRequestSchema }, + validate: { + request: GetAllFleetServerHostRequestSchema, + response: { + 200: { + body: () => ListResponseSchema(FleetServerHostSchema), + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getAllFleetServerHostsHandler ); @@ -46,11 +67,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: `Create Fleet Server host`, + options: { + tags: ['oas_tag:Fleet Server hosts'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PostFleetServerHostRequestSchema }, + validate: { + request: PostFleetServerHostRequestSchema, + response: { + 200: { + body: () => FleetServerHostResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, postFleetServerHost ); @@ -60,11 +95,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { readSettings: true }, }, + description: `Get Fleet Server host by ID`, + options: { + tags: ['oas_tag:Fleet Server hosts'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetOneFleetServerHostRequestSchema }, + validate: { + request: GetOneFleetServerHostRequestSchema, + response: { + 200: { + body: () => FleetServerHostResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getFleetServerHostHandler ); @@ -74,11 +123,28 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: `Delete Fleet Server host by ID`, + options: { + tags: ['oas_tag:Fleet Server hosts'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetOneFleetServerHostRequestSchema }, + validate: { + request: GetOneFleetServerHostRequestSchema, + response: { + 200: { + body: () => + schema.object({ + id: schema.string(), + }), + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, deleteFleetServerHostHandler ); @@ -88,11 +154,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: `Update Fleet Server host by ID`, + options: { + tags: ['oas_tag:Fleet Server hosts'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PutFleetServerHostRequestSchema }, + validate: { + request: PutFleetServerHostRequestSchema, + response: { + 200: { + body: () => FleetServerHostResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, putFleetServerHostHandler ); diff --git a/x-pack/plugins/fleet/server/routes/health_check/handler.test.ts b/x-pack/plugins/fleet/server/routes/health_check/handler.test.ts index d418b0d7c560bb..bb36c2ec8146b4 100644 --- a/x-pack/plugins/fleet/server/routes/health_check/handler.test.ts +++ b/x-pack/plugins/fleet/server/routes/health_check/handler.test.ts @@ -8,7 +8,9 @@ import fetch from 'node-fetch'; import * as fleetServerService from '../../services/fleet_server_host'; -import { postHealthCheckHandler } from '.'; +import { PostHealthCheckResponseSchema } from '../../types'; + +import { postHealthCheckHandler } from './handler'; jest.mock('node-fetch'); @@ -102,15 +104,19 @@ describe('Fleet server health_check handler', () => { mockResponse as any ); + const expectedResponse = { + host: 'https://localhost:8220', + host_id: 'default-fleet-server', + name: 'Default', + status: 'ONLINE', + }; expect(res).toEqual({ - body: { - host: 'https://localhost:8220', - host_id: 'default-fleet-server', - name: 'Default', - status: 'ONLINE', - }, + body: expectedResponse, statusCode: 200, }); + + const validateResp = PostHealthCheckResponseSchema.validate(expectedResponse); + expect(validateResp).toEqual(expectedResponse); }); it('should return an error when host id is not found', async () => { @@ -147,12 +153,15 @@ describe('Fleet server health_check handler', () => { mockResponse as any ); + const expectedResponse = { + host_id: 'default-fleet-server', + status: 'OFFLINE', + }; expect(res).toEqual({ - body: { - host_id: 'default-fleet-server', - status: 'OFFLINE', - }, + body: expectedResponse, statusCode: 200, }); + const validateResp = PostHealthCheckResponseSchema.validate(expectedResponse); + expect(validateResp).toEqual(expectedResponse); }); }); diff --git a/x-pack/plugins/fleet/server/routes/health_check/handler.ts b/x-pack/plugins/fleet/server/routes/health_check/handler.ts new file mode 100644 index 00000000000000..d6a782d271d9dc --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/health_check/handler.ts @@ -0,0 +1,89 @@ +/* + * 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 https from 'https'; + +import type { TypeOf } from '@kbn/config-schema'; +import fetch from 'node-fetch'; + +import { getFleetServerHost } from '../../services/fleet_server_host'; + +import type { FleetRequestHandler, PostHealthCheckRequestSchema } from '../../types'; + +import { defaultFleetErrorHandler } from '../../errors'; + +export const postHealthCheckHandler: FleetRequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const abortController = new AbortController(); + const { id, host: deprecatedField } = request.body; + const coreContext = await context.core; + const soClient = coreContext.savedObjects.client; + + if (deprecatedField) { + return response.badRequest({ + body: { + message: `Property 'host' is deprecated. Please use id instead.`, + }, + }); + } + try { + const fleetServerHost = await getFleetServerHost(soClient, id); + + if ( + !fleetServerHost || + !fleetServerHost?.host_urls || + fleetServerHost?.host_urls?.length === 0 + ) { + return response.badRequest({ + body: { + message: `The requested host id ${id} does not have associated host urls.`, + }, + }); + } + + // Sometimes when the host is not online, the request hangs + // Setting a timeout to abort the request after 5s + setTimeout(() => { + abortController.abort(); + }, 5000); + const host = fleetServerHost.host_urls[0]; + + const res = await fetch(`${host}/api/status`, { + headers: { + accept: '*/*', + }, + method: 'GET', + agent: new https.Agent({ + rejectUnauthorized: false, + }), + signal: abortController.signal, + }); + const bodyRes = await res.json(); + const body = { ...bodyRes, host }; + + return response.ok({ body }); + } catch (error) { + if (error.isBoom && error.output.statusCode === 404) { + return response.notFound({ + body: { + message: `The requested host id ${request.body.id} does not exist.`, + }, + }); + } + + // when the request is aborted, return offline status + if (error.name === 'AbortError' || error.message.includes('user aborted')) { + return response.ok({ + body: { status: `OFFLINE`, host_id: request.body.id }, + }); + } + return defaultFleetErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/fleet/server/routes/health_check/index.ts b/x-pack/plugins/fleet/server/routes/health_check/index.ts index 54f4f68b2e7c38..a9f7305bdf9cc1 100644 --- a/x-pack/plugins/fleet/server/routes/health_check/index.ts +++ b/x-pack/plugins/fleet/server/routes/health_check/index.ts @@ -4,21 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import https from 'https'; - -import type { TypeOf } from '@kbn/config-schema'; -import fetch from 'node-fetch'; - -import { getFleetServerHost } from '../../services/fleet_server_host'; import { API_VERSIONS } from '../../../common/constants'; import type { FleetAuthzRouter } from '../../services/security'; import { APP_API_ROUTES } from '../../constants'; -import type { FleetRequestHandler } from '../../types'; +import { PostHealthCheckRequestSchema, PostHealthCheckResponseSchema } from '../../types'; +import { genericErrorResponse } from '../schema/errors'; -import { defaultFleetErrorHandler } from '../../errors'; -import { PostHealthCheckRequestSchema } from '../../types'; +import { postHealthCheckHandler } from './handler'; export const registerRoutes = (router: FleetAuthzRouter) => { // get fleet server health check by host id @@ -29,84 +23,28 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleet: { allSettings: true }, }, description: `Check Fleet Server health`, + options: { + tags: ['oas_tag:Fleet internals'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PostHealthCheckRequestSchema }, + validate: { + request: PostHealthCheckRequestSchema, + response: { + 200: { + body: () => PostHealthCheckResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + 404: { + body: genericErrorResponse, + }, + }, + }, }, postHealthCheckHandler ); }; - -export const postHealthCheckHandler: FleetRequestHandler< - undefined, - undefined, - TypeOf -> = async (context, request, response) => { - const abortController = new AbortController(); - const { id, host: deprecatedField } = request.body; - const coreContext = await context.core; - const soClient = coreContext.savedObjects.client; - - if (deprecatedField) { - return response.badRequest({ - body: { - message: `Property 'host' is deprecated. Please use id instead.`, - }, - }); - } - try { - const fleetServerHost = await getFleetServerHost(soClient, id); - - if ( - !fleetServerHost || - !fleetServerHost?.host_urls || - fleetServerHost?.host_urls?.length === 0 - ) { - return response.badRequest({ - body: { - message: `The requested host id ${id} does not have associated host urls.`, - }, - }); - } - - // Sometimes when the host is not online, the request hangs - // Setting a timeout to abort the request after 5s - setTimeout(() => { - abortController.abort(); - }, 5000); - const host = fleetServerHost.host_urls[0]; - - const res = await fetch(`${host}/api/status`, { - headers: { - accept: '*/*', - }, - method: 'GET', - agent: new https.Agent({ - rejectUnauthorized: false, - }), - signal: abortController.signal, - }); - const bodyRes = await res.json(); - const body = { ...bodyRes, host }; - - return response.ok({ body }); - } catch (error) { - if (error.isBoom && error.output.statusCode === 404) { - return response.notFound({ - body: { - message: `The requested host id ${request.body.id} does not exist.`, - }, - }); - } - - // when the request is aborted, return offline status - if (error.name === 'AbortError' || error.message.includes('user aborted')) { - return response.ok({ - body: { status: `OFFLINE`, host_id: request.body.id }, - }); - } - return defaultFleetErrorHandler({ error, response }); - } -}; diff --git a/x-pack/plugins/fleet/server/routes/message_signing_service/index.ts b/x-pack/plugins/fleet/server/routes/message_signing_service/index.ts index c2d7f125bd0823..a94abf8b8591ed 100644 --- a/x-pack/plugins/fleet/server/routes/message_signing_service/index.ts +++ b/x-pack/plugins/fleet/server/routes/message_signing_service/index.ts @@ -4,12 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { schema } from '@kbn/config-schema'; import type { FleetAuthzRouter } from '../../services/security'; import { API_VERSIONS } from '../../../common/constants'; import { MESSAGE_SIGNING_SERVICE_API_ROUTES } from '../../constants'; import { RotateKeyPairSchema } from '../../types'; +import { genericErrorResponse } from '../schema/errors'; + import { rotateKeyPairHandler } from './handlers'; export const registerRoutes = (router: FleetAuthzRouter) => { @@ -20,11 +23,28 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { all: true }, }, + description: 'Rotate fleet message signing key pair', }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: RotateKeyPairSchema }, + validate: { + request: RotateKeyPairSchema, + response: { + 200: { + body: () => + schema.object({ + message: schema.string(), + }), + }, + 400: { + body: genericErrorResponse, + }, + 500: { + body: genericErrorResponse, + }, + }, + }, }, rotateKeyPairHandler ); diff --git a/x-pack/plugins/fleet/server/routes/output/index.test.ts b/x-pack/plugins/fleet/server/routes/output/index.test.ts new file mode 100644 index 00000000000000..ea570b7dbc12a8 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/output/index.test.ts @@ -0,0 +1,252 @@ +/* + * 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 { httpServerMock } from '@kbn/core-http-server-mocks'; + +import type { FleetRequestHandlerContext } from '../..'; +import { outputService } from '../../services/output'; + +import { xpackMocks } from '../../mocks'; +import type { Output } from '../../types'; +import { + DeleteOutputResponseSchema, + GenerateLogstashApiKeyResponseSchema, + GetLatestOutputHealthResponseSchema, + GetOutputsResponseSchema, + OutputResponseSchema, +} from '../../types'; + +import { + deleteOutputHandler, + getLatestOutputHealth, + getOneOuputHandler, + getOutputsHandler, + postLogstashApiKeyHandler, + postOutputHandler, + putOutputHandler, +} from './handler'; + +jest.mock('../../services', () => ({ + agentPolicyService: { + bumpAllAgentPoliciesForOutput: jest.fn().mockResolvedValue({} as any), + bumpAllAgentPolicies: jest.fn().mockResolvedValue({} as any), + }, + appContextService: { + getLogger: jest.fn().mockReturnValue({ error: jest.fn() } as any), + getCloud: jest.fn().mockReturnValue({ isServerlessEnabled: false } as any), + }, +})); + +jest.mock('../../services/output', () => ({ + outputService: { + list: jest.fn().mockResolvedValue({ + items: [ + { + id: 'output1', + type: 'elasticsearch', + hosts: ['http://elasticsearch:9200'], + is_default: true, + is_default_monitoring: true, + name: 'Default', + }, + ], + total: 1, + page: 1, + perPage: 20, + }), + create: jest.fn().mockResolvedValue({ id: 'output1' }), + update: jest.fn().mockResolvedValue({}), + get: jest.fn().mockResolvedValue({ id: 'output1' }), + delete: jest.fn().mockResolvedValue({}), + getLatestOutputHealth: jest.fn().mockResolvedValue({ + state: 'HEALTHY', + message: '', + timestamp: '2021-01-01T00:00:00Z', + }), + }, +})); + +jest.mock('../../services/api_keys', () => ({ + canCreateLogstashApiKey: jest.fn().mockResolvedValue(true), + generateLogstashApiKey: jest.fn().mockResolvedValue({ + id: 'id', + api_key: 'apikey', + }), +})); + +const outputServiceMock = outputService as jest.Mocked; + +describe('schema validation', () => { + let context: FleetRequestHandlerContext; + let response: ReturnType; + + beforeEach(() => { + context = xpackMocks.createRequestHandlerContext() as unknown as FleetRequestHandlerContext; + response = httpServerMock.createResponseFactory(); + }); + + it('get outputs should return valid response', async () => { + const expectedResponse = { + items: [ + { + id: 'output1', + type: 'elasticsearch', + hosts: ['http://elasticsearch:9200'], + is_default: true, + is_default_monitoring: true, + name: 'Default', + }, + ], + total: 1, + page: 1, + perPage: 20, + }; + await getOutputsHandler(context, {} as any, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = GetOutputsResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('get one output should return valid response', async () => { + const output: Output = { + id: 'output1', + type: 'remote_elasticsearch', + hosts: ['http://elasticsearch:9200'], + is_default: true, + is_default_monitoring: true, + name: 'Default', + secrets: { + service_token: 'ref1', + }, + }; + const expectedResponse = { item: output }; + outputServiceMock.get.mockResolvedValue(output); + await getOneOuputHandler(context, { params: { outputId: 'output1' } } as any, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = OutputResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('put output should return valid response', async () => { + const output: Output = { + id: 'output1', + type: 'logstash', + hosts: ['logstash'], + is_default: false, + is_default_monitoring: false, + name: 'Default', + secrets: { + ssl: { key: 'ref1' }, + }, + client_id: null, // extra field from previous kafka output + } as any; + const expectedResponse = { item: output }; + outputServiceMock.get.mockResolvedValue(output); + await putOutputHandler( + context, + { + params: { outputId: 'output1' }, + body: { + name: 'Default', + }, + } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = OutputResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('create output should return valid response', async () => { + const output: Output = { + id: 'output1', + type: 'kafka', + hosts: ['kafka:8000'], + is_default: false, + is_default_monitoring: false, + name: 'Default', + secrets: { + ssl: { key: 'ref1' }, + password: 'password', + }, + auth_type: 'ssl', + sasl: { + mechanism: 'PLAIN', + }, + password: null, + username: null, + }; + const expectedResponse = { item: output }; + outputServiceMock.create.mockResolvedValue(output); + await postOutputHandler( + context, + { + params: { outputId: 'output1' }, + body: { + name: 'Default', + }, + } as any, + response + ); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = OutputResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('delete output should return valid response', async () => { + const expectedResponse = { + id: 'output1', + }; + await deleteOutputHandler(context, { params: { outputId: 'output1' } } as any, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = DeleteOutputResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('post logstash api key should return valid response', async () => { + const expectedResponse = { + api_key: 'id:apikey', + }; + await postLogstashApiKeyHandler(context, {} as any, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = GenerateLogstashApiKeyResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); + + it('get latest output health should return valid response', async () => { + const expectedResponse = { + state: 'HEALTHY', + message: '', + timestamp: '2021-01-01T00:00:00Z', + }; + await getLatestOutputHealth(context, { params: { outputId: 'output1' } } as any, response); + + expect(response.ok).toHaveBeenCalledWith({ + body: expectedResponse, + }); + const validationResp = GetLatestOutputHealthResponseSchema.validate(expectedResponse); + expect(validationResp).toEqual(expectedResponse); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/output/index.ts b/x-pack/plugins/fleet/server/routes/output/index.ts index 0ca7f350b29257..4a8d4aeb575039 100644 --- a/x-pack/plugins/fleet/server/routes/output/index.ts +++ b/x-pack/plugins/fleet/server/routes/output/index.ts @@ -12,13 +12,20 @@ import { API_VERSIONS } from '../../../common/constants'; import { OUTPUT_API_ROUTES } from '../../constants'; import { DeleteOutputRequestSchema, + DeleteOutputResponseSchema, + GenerateLogstashApiKeyResponseSchema, GetLatestOutputHealthRequestSchema, + GetLatestOutputHealthResponseSchema, GetOneOutputRequestSchema, GetOutputsRequestSchema, + GetOutputsResponseSchema, + OutputResponseSchema, PostOutputRequestSchema, PutOutputRequestSchema, } from '../../types'; +import { genericErrorResponse } from '../schema/errors'; + import { deleteOutputHandler, getOneOuputHandler, @@ -36,11 +43,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { readSettings: true }, }, + description: 'List outputs', + options: { + tags: ['oas_tag:Fleet outputs'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetOutputsRequestSchema }, + validate: { + request: GetOutputsRequestSchema, + response: { + 200: { + body: () => GetOutputsResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getOutputsHandler ); @@ -50,11 +71,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { readSettings: true }, }, + description: 'Get output by ID', + options: { + tags: ['oas_tag:Fleet outputs'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetOneOutputRequestSchema }, + validate: { + request: GetOneOutputRequestSchema, + response: { + 200: { + body: () => OutputResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getOneOuputHandler ); @@ -64,11 +99,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: 'Update output by ID', + options: { + tags: ['oas_tag:Fleet outputs'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PutOutputRequestSchema }, + validate: { + request: PutOutputRequestSchema, + response: { + 200: { + body: () => OutputResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, putOutputHandler ); @@ -79,11 +128,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: 'Create output', + options: { + tags: ['oas_tag:Fleet outputs'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: PostOutputRequestSchema }, + validate: { + request: PostOutputRequestSchema, + response: { + 200: { + body: () => OutputResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, postOutputHandler ); @@ -94,11 +157,28 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: 'Delete output by ID', + options: { + tags: ['oas_tag:Fleet outputs'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: DeleteOutputRequestSchema }, + validate: { + request: DeleteOutputRequestSchema, + response: { + 200: { + body: () => DeleteOutputResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + 404: { + body: genericErrorResponse, + }, + }, + }, }, deleteOutputHandler ); @@ -109,11 +189,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { allSettings: true }, }, + description: 'Generate Logstash API keyy', + options: { + tags: ['oas_tag:Fleet outputs'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: false, + validate: { + request: {}, + response: { + 200: { + body: () => GenerateLogstashApiKeyResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, postLogstashApiKeyHandler ); @@ -124,11 +218,25 @@ export const registerRoutes = (router: FleetAuthzRouter) => { fleetAuthz: { fleet: { readSettings: true }, }, + description: 'Get latest output health', + options: { + tags: ['oas_tag:Fleet outputs'], + }, }) .addVersion( { version: API_VERSIONS.public.v1, - validate: { request: GetLatestOutputHealthRequestSchema }, + validate: { + request: GetLatestOutputHealthRequestSchema, + response: { + 200: { + body: () => GetLatestOutputHealthResponseSchema, + }, + 400: { + body: genericErrorResponse, + }, + }, + }, }, getLatestOutputHealth ); diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index 38441b06eabbfa..34aa75b60bae54 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -65,10 +65,10 @@ export const AgentPolicyBaseSchema = { ) ), keep_monitoring_alive: schema.maybe(schema.boolean({ defaultValue: false })), - data_output_id: schema.maybe(schema.nullable(schema.string())), - monitoring_output_id: schema.maybe(schema.nullable(schema.string())), - download_source_id: schema.maybe(schema.nullable(schema.string())), - fleet_server_host_id: schema.maybe(schema.nullable(schema.string())), + data_output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + monitoring_output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + download_source_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + fleet_server_host_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), agent_features: schema.maybe( schema.arrayOf( schema.object({ diff --git a/x-pack/plugins/fleet/server/types/models/output.ts b/x-pack/plugins/fleet/server/types/models/output.ts index 774d070931b5b8..4385f9911e3db0 100644 --- a/x-pack/plugins/fleet/server/types/models/output.ts +++ b/x-pack/plugins/fleet/server/types/models/output.ts @@ -66,39 +66,47 @@ const BaseSchema = { is_default: schema.boolean({ defaultValue: false }), is_default_monitoring: schema.boolean({ defaultValue: false }), is_internal: schema.maybe(schema.boolean()), - ca_sha256: schema.maybe(schema.string()), - ca_trusted_fingerprint: schema.maybe(schema.string()), - config_yaml: schema.maybe(schema.string()), + is_preconfigured: schema.maybe(schema.boolean()), + ca_sha256: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + ca_trusted_fingerprint: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + config_yaml: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), ssl: schema.maybe( - schema.object({ - certificate_authorities: schema.maybe(schema.arrayOf(schema.string())), - certificate: schema.maybe(schema.string()), - key: schema.maybe(schema.string()), - verification_mode: schema.maybe( - schema.oneOf([ - schema.literal(kafkaVerificationModes.Full), - schema.literal(kafkaVerificationModes.None), - schema.literal(kafkaVerificationModes.Certificate), - schema.literal(kafkaVerificationModes.Strict), - ]) - ), - }) + schema.oneOf([ + schema.literal(null), + schema.object({ + certificate_authorities: schema.maybe(schema.arrayOf(schema.string())), + certificate: schema.maybe(schema.string()), + key: schema.maybe(schema.string()), + verification_mode: schema.maybe( + schema.oneOf([ + schema.literal(kafkaVerificationModes.Full), + schema.literal(kafkaVerificationModes.None), + schema.literal(kafkaVerificationModes.Certificate), + schema.literal(kafkaVerificationModes.Strict), + ]) + ), + }), + ]) ), - proxy_id: schema.nullable(schema.string()), + proxy_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), shipper: schema.maybe( - schema.object({ - disk_queue_enabled: schema.nullable(schema.boolean({ defaultValue: false })), - disk_queue_path: schema.nullable(schema.string()), - disk_queue_max_size: schema.nullable(schema.number()), - disk_queue_encryption_enabled: schema.nullable(schema.boolean()), - disk_queue_compression_enabled: schema.nullable(schema.boolean()), - compression_level: schema.nullable(schema.number()), - loadbalance: schema.nullable(schema.boolean()), - mem_queue_events: schema.nullable(schema.number()), - queue_flush_timeout: schema.nullable(schema.number()), - max_batch_bytes: schema.nullable(schema.number()), - }) + schema.oneOf([ + schema.literal(null), + schema.object({ + disk_queue_enabled: schema.nullable(schema.boolean({ defaultValue: false })), + disk_queue_path: schema.nullable(schema.string()), + disk_queue_max_size: schema.nullable(schema.number()), + disk_queue_encryption_enabled: schema.nullable(schema.boolean()), + disk_queue_compression_enabled: schema.nullable(schema.boolean()), + compression_level: schema.nullable(schema.number()), + loadbalance: schema.nullable(schema.boolean()), + mem_queue_events: schema.nullable(schema.number()), + queue_flush_timeout: schema.nullable(schema.number()), + max_batch_bytes: schema.nullable(schema.number()), + }), + ]) ), + allow_edit: schema.maybe(schema.arrayOf(schema.string())), }; const UpdateSchema = { @@ -108,6 +116,14 @@ const UpdateSchema = { is_default_monitoring: schema.maybe(schema.boolean()), }; +const PresetSchema = schema.oneOf([ + schema.literal('balanced'), + schema.literal('custom'), + schema.literal('throughput'), + schema.literal('scale'), + schema.literal('latency'), +]); + /** * Elasticsearch schemas */ @@ -116,30 +132,14 @@ export const ElasticSearchSchema = { ...BaseSchema, type: schema.literal(outputType.Elasticsearch), hosts: schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { minSize: 1 }), - preset: schema.maybe( - schema.oneOf([ - schema.literal('balanced'), - schema.literal('custom'), - schema.literal('throughput'), - schema.literal('scale'), - schema.literal('latency'), - ]) - ), + preset: schema.maybe(PresetSchema), }; const ElasticSearchUpdateSchema = { ...UpdateSchema, type: schema.maybe(schema.literal(outputType.Elasticsearch)), hosts: schema.maybe(schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { minSize: 1 })), - preset: schema.maybe( - schema.oneOf([ - schema.literal('balanced'), - schema.literal('custom'), - schema.literal('throughput'), - schema.literal('scale'), - schema.literal('latency'), - ]) - ), + preset: schema.maybe(PresetSchema), }; /** @@ -149,7 +149,7 @@ const ElasticSearchUpdateSchema = { export const RemoteElasticSearchSchema = { ...ElasticSearchSchema, type: schema.literal(outputType.RemoteElasticsearch), - service_token: schema.maybe(schema.string()), + service_token: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), secrets: schema.maybe( schema.object({ service_token: schema.maybe(secretRefSchema), @@ -160,7 +160,7 @@ export const RemoteElasticSearchSchema = { const RemoteElasticSearchUpdateSchema = { ...ElasticSearchUpdateSchema, type: schema.maybe(schema.literal(outputType.RemoteElasticsearch)), - service_token: schema.maybe(schema.string()), + service_token: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), secrets: schema.maybe( schema.object({ service_token: schema.maybe(secretRefSchema), @@ -255,33 +255,40 @@ export const KafkaSchema = { ]), schema.never() ), - username: schema.conditional( - schema.siblingRef('auth_type'), - kafkaAuthType.Userpass, - schema.string(), - schema.never() - ), - password: schema.conditional( - schema.siblingRef('secrets.password'), - secretRefSchema, - schema.never(), + username: schema.nullable( schema.conditional( - schema.siblingRef('username'), - schema.string(), + schema.siblingRef('auth_type'), + kafkaAuthType.Userpass, schema.string(), schema.never() ) ), + password: schema.nullable( + schema.conditional( + schema.siblingRef('secrets.password'), + secretRefSchema, + schema.never(), + schema.conditional( + schema.siblingRef('username'), + schema.string(), + schema.string(), + schema.never() + ) + ) + ), sasl: schema.maybe( - schema.object({ - mechanism: schema.maybe( - schema.oneOf([ - schema.literal(kafkaSaslMechanism.Plain), - schema.literal(kafkaSaslMechanism.ScramSha256), - schema.literal(kafkaSaslMechanism.ScramSha512), - ]) - ), - }) + schema.oneOf([ + schema.literal(null), + schema.object({ + mechanism: schema.maybe( + schema.oneOf([ + schema.literal(kafkaSaslMechanism.Plain), + schema.literal(kafkaSaslMechanism.ScramSha256), + schema.literal(kafkaSaslMechanism.ScramSha512), + ]) + ), + }), + ]) ), partition: schema.maybe( schema.oneOf([ @@ -338,6 +345,12 @@ export const OutputSchema = schema.oneOf([ schema.object({ ...KafkaSchema }), ]); +export const OutputResponseSchema = schema.object({ + item: OutputSchema.extendsDeep({ + unknowns: 'allow', + }), +}); + export const UpdateOutputSchema = schema.oneOf([ schema.object({ ...ElasticSearchUpdateSchema }), schema.object({ ...RemoteElasticSearchUpdateSchema }), diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index 79586b1885ed97..5c49ef98aabcf0 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -162,7 +162,7 @@ export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( }), description: schema.maybe(schema.string()), namespace: schema.maybe(PackagePolicyNamespaceSchema), - output_id: schema.nullable(schema.maybe(schema.string())), + output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), inputs: schema.maybe( schema.arrayOf( schema.object({ diff --git a/x-pack/plugins/fleet/server/types/rest_spec/check_permissions.ts b/x-pack/plugins/fleet/server/types/rest_spec/check_permissions.ts index 31850308f9a115..9905e72335ee37 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/check_permissions.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/check_permissions.ts @@ -12,3 +12,14 @@ export const CheckPermissionsRequestSchema = { fleetServerSetup: schema.maybe(schema.boolean()), }), }; + +export const CheckPermissionsResponseSchema = schema.object({ + success: schema.boolean(), + error: schema.maybe( + schema.oneOf([ + schema.literal('MISSING_SECURITY'), + schema.literal('MISSING_PRIVILEGES'), + schema.literal('MISSING_FLEET_SERVER_SETUP_PRIVILEGES'), + ]) + ), +}); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/fleet_proxies.ts b/x-pack/plugins/fleet/server/types/rest_spec/fleet_proxies.ts index c3f1f8535760c5..fc19ecd6151db2 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/fleet_proxies.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/fleet_proxies.ts @@ -15,25 +15,37 @@ function validateUrl(value: string) { } } -export const PostFleetProxyRequestSchema = { - body: schema.object({ - id: schema.maybe(schema.string()), - url: schema.string({ - validate: validateUrl, - }), - name: schema.string(), - proxy_headers: schema.maybe( +export const FleetProxySchema = schema.object({ + id: schema.string(), + url: schema.string({ + validate: validateUrl, + }), + name: schema.string(), + proxy_headers: schema.maybe( + schema.oneOf([ + schema.literal(null), schema.recordOf( schema.string(), schema.oneOf([schema.string(), schema.boolean(), schema.number()]) - ) - ), - certificate_authorities: schema.maybe(schema.string()), - certificate: schema.maybe(schema.string()), - certificate_key: schema.maybe(schema.string()), + ), + ]) + ), + certificate_authorities: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + certificate: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + certificate_key: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), + is_preconfigured: schema.boolean({ defaultValue: false }), +}); + +export const PostFleetProxyRequestSchema = { + body: FleetProxySchema.extends({ + id: schema.maybe(schema.string()), }), }; +export const FleetProxyResponseSchema = schema.object({ + item: FleetProxySchema, +}); + export const PutFleetProxyRequestSchema = { params: schema.object({ itemId: schema.string() }), body: schema.object({ diff --git a/x-pack/plugins/fleet/server/types/rest_spec/fleet_server_policy_config.ts b/x-pack/plugins/fleet/server/types/rest_spec/fleet_server_policy_config.ts index d3f2f36a1624f0..94100e24c2f6c1 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/fleet_server_policy_config.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/fleet_server_policy_config.ts @@ -7,14 +7,23 @@ import { schema } from '@kbn/config-schema'; +export const FleetServerHostSchema = schema.object({ + id: schema.string(), + name: schema.string(), + host_urls: schema.arrayOf(schema.string(), { minSize: 1 }), + is_default: schema.boolean({ defaultValue: false }), + is_internal: schema.maybe(schema.boolean()), + is_preconfigured: schema.boolean({ defaultValue: false }), + proxy_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), +}); + +export const FleetServerHostResponseSchema = schema.object({ + item: FleetServerHostSchema, +}); + export const PostFleetServerHostRequestSchema = { - body: schema.object({ + body: FleetServerHostSchema.extends({ id: schema.maybe(schema.string()), - name: schema.string(), - host_urls: schema.arrayOf(schema.string(), { minSize: 1 }), - is_default: schema.boolean({ defaultValue: false }), - is_internal: schema.maybe(schema.boolean()), - proxy_id: schema.nullable(schema.string()), }), }; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/health_check.ts b/x-pack/plugins/fleet/server/types/rest_spec/health_check.ts index 0a3ca309ddf51d..72c40e5bd6a681 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/health_check.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/health_check.ts @@ -14,3 +14,16 @@ export const PostHealthCheckRequestSchema = { host: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), }), }; + +export const PostHealthCheckResponseSchema = schema.object({ + status: schema.string(), + name: schema.maybe(schema.string()), + host: schema.maybe( + schema.string({ + meta: { + deprecated: true, + }, + }) + ), + host_id: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/fleet/server/types/rest_spec/output.ts b/x-pack/plugins/fleet/server/types/rest_spec/output.ts index 8a2d6babbd7748..e6e2832b102a4d 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/output.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/output.ts @@ -8,6 +8,7 @@ import { schema } from '@kbn/config-schema'; import { OutputSchema, UpdateOutputSchema } from '../models'; +import { ListResponseSchema } from '../../routes/schema/utils'; export const GetOneOutputRequestSchema = { params: schema.object({ @@ -21,8 +22,22 @@ export const DeleteOutputRequestSchema = { }), }; +export const DeleteOutputResponseSchema = schema.object({ + id: schema.string(), +}); + +export const GenerateLogstashApiKeyResponseSchema = schema.object({ + api_key: schema.string(), +}); + export const GetOutputsRequestSchema = {}; +export const GetOutputsResponseSchema = ListResponseSchema( + OutputSchema.extendsDeep({ + unknowns: 'allow', + }) +); + export const PostOutputRequestSchema = { body: OutputSchema, }; @@ -39,3 +54,21 @@ export const GetLatestOutputHealthRequestSchema = { outputId: schema.string(), }), }; + +export const GetLatestOutputHealthResponseSchema = schema.object({ + state: schema.string({ + meta: { + description: 'state of output, HEALTHY or DEGRADED', + }, + }), + message: schema.string({ + meta: { + description: 'long message if unhealthy', + }, + }), + timestamp: schema.string({ + meta: { + description: 'timestamp of reported state', + }, + }), +}); diff --git a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts index a6c266cc2c8ced..564259cb49a23a 100644 --- a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts @@ -1892,6 +1892,64 @@ export default function (providerContext: FtrProviderContext) { // not found } }); + + it('should update kafka output to logstash output', async function () { + // Output secrets require at least one Fleet server on 8.12.0 or higher (and none under 8.12.0). + await clearAgents(); + await createFleetServerAgent(fleetServerPolicyId, 'server_1', '8.12.0'); + const res = await supertest + .post(`/api/fleet/outputs`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Kafka Output With Secret', + type: 'kafka', + hosts: ['test.fr:2000'], + auth_type: 'ssl', + topics: [{ topic: 'topic1' }], + config_yaml: '', + compression: 'none', + client_id: 'Elastic', + partition: 'random', + version: '1.0.0', + required_acks: 1, + ssl: { + certificate: 'CERTIFICATE', + certificate_authorities: ['CA1', 'CA2'], + }, + secrets: { + ssl: { + key: 'KEY', + }, + }, + }) + .expect(200); + + const outputWithSecretsId = res.body.item.id; + + const updateRes = await supertest + .put(`/api/fleet/outputs/${outputWithSecretsId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'kafka_to_logstash', + type: 'logstash', + hosts: ['logstash'], + is_default: false, + is_default_monitoring: false, + config_yaml: '', + ssl: { certificate: 'cert', certificate_authorities: ['ca'] }, + secrets: { ssl: { key: 'key' } }, + proxy_id: null, + }) + .expect(200); + + expect(updateRes.body.item.type).to.eql('logstash'); + expect(updateRes.body.item.topics).to.eql(null); + + await supertest + .delete(`/api/fleet/outputs/${outputWithSecretsId}`) + .set('kbn-xsrf', 'xxxx') + .expect(200); + }); }); }); });