diff --git a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts index 24cd482352930..33360bf82ef0c 100644 --- a/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts +++ b/packages/kbn-test/src/kbn_client/kbn_client_saved_objects.ts @@ -254,6 +254,7 @@ export class KbnClientSavedObjects { 'epm-packages', 'epm-packages-assets', 'fleet-preconfiguration-deletion-record', + 'fleet-fleet-server-host', ]; const newOptions = { types, space: options?.space }; diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index d7704db87476f..e7d02da09889d 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -88,6 +88,7 @@ describe('checking migration metadata changes on all registered SO types', () => "file": "280f28bd48b3ad1f1a9f84c6c0ae6dd5ed1179da", "file-upload-usage-collection-telemetry": "8478924cf0057bd90df737155b364f98d05420a5", "fileShare": "3f88784b041bb8728a7f40763a08981828799a75", + "fleet-fleet-server-host": "f00ca963f1bee868806319789cdc33f1f53a97e2", "fleet-preconfiguration-deletion-record": "7b28f200513c28ae774f1b7d7d7906954e3c6e16", "graph-workspace": "3342f2cd561afdde8f42f5fb284bf550dee8ebb5", "guided-onboarding-guide-state": "561db8d481b131a2bbf46b1e534d6ce960255135", diff --git a/src/core/server/integration_tests/saved_objects/migrations/type_registrations.test.ts b/src/core/server/integration_tests/saved_objects/migrations/type_registrations.test.ts index 4fd5ca5cd2aea..eb9eb8a420695 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/type_registrations.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/type_registrations.test.ts @@ -58,6 +58,7 @@ const previouslyRegisteredTypes = [ 'fleet-agents', 'fleet-enrollment-api-keys', 'fleet-preconfiguration-deletion-record', + 'fleet-fleet-server-host', 'graph-workspace', 'guided-setup-state', 'guided-onboarding-guide-state', diff --git a/x-pack/plugins/fleet/common/constants/fleet_server_policy_config.ts b/x-pack/plugins/fleet/common/constants/fleet_server_policy_config.ts new file mode 100644 index 0000000000000..17a2193bf19c2 --- /dev/null +++ b/x-pack/plugins/fleet/common/constants/fleet_server_policy_config.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export const FLEET_SERVER_HOST_SAVED_OBJECT_TYPE = 'fleet-fleet-server-host'; + +export const DEFAULT_FLEET_SERVER_HOST_ID = 'fleet-default-fleet-server-host'; diff --git a/x-pack/plugins/fleet/common/constants/index.ts b/x-pack/plugins/fleet/common/constants/index.ts index 955abb6b7456c..01193687125c6 100644 --- a/x-pack/plugins/fleet/common/constants/index.ts +++ b/x-pack/plugins/fleet/common/constants/index.ts @@ -16,6 +16,7 @@ export * from './enrollment_api_key'; export * from './settings'; export * from './preconfiguration'; export * from './download_source'; +export * from './fleet_server_policy_config'; export * from './authz'; // TODO: This is the default `index.max_result_window` ES setting, which dictates diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index fa9e074899163..d48e07223cdf9 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -88,6 +88,15 @@ export const OUTPUT_API_ROUTES = { LOGSTASH_API_KEY_PATTERN: `${API_ROOT}/logstash_api_keys`, }; +// Fleet server API routes +export const FLEET_SERVER_HOST_API_ROUTES = { + LIST_PATTERN: `${API_ROOT}/fleet_server_hosts`, + CREATE_PATTERN: `${API_ROOT}/fleet_server_hosts`, + INFO_PATTERN: `${API_ROOT}/fleet_server_hosts/{itemId}`, + UPDATE_PATTERN: `${API_ROOT}/fleet_server_hosts/{itemId}`, + DELETE_PATTERN: `${API_ROOT}/fleet_server_hosts/{itemId}`, +}; + // Settings API routes export const SETTINGS_API_ROUTES = { INFO_PATTERN: `${API_ROOT}/settings`, diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 71b4ec04d4e98..b228433c1626f 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -3706,6 +3706,230 @@ } ] } + }, + "/fleet_server_hosts": { + "get": { + "summary": "Fleet Server Hosts - List", + "description": "Return a list of Fleet server host", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/fleet_server_host" + } + }, + "total": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "perPage": { + "type": "integer" + } + } + } + } + } + } + }, + "operationId": "get-fleet-server-hosts" + }, + "post": { + "summary": "Fleet Server Hosts - Create", + "description": "Create a new Fleet Server Host", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/fleet_server_host" + } + } + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "host_urls": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "name", + "host_urls" + ] + } + } + } + }, + "operationId": "post-fleet-server-hosts" + } + }, + "/fleet_server_hosts/{itemId}": { + "get": { + "summary": "Fleet Server Hosts - Info", + "tags": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/fleet_server_host" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "operationId": "get-one-fleet-server-hosts" + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "itemId", + "in": "path", + "required": true + } + ], + "delete": { + "summary": "Fleet Server Hosts - Delete", + "operationId": "delete-fleet-server-hosts", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ] + } + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "itemId", + "in": "path", + "required": true + }, + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + }, + "put": { + "summary": "Fleet Server Hosts - Update", + "operationId": "update-fleet-server-hosts", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "host_urls": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "item": { + "$ref": "#/components/schemas/fleet_server_host" + } + }, + "required": [ + "item" + ] + } + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "name": "itemId", + "in": "path", + "required": true + }, + { + "$ref": "#/components/parameters/kbn_xsrf" + } + ] + } } }, "components": { @@ -3977,9 +4201,6 @@ "has_seen_add_data_notice": { "type": "boolean" }, - "has_seen_fleet_migration_notice": { - "type": "boolean" - }, "fleet_server_hosts": { "type": "array", "items": { @@ -5470,6 +5691,37 @@ "name", "host" ] + }, + "fleet_server_host": { + "title": "Fleet Server Host", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "is_preconfigured": { + "type": "boolean" + }, + "host_urls": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "fleet_server_hosts", + "id", + "is_default", + "is_preconfigured", + "host_urls" + ] } } }, diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 92347809b0616..f5f9dedf462d2 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -2289,6 +2289,145 @@ paths: operationId: generate-logstash-api-key parameters: - $ref: '#/components/parameters/kbn_xsrf' + /fleet_server_hosts: + get: + summary: Fleet Server Hosts - List + description: Return a list of Fleet server host + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/fleet_server_host' + total: + type: integer + page: + type: integer + perPage: + type: integer + operationId: get-fleet-server-hosts + post: + summary: Fleet Server Hosts - Create + description: Create a new Fleet Server Host + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/fleet_server_host' + requestBody: + content: + application/json: + schema: + type: object + properties: + id: + type: string + name: + type: string + is_default: + type: boolean + host_urls: + type: array + items: + type: string + required: + - name + - host_urls + operationId: post-fleet-server-hosts + /fleet_server_hosts/{itemId}: + get: + summary: Fleet Server Hosts - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/fleet_server_host' + required: + - item + operationId: get-one-fleet-server-hosts + parameters: + - schema: + type: string + name: itemId + in: path + required: true + delete: + summary: Fleet Server Hosts - Delete + operationId: delete-fleet-server-hosts + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: string + required: + - id + parameters: + - schema: + type: string + name: itemId + in: path + required: true + - $ref: '#/components/parameters/kbn_xsrf' + put: + summary: Fleet Server Hosts - Update + operationId: update-fleet-server-hosts + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + is_default: + type: boolean + host_urls: + type: array + items: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: '#/components/schemas/fleet_server_host' + required: + - item + parameters: + - schema: + type: string + name: itemId + in: path + required: true + - $ref: '#/components/parameters/kbn_xsrf' components: securitySchemes: basicAuth: @@ -2474,8 +2613,6 @@ components: type: string has_seen_add_data_notice: type: boolean - has_seen_fleet_migration_notice: - type: boolean fleet_server_hosts: type: array items: @@ -3509,5 +3646,27 @@ components: - is_default - name - host + fleet_server_host: + title: Fleet Server Host + type: object + properties: + id: + type: string + name: + type: string + is_default: + type: boolean + is_preconfigured: + type: boolean + host_urls: + type: array + items: + type: string + required: + - fleet_server_hosts + - id + - is_default + - is_preconfigured + - host_urls security: - basicAuth: [] diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_server_host.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_server_host.yaml new file mode 100644 index 0000000000000..133bc7fcce13c --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/fleet_server_host.yaml @@ -0,0 +1,21 @@ +title: Fleet Server Host +type: object +properties: + id: + type: string + name: + type: string + is_default: + type: boolean + is_preconfigured: + type: boolean + host_urls: + type: array + items: + type: string +required: + - fleet_server_hosts + - id + - is_default + - is_preconfigured + - host_urls diff --git a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml index 4d5126d10a412..bccfa64ff772d 100644 --- a/x-pack/plugins/fleet/common/openapi/entrypoint.yaml +++ b/x-pack/plugins/fleet/common/openapi/entrypoint.yaml @@ -124,6 +124,11 @@ paths: $ref: paths/agent_download_sources@{source_id}.yaml /logstash_api_keys: $ref: paths/logstash_api_keys.yaml + # Fleet server hosts + /fleet_server_hosts: + $ref: paths/fleet_server_hosts.yaml + /fleet_server_hosts/{itemId}: + $ref: paths/fleet_server_hosts@{item_id}.yaml components: securitySchemes: basicAuth: diff --git a/x-pack/plugins/fleet/common/openapi/paths/fleet_server_hosts.yaml b/x-pack/plugins/fleet/common/openapi/paths/fleet_server_hosts.yaml new file mode 100644 index 0000000000000..4ad7d867edc97 --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/fleet_server_hosts.yaml @@ -0,0 +1,57 @@ +get: + summary: Fleet Server Hosts - List + description: Return a list of Fleet server hosts + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + items: + type: array + items: + $ref: ../components/schemas/fleet_server_host.yaml + total: + type: integer + page: + type: integer + perPage: + type: integer + operationId: get-fleet-server-hosts +post: + summary: Fleet Server Hosts - Create + description: 'Create a new Fleet Server Host' + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/fleet_server_host.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + id: + type: string + name: + type: string + is_default: + type: boolean + host_urls: + type: array + items: + type: string + required: + - name + - host_urls + operationId: post-fleet-server-hosts diff --git a/x-pack/plugins/fleet/common/openapi/paths/fleet_server_hosts@{item_id}.yaml b/x-pack/plugins/fleet/common/openapi/paths/fleet_server_hosts@{item_id}.yaml new file mode 100644 index 0000000000000..b2a50f8b52b8e --- /dev/null +++ b/x-pack/plugins/fleet/common/openapi/paths/fleet_server_hosts@{item_id}.yaml @@ -0,0 +1,80 @@ +get: + summary: Fleet Server Hosts - Info + tags: [] + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/fleet_server_host.yaml + required: + - item + operationId: get-one-fleet-server-hosts +parameters: + - schema: + type: string + name: itemId + in: path + required: true +delete: + summary: Fleet Server Hosts - Delete + operationId: delete-fleet-server-hosts + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + id: + type: string + required: + - id + parameters: + - schema: + type: string + name: itemId + in: path + required: true + - $ref: ../components/headers/kbn_xsrf.yaml +put: + summary: Fleet Server Hosts - Update + operationId: update-fleet-server-hosts + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + is_default: + type: boolean + host_urls: + type: array + items: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + item: + $ref: ../components/schemas/fleet_server_host.yaml + required: + - item + parameters: + - schema: + type: string + name: itemId + in: path + required: true + - $ref: ../components/headers/kbn_xsrf.yaml diff --git a/x-pack/plugins/fleet/common/types/models/fleet_server_policy_config.ts b/x-pack/plugins/fleet/common/types/models/fleet_server_policy_config.ts new file mode 100644 index 0000000000000..11930e7bfd637 --- /dev/null +++ b/x-pack/plugins/fleet/common/types/models/fleet_server_policy_config.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export interface NewFleetServerHost { + name: string; + host_urls: string[]; + is_default: boolean; + is_preconfigured: boolean; +} + +export interface FleetServerHost extends NewFleetServerHost { + id: string; +} + +export type FleetServerHostSOAttributes = NewFleetServerHost; diff --git a/x-pack/plugins/fleet/common/types/models/index.ts b/x-pack/plugins/fleet/common/types/models/index.ts index 07becab0af3f3..78429c36dd0ae 100644 --- a/x-pack/plugins/fleet/common/types/models/index.ts +++ b/x-pack/plugins/fleet/common/types/models/index.ts @@ -16,3 +16,4 @@ export * from './enrollment_api_key'; export * from './settings'; export * from './preconfiguration'; export * from './download_sources'; +export * from './fleet_server_policy_config'; diff --git a/x-pack/plugins/fleet/common/types/rest_spec/fleet_server_hosts.ts b/x-pack/plugins/fleet/common/types/rest_spec/fleet_server_hosts.ts new file mode 100644 index 0000000000000..bf8be3cb38407 --- /dev/null +++ b/x-pack/plugins/fleet/common/types/rest_spec/fleet_server_hosts.ts @@ -0,0 +1,12 @@ +/* + * 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 type { FleetServerHost } from '../models'; + +import type { ListResult } from './common'; + +export type GetFleetServerHostsResponse = ListResult; diff --git a/x-pack/plugins/fleet/server/config.ts b/x-pack/plugins/fleet/server/config.ts index 48641d49f6e51..0e685e8b45135 100644 --- a/x-pack/plugins/fleet/server/config.ts +++ b/x-pack/plugins/fleet/server/config.ts @@ -21,6 +21,7 @@ import { PreconfiguredPackagesSchema, PreconfiguredAgentPoliciesSchema, PreconfiguredOutputsSchema, + PreconfiguredFleetServerHostsSchema, } from './types'; const DEFAULT_BUNDLED_PACKAGE_LOCATION = path.join(__dirname, '../target/bundled_packages'); @@ -115,6 +116,7 @@ export const config: PluginConfigDescriptor = { packages: PreconfiguredPackagesSchema, agentPolicies: PreconfiguredAgentPoliciesSchema, outputs: PreconfiguredOutputsSchema, + fleetServerHosts: PreconfiguredFleetServerHostsSchema, agentIdVerificationEnabled: schema.boolean({ defaultValue: true }), developer: schema.object({ disableRegistryVersionCheck: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index cd0c0262a77d2..33004fe3030cf 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -64,6 +64,9 @@ export { DEFAULT_DOWNLOAD_SOURCE_URI, DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, DEFAULT_DOWNLOAD_SOURCE_ID, + // Fleet server host + DEFAULT_FLEET_SERVER_HOST_ID, + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, // Authz ENDPOINT_PRIVILEGES, } from '../../common/constants'; diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index 1b8a627a35722..d899f96886bc6 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -76,6 +76,8 @@ export class OutputInvalidError extends FleetError {} export class OutputLicenceError extends FleetError {} export class DownloadSourceError extends FleetError {} +export class FleetServerHostUnauthorizedError extends FleetError {} + export class ArtifactsClientError extends FleetError {} export class ArtifactsClientAccessDeniedError extends FleetError { constructor(deniedPackageName: string, allowedPackageName: string) { diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index bbd55a517753a..9e02ad3f96407 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -64,6 +64,7 @@ import { ASSETS_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, } from './constants'; import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects'; import { @@ -80,6 +81,7 @@ import { registerPreconfigurationRoutes, registerDownloadSourcesRoutes, registerHealthCheckRoutes, + registerFleetServerHostRoutes, } from './routes'; import type { ExternalCallback, FleetRequestHandlerContext } from './types'; @@ -161,6 +163,7 @@ const allSavedObjectTypes = [ ASSETS_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, ]; /** @@ -399,6 +402,7 @@ export class FleetPlugin registerSettingsRoutes(fleetAuthzRouter); registerDataStreamRoutes(fleetAuthzRouter); registerPreconfigurationRoutes(fleetAuthzRouter); + registerFleetServerHostRoutes(fleetAuthzRouter); registerDownloadSourcesRoutes(fleetAuthzRouter); registerHealthCheckRoutes(fleetAuthzRouter); diff --git a/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/handler.ts b/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/handler.ts new file mode 100644 index 0000000000000..3df4daf126119 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/handler.ts @@ -0,0 +1,143 @@ +/* + * 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 type { TypeOf } from '@kbn/config-schema'; +import type { RequestHandler } from '@kbn/core/server'; +import { SavedObjectsErrorHelpers } from '@kbn/core/server'; + +import { defaultFleetErrorHandler } from '../../errors'; +import { agentPolicyService } from '../../services'; +import { + createFleetServerHost, + deleteFleetServerHost, + getFleetServerHost, + listFleetServerHosts, + updateFleetServerHost, +} from '../../services/fleet_server_host'; +import type { + GetOneFleetServerHostRequestSchema, + PostFleetServerHostRequestSchema, + PutFleetServerHostRequestSchema, +} from '../../types'; + +export const postFleetServerHost: RequestHandler< + undefined, + undefined, + TypeOf +> = async (context, request, response) => { + const coreContext = await context.core; + const soClient = coreContext.savedObjects.client; + const esClient = coreContext.elasticsearch.client.asInternalUser; + try { + const { id, ...data } = request.body; + const FleetServerHost = await createFleetServerHost( + soClient, + { ...data, is_preconfigured: false }, + { id } + ); + if (FleetServerHost.is_default) { + await agentPolicyService.bumpAllAgentPolicies(soClient, esClient); + } + + const body = { + item: FleetServerHost, + }; + + return response.ok({ body }); + } catch (error) { + return defaultFleetErrorHandler({ error, response }); + } +}; + +export const getFleetServerPolicyHandler: RequestHandler< + TypeOf +> = async (context, request, response) => { + const soClient = (await context.core).savedObjects.client; + try { + const item = await getFleetServerHost(soClient, request.params.itemId); + const body = { + item, + }; + + return response.ok({ body }); + } catch (error) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + return response.notFound({ + body: { message: `Fleet server ${request.params.itemId} not found` }, + }); + } + + return defaultFleetErrorHandler({ error, response }); + } +}; + +export const deleteFleetServerPolicyHandler: RequestHandler< + TypeOf +> = async (context, request, response) => { + const soClient = (await context.core).savedObjects.client; + try { + await deleteFleetServerHost(soClient, request.params.itemId); + const body = { + id: request.params.itemId, + }; + + return response.ok({ body }); + } catch (error) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + return response.notFound({ + body: { message: `Fleet server ${request.params.itemId} not found` }, + }); + } + + return defaultFleetErrorHandler({ error, response }); + } +}; + +export const putFleetServerPolicyHandler: RequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = (await context.core).savedObjects.client; + try { + const item = await updateFleetServerHost(soClient, request.params.itemId, request.body); + const body = { + item, + }; + + return response.ok({ body }); + } catch (error) { + if (SavedObjectsErrorHelpers.isNotFoundError(error)) { + return response.notFound({ + body: { message: `Fleet server ${request.params.itemId} not found` }, + }); + } + + return defaultFleetErrorHandler({ error, response }); + } +}; + +export const getAllFleetServerPolicyHandler: RequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { + const soClient = (await context.core).savedObjects.client; + try { + const res = await listFleetServerHosts(soClient); + const body = { + items: res.items, + page: res.page, + perPage: res.perPage, + total: res.total, + }; + + return response.ok({ body }); + } catch (error) { + return defaultFleetErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/index.ts b/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/index.ts new file mode 100644 index 0000000000000..48607c5df7a72 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/fleet_server_policy_config/index.ts @@ -0,0 +1,77 @@ +/* + * 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 { FLEET_SERVER_HOST_API_ROUTES } from '../../../common/constants'; +import { + GetAllFleetServerHostRequestSchema, + GetOneFleetServerHostRequestSchema, + PostFleetServerHostRequestSchema, + PutFleetServerHostRequestSchema, +} from '../../types'; + +import type { FleetAuthzRouter } from '../security'; + +import { + deleteFleetServerPolicyHandler, + getAllFleetServerPolicyHandler, + getFleetServerPolicyHandler, + postFleetServerHost, + putFleetServerPolicyHandler, +} from './handler'; + +export const registerRoutes = (router: FleetAuthzRouter) => { + router.get( + { + path: FLEET_SERVER_HOST_API_ROUTES.LIST_PATTERN, + validate: GetAllFleetServerHostRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + getAllFleetServerPolicyHandler + ); + router.post( + { + path: FLEET_SERVER_HOST_API_ROUTES.CREATE_PATTERN, + validate: PostFleetServerHostRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + postFleetServerHost + ); + router.get( + { + path: FLEET_SERVER_HOST_API_ROUTES.INFO_PATTERN, + validate: GetOneFleetServerHostRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + getFleetServerPolicyHandler + ); + router.delete( + { + path: FLEET_SERVER_HOST_API_ROUTES.DELETE_PATTERN, + validate: GetOneFleetServerHostRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + deleteFleetServerPolicyHandler + ); + router.put( + { + path: FLEET_SERVER_HOST_API_ROUTES.UPDATE_PATTERN, + validate: PutFleetServerHostRequestSchema, + fleetAuthz: { + fleet: { all: true }, + }, + }, + putFleetServerPolicyHandler + ); +}; diff --git a/x-pack/plugins/fleet/server/routes/index.ts b/x-pack/plugins/fleet/server/routes/index.ts index 24c0947a419f6..7dd720f5d267e 100644 --- a/x-pack/plugins/fleet/server/routes/index.ts +++ b/x-pack/plugins/fleet/server/routes/index.ts @@ -18,3 +18,4 @@ export { registerRoutes as registerAppRoutes } from './app'; export { registerRoutes as registerPreconfigurationRoutes } from './preconfiguration'; export { registerRoutes as registerDownloadSourcesRoutes } from './download_source'; export { registerRoutes as registerHealthCheckRoutes } from './health_check'; +export { registerRoutes as registerFleetServerHostRoutes } from './fleet_server_policy_config'; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index ed03e26b64537..4a943789cac68 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -18,6 +18,7 @@ import { GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE, + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, } from '../constants'; import { @@ -344,6 +345,22 @@ const getSavedObjectTypes = ( }, }, }, + [FLEET_SERVER_HOST_SAVED_OBJECT_TYPE]: { + name: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: false, + }, + mappings: { + properties: { + name: { type: 'keyword' }, + is_default: { type: 'boolean' }, + host_urls: { type: 'keyword', index: false }, + is_preconfigured: { type: 'boolean' }, + }, + }, + }, }); export function registerSavedObjects( diff --git a/x-pack/plugins/fleet/server/services/fleet_server_host.test.ts b/x-pack/plugins/fleet/server/services/fleet_server_host.test.ts new file mode 100644 index 0000000000000..72602df6af3db --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server_host.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { savedObjectsClientMock } from '@kbn/core/server/mocks'; + +import { + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + DEFAULT_FLEET_SERVER_HOST_ID, +} from '../constants'; + +import { appContextService } from './app_context'; +import { migrateSettingsToFleetServerHost } from './fleet_server_host'; +import { getCloudFleetServersHosts } from './settings'; + +jest.mock('./app_context'); + +const mockedAppContextService = appContextService as jest.Mocked; + +describe('getCloudFleetServersHosts', () => { + afterEach(() => { + mockedAppContextService.getCloud.mockReset(); + }); + it('should return undefined if cloud is not setup', () => { + expect(getCloudFleetServersHosts()).toBeUndefined(); + }); + + it('should return fleet server hosts if cloud is correctly setup with default port == 443', () => { + mockedAppContextService.getCloud.mockReturnValue({ + cloudId: + 'dXMtZWFzdC0xLmF3cy5mb3VuZC5pbyRjZWM2ZjI2MWE3NGJmMjRjZTMzYmI4ODExYjg0Mjk0ZiRjNmMyY2E2ZDA0MjI0OWFmMGNjN2Q3YTllOTYyNTc0Mw==', + isCloudEnabled: true, + deploymentId: 'deployment-id-1', + apm: {}, + }); + + expect(getCloudFleetServersHosts()).toMatchInlineSnapshot(` + Array [ + "https://deployment-id-1.fleet.us-east-1.aws.found.io", + ] + `); + }); + + it('should return fleet server hosts if cloud is correctly setup with a default port', () => { + mockedAppContextService.getCloud.mockReturnValue({ + cloudId: + 'test:dGVzdC5mcjo5MjQzJGRhM2I2YjNkYWY5ZDRjODE4ZjI4ZmEzNDdjMzgzODViJDgxMmY4NWMxZjNjZTQ2YTliYjgxZjFjMWIxMzRjNmRl', + isCloudEnabled: true, + deploymentId: 'deployment-id-1', + apm: {}, + }); + + expect(getCloudFleetServersHosts()).toMatchInlineSnapshot(` + Array [ + "https://deployment-id-1.fleet.test.fr:9243", + ] + `); + }); +}); + +describe('migrateSettingsToFleetServerHost', () => { + it('should not migrate settings if a default fleet server policy config exists', async () => { + const soClient = savedObjectsClientMock.create(); + soClient.find.mockImplementation(({ type }) => { + if (type === FLEET_SERVER_HOST_SAVED_OBJECT_TYPE) { + return { saved_objects: [{ id: 'test123' }] } as any; + } + + throw new Error('Not mocked'); + }); + + await migrateSettingsToFleetServerHost(soClient); + + expect(soClient.create).not.toBeCalled(); + }); + + it('should not migrate settings if there is not old settings', async () => { + const soClient = savedObjectsClientMock.create(); + soClient.find.mockImplementation(({ type }) => { + if (type === FLEET_SERVER_HOST_SAVED_OBJECT_TYPE) { + return { saved_objects: [] } as any; + } + + if (type === GLOBAL_SETTINGS_SAVED_OBJECT_TYPE) { + return { + saved_objects: [], + } as any; + } + + throw new Error('Not mocked'); + }); + + soClient.create.mockResolvedValue({ + id: DEFAULT_FLEET_SERVER_HOST_ID, + attributes: {}, + } as any); + + await migrateSettingsToFleetServerHost(soClient); + expect(soClient.create).not.toBeCalled(); + }); + + it('should migrate settings to new saved object', async () => { + const soClient = savedObjectsClientMock.create(); + soClient.find.mockImplementation(({ type }) => { + if (type === FLEET_SERVER_HOST_SAVED_OBJECT_TYPE) { + return { saved_objects: [] } as any; + } + + if (type === GLOBAL_SETTINGS_SAVED_OBJECT_TYPE) { + return { + saved_objects: [ + { + attributes: { + fleet_server_hosts: ['https://fleetserver:8220'], + }, + }, + ], + } as any; + } + + throw new Error('Not mocked'); + }); + + soClient.create.mockResolvedValue({ + id: DEFAULT_FLEET_SERVER_HOST_ID, + attributes: {}, + } as any); + + await migrateSettingsToFleetServerHost(soClient); + expect(soClient.create).toBeCalledWith( + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + expect.objectContaining({ + is_default: true, + host_urls: ['https://fleetserver:8220'], + }), + expect.objectContaining({ + id: DEFAULT_FLEET_SERVER_HOST_ID, + }) + ); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/fleet_server_host.ts b/x-pack/plugins/fleet/server/services/fleet_server_host.ts new file mode 100644 index 0000000000000..a3ade854770c3 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/fleet_server_host.ts @@ -0,0 +1,237 @@ +/* + * 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 type { SavedObjectsClientContract } from '@kbn/core/server'; + +import { + GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + DEFAULT_FLEET_SERVER_HOST_ID, + SO_SEARCH_LIMIT, +} from '../constants'; + +import type { + SettingsSOAttributes, + FleetServerHostSOAttributes, + FleetServerHost, + NewFleetServerHost, +} from '../types'; +import { FleetServerHostUnauthorizedError } from '../errors'; + +export async function createFleetServerHost( + soClient: SavedObjectsClientContract, + data: NewFleetServerHost, + options?: { id?: string; overwrite?: boolean; fromPreconfiguration?: boolean } +): Promise { + if (data.is_default) { + const defaultItem = await getDefaultFleetServerHost(soClient); + if (defaultItem) { + await updateFleetServerHost( + soClient, + defaultItem.id, + { is_default: false }, + { fromPreconfiguration: options?.fromPreconfiguration } + ); + } + } + + const res = await soClient.create( + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + data, + { id: options?.id, overwrite: options?.overwrite } + ); + + return { + id: res.id, + ...res.attributes, + }; +} + +export async function getFleetServerHost( + soClient: SavedObjectsClientContract, + id: string +): Promise { + const res = await soClient.get( + FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + id + ); + + return { + id: res.id, + ...res.attributes, + }; +} + +export async function listFleetServerHosts(soClient: SavedObjectsClientContract) { + const res = await soClient.find({ + type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + perPage: SO_SEARCH_LIMIT, + }); + + return { + items: res.saved_objects.map((so) => ({ + id: so.id, + ...so.attributes, + })), + total: res.total, + page: res.page, + perPage: res.per_page, + }; +} + +export async function deleteFleetServerHost( + soClient: SavedObjectsClientContract, + id: string, + options?: { fromPreconfiguration?: boolean } +) { + const fleetServerHost = await getFleetServerHost(soClient, id); + + if (fleetServerHost.is_preconfigured && !options?.fromPreconfiguration) { + throw new FleetServerHostUnauthorizedError( + `Cannot delete ${id} preconfigured fleet server host` + ); + } + + if (fleetServerHost.is_default) { + throw new FleetServerHostUnauthorizedError( + `Default Fleet Server hosts ${id} cannot be deleted.` + ); + } + + return await soClient.delete(FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, id); +} + +export async function updateFleetServerHost( + soClient: SavedObjectsClientContract, + id: string, + data: Partial, + options?: { fromPreconfiguration?: boolean } +) { + const originalItem = await getFleetServerHost(soClient, id); + + if (data.is_preconfigured && !options?.fromPreconfiguration) { + throw new FleetServerHostUnauthorizedError( + `Cannot update ${id} preconfigured fleet server host` + ); + } + + if (data.is_default) { + const defaultItem = await getDefaultFleetServerHost(soClient); + if (defaultItem && defaultItem.id !== id) { + await updateFleetServerHost( + soClient, + defaultItem.id, + { + is_default: false, + }, + { fromPreconfiguration: options?.fromPreconfiguration } + ); + } + } + + await soClient.update(FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, id, data); + + return { + ...originalItem, + ...data, + }; +} + +export async function bulkGetFleetServerHosts( + soClient: SavedObjectsClientContract, + ids: string[], + { ignoreNotFound = false } = { ignoreNotFound: true } +) { + if (ids.length === 0) { + return []; + } + + const res = await soClient.bulkGet( + ids.map((id) => ({ + id, + type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + })) + ); + + return res.saved_objects + .map((so) => { + if (so.error) { + if (!ignoreNotFound || so.error.statusCode !== 404) { + throw so.error; + } + return undefined; + } + + return { + id: so.id, + ...so.attributes, + }; + }) + .filter( + (fleetServerHostOrUndefined): fleetServerHostOrUndefined is FleetServerHost => + typeof fleetServerHostOrUndefined !== 'undefined' + ); +} + +/** + * Get the default Fleet server policy hosts or throw if it does not exists + */ +export async function getDefaultFleetServerHost( + soClient: SavedObjectsClientContract +): Promise { + const res = await soClient.find({ + type: FLEET_SERVER_HOST_SAVED_OBJECT_TYPE, + filter: `${FLEET_SERVER_HOST_SAVED_OBJECT_TYPE}.attributes.is_default:true`, + }); + + if (res.saved_objects.length === 0) { + return null; + } + + return { + id: res.saved_objects[0].id, + ...res.saved_objects[0].attributes, + }; +} + +/** + * Migrate Global setting fleet server hosts to their own saved object + */ +export async function migrateSettingsToFleetServerHost(soClient: SavedObjectsClientContract) { + const defaultFleetServerHost = await getDefaultFleetServerHost(soClient); + if (defaultFleetServerHost) { + return; + } + + const res = await soClient.find({ + type: GLOBAL_SETTINGS_SAVED_OBJECT_TYPE, + }); + + const oldSettings = res.saved_objects[0]; + if ( + !oldSettings || + !oldSettings.attributes.fleet_server_hosts || + oldSettings.attributes.fleet_server_hosts.length === 0 + ) { + return; + } + + // Migrate + await createFleetServerHost( + soClient, + { + name: 'Default', + host_urls: oldSettings.attributes.fleet_server_hosts, + is_default: true, + is_preconfigured: false, + }, + { + id: DEFAULT_FLEET_SERVER_HOST_ID, + overwrite: true, + } + ); +} diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts index a23a049bdc762..aafbb383cbb0d 100644 --- a/x-pack/plugins/fleet/server/services/index.ts +++ b/x-pack/plugins/fleet/server/services/index.ts @@ -59,3 +59,6 @@ export { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration'; // Package Services export { PackageServiceImpl } from './epm'; export type { PackageService, PackageClient } from './epm'; + +// Fleet server policy config +export { migrateSettingsToFleetServerHost } from './fleet_server_host'; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.test.ts new file mode 100644 index 0000000000000..468058f87f448 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { getPreconfiguredFleetServerHostFromConfig } from './fleet_server_host'; + +jest.mock('../fleet_server_host'); + +describe('getPreconfiguredFleetServerHostFromConfig', () => { + it('should work with preconfigured fleetServerHosts', () => { + const config = { + fleetServerHosts: [ + { + id: 'fleet-123', + name: 'TEST', + is_default: true, + host_urls: ['http://test.fr'], + }, + ], + }; + + const res = getPreconfiguredFleetServerHostFromConfig(config); + + expect(res).toEqual(config.fleetServerHosts); + }); + + it('should work with agents.fleet_server.hosts', () => { + const config = { + agents: { fleet_server: { hosts: ['http://test.fr'] } }, + }; + + const res = getPreconfiguredFleetServerHostFromConfig(config); + + expect(res).toEqual([ + { + id: 'fleet-default-fleet-server-host', + name: 'Default', + host_urls: ['http://test.fr'], + is_default: true, + }, + ]); + }); + + it('should work with agents.fleet_server.hosts and preconfigured outputs', () => { + const config = { + agents: { fleet_server: { hosts: ['http://test.fr'] } }, + fleetServerHosts: [ + { + id: 'fleet-123', + name: 'TEST', + is_default: false, + host_urls: ['http://test.fr'], + }, + ], + }; + + const res = getPreconfiguredFleetServerHostFromConfig(config); + + expect(res).toHaveLength(2); + expect(res.map(({ id }) => id)).toEqual(['fleet-123', 'fleet-default-fleet-server-host']); + }); + + it('should throw if there is multiple default outputs', () => { + const config = { + agents: { fleet_server: { hosts: ['http://test.fr'] } }, + fleetServerHosts: [ + { + id: 'fleet-123', + name: 'TEST', + is_default: true, + host_urls: ['http://test.fr'], + }, + ], + }; + + expect(() => getPreconfiguredFleetServerHostFromConfig(config)).toThrowError( + /Only one default Fleet Server host is allowed/ + ); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.ts b/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.ts new file mode 100644 index 0000000000000..465a2f8706ea9 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/preconfiguration/fleet_server_host.ts @@ -0,0 +1,145 @@ +/* + * 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 type { SavedObjectsClientContract } from '@kbn/core/server'; +import { isEqual } from 'lodash'; + +import type { FleetConfigType } from '../../config'; +import { DEFAULT_FLEET_SERVER_HOST_ID } from '../../constants'; + +import type { FleetServerHost } from '../../types'; +import { + bulkGetFleetServerHosts, + createFleetServerHost, + deleteFleetServerHost, + listFleetServerHosts, + updateFleetServerHost, +} from '../fleet_server_host'; + +export function getPreconfiguredFleetServerHostFromConfig(config?: FleetConfigType) { + const { fleetServerHosts: fleetServerHostsFromConfig } = config; + + const legacyFleetServerHostsConfig = getConfigFleetServerHosts(config); + + const fleetServerHosts: FleetServerHost[] = (fleetServerHostsFromConfig || []).concat([ + ...(legacyFleetServerHostsConfig + ? [ + { + name: 'Default', + is_default: true, + id: DEFAULT_FLEET_SERVER_HOST_ID, + host_urls: legacyFleetServerHostsConfig, + }, + ] + : []), + ]); + + if (fleetServerHosts.filter((fleetServerHost) => fleetServerHost.is_default).length > 1) { + throw new Error('Only one default Fleet Server host is allowed'); + } + + return fleetServerHosts; +} + +export async function ensurePreconfiguredFleetServerHosts( + soClient: SavedObjectsClientContract, + preconfiguredFleetServerHosts: FleetServerHost[] +) { + await createOrUpdatePreconfiguredFleetServerHosts(soClient, preconfiguredFleetServerHosts); + await cleanPreconfiguredFleetServerHosts(soClient, preconfiguredFleetServerHosts); +} + +export async function createOrUpdatePreconfiguredFleetServerHosts( + soClient: SavedObjectsClientContract, + preconfiguredFleetServerHosts: FleetServerHost[] +) { + const existingFleetServerHosts = await bulkGetFleetServerHosts( + soClient, + preconfiguredFleetServerHosts.map(({ id }) => id), + { ignoreNotFound: true } + ); + + await Promise.all( + preconfiguredFleetServerHosts.map(async (preconfiguredFleetServerHost) => { + const existingHost = existingFleetServerHosts.find( + (fleetServerHost) => fleetServerHost.id === preconfiguredFleetServerHost.id + ); + + const { id, ...data } = preconfiguredFleetServerHost; + + const isCreate = !existingHost; + const isUpdateWithNewData = + existingHost && + (!existingHost.is_preconfigured || + existingHost.is_default !== preconfiguredFleetServerHost.is_default || + existingHost.name !== preconfiguredFleetServerHost.name || + !isEqual(existingHost?.host_urls, preconfiguredFleetServerHost.host_urls)); + + if (isCreate) { + await createFleetServerHost( + soClient, + { + ...data, + is_preconfigured: true, + }, + { id, overwrite: true, fromPreconfiguration: true } + ); + } else if (isUpdateWithNewData) { + await updateFleetServerHost( + soClient, + id, + { + ...data, + is_preconfigured: true, + }, + { fromPreconfiguration: true } + ); + // TODO Bump revision of all policies using that output + } + }) + ); +} + +export async function cleanPreconfiguredFleetServerHosts( + soClient: SavedObjectsClientContract, + preconfiguredFleetServerHosts: FleetServerHost[] +) { + const existingFleetServerHosts = await listFleetServerHosts(soClient); + const existingPreconfiguredHosts = existingFleetServerHosts.items.filter( + (o) => o.is_preconfigured === true + ); + + for (const existingFleetServerHost of existingPreconfiguredHosts) { + const hasBeenDelete = !preconfiguredFleetServerHosts.find( + ({ id }) => existingFleetServerHost.id === id + ); + if (!hasBeenDelete) { + continue; + } + + if (existingFleetServerHost.is_default) { + await updateFleetServerHost( + soClient, + existingFleetServerHost.id, + { is_preconfigured: false }, + { + fromPreconfiguration: true, + } + ); + } else { + await deleteFleetServerHost(soClient, existingFleetServerHost.id, { + fromPreconfiguration: true, + }); + } + } +} + +function getConfigFleetServerHosts(config?: FleetConfigType) { + return config?.agents?.fleet_server?.hosts && config.agents.fleet_server.hosts.length > 0 + ? config?.agents?.fleet_server?.hosts + : undefined; +} diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts index eb4a9313e169f..37f368a4b8647 100644 --- a/x-pack/plugins/fleet/server/services/setup.ts +++ b/x-pack/plugins/fleet/server/services/setup.ts @@ -44,6 +44,11 @@ import { upgradeManagedPackagePolicies } from './managed_package_policies'; import { getBundledPackages } from './epm/packages'; import { upgradePackageInstallVersion } from './setup/upgrade_package_install_version'; import { upgradeAgentPolicySchemaVersion } from './setup/upgrade_agent_policy_schema_version'; +import { migrateSettingsToFleetServerHost } from './fleet_server_host'; +import { + ensurePreconfiguredFleetServerHosts, + getPreconfiguredFleetServerHostFromConfig, +} from './preconfiguration/fleet_server_host'; export interface SetupStatus { isInitialized: boolean; @@ -70,25 +75,32 @@ async function createSetupSideEffects( const { agentPolicies: policiesOrUndefined, packages: packagesOrUndefined } = appContextService.getConfig() ?? {}; - const policies = policiesOrUndefined ?? []; let packages = packagesOrUndefined ?? []; + logger.debug('Setting Fleet server config'); + await migrateSettingsToFleetServerHost(soClient); + logger.debug('Setting up Fleet download source'); + const defaultDownloadSource = await downloadSourceService.ensureDefault(soClient); + logger.debug('Setting up Fleet outputs'); + await ensurePreconfiguredFleetServerHosts( + soClient, + getPreconfiguredFleetServerHostFromConfig(appContextService.getConfig()) + ); await Promise.all([ ensurePreconfiguredOutputs( soClient, esClient, getPreconfiguredOutputFromConfig(appContextService.getConfig()) ), + settingsService.settingsSetup(soClient), ]); const defaultOutput = await outputService.ensureDefaultOutput(soClient); - const defaultDownloadSource = await downloadSourceService.ensureDefault(soClient); - if (appContextService.getConfig()?.agentIdVerificationEnabled) { logger.debug('Setting up Fleet Elasticsearch assets'); await ensureFleetGlobalEsAssets(soClient, esClient); diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index fd0cee334cc50..ea662f51864fb 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -36,6 +36,9 @@ export type { OutputType, EnrollmentAPIKey, EnrollmentAPIKeySOAttributes, + NewFleetServerHost, + FleetServerHost, + FleetServerHostSOAttributes, Installation, EpmPackageInstallStatus, InstallationStatus, diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts index ad4f1958a1a21..3100fb04a46f1 100644 --- a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts @@ -85,6 +85,16 @@ export const PreconfiguredOutputsSchema = schema.arrayOf( } ); +export const PreconfiguredFleetServerHostsSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + name: schema.string(), + is_default: schema.boolean({ defaultValue: false }), + host_urls: schema.arrayOf(schema.string(), { minSize: 1 }), + }), + { defaultValue: [] } +); + export const PreconfiguredAgentPoliciesSchema = schema.arrayOf( schema.object({ ...AgentPolicyBaseSchema, 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 new file mode 100644 index 0000000000000..b011d9065fd42 --- /dev/null +++ b/x-pack/plugins/fleet/server/types/rest_spec/fleet_server_policy_config.ts @@ -0,0 +1,32 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +export const PostFleetServerHostRequestSchema = { + body: schema.object({ + id: schema.maybe(schema.string()), + name: schema.string(), + host_urls: schema.arrayOf(schema.string(), { minSize: 1 }), + is_default: schema.boolean({ defaultValue: false }), + }), +}; + +export const GetOneFleetServerHostRequestSchema = { + params: schema.object({ itemId: schema.string() }), +}; + +export const PutFleetServerHostRequestSchema = { + params: schema.object({ itemId: schema.string() }), + body: schema.object({ + name: schema.maybe(schema.string()), + host_urls: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })), + is_default: schema.maybe(schema.boolean({ defaultValue: false })), + }), +}; + +export const GetAllFleetServerHostRequestSchema = {}; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/index.ts b/x-pack/plugins/fleet/server/types/rest_spec/index.ts index 98f14cb0c879d..ffe8514073c3d 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/index.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/index.ts @@ -11,6 +11,7 @@ export * from './agent'; export * from './package_policy'; export * from './epm'; export * from './enrollment_api_key'; +export * from './fleet_server_policy_config'; export * from './output'; export * from './preconfiguration'; export * from './settings'; diff --git a/x-pack/test/fleet_api_integration/apis/fleet_server_hosts/crud.ts b/x-pack/test/fleet_api_integration/apis/fleet_server_hosts/crud.ts new file mode 100644 index 0000000000000..21bc912dbad29 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fleet_server_hosts/crud.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { setupFleetAndAgents } from '../agents/services'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + describe('fleet_fleet_server_hosts_crud', async function () { + skipIfNoDockerRegistry(providerContext); + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + await kibanaServer.savedObjects.cleanStandardList(); + }); + setupFleetAndAgents(providerContext); + + let defaultFleetServerHostId: string; + + before(async function () { + await kibanaServer.savedObjects.clean({ + types: ['fleet-fleet-server-host'], + }); + const { body: defaultRes } = await supertest + .post(`/api/fleet/fleet_server_hosts`) + .set('kbn-xsrf', 'xxxx') + .send({ + id: 'test-default-123', + name: 'Default', + is_default: true, + host_urls: ['https://test.fr:8080', 'https://test.fr:8081'], + }) + .expect(200); + + await supertest + .post(`/api/fleet/fleet_server_hosts`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test', + host_urls: ['https://test.fr:8080', 'https://test.fr:8081'], + }) + .expect(200); + + defaultFleetServerHostId = defaultRes.item.id; + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); + }); + + describe('GET /fleet_server_hosts', () => { + it('should list the fleet server hosts', async () => { + const { body: res } = await supertest.get(`/api/fleet/fleet_server_hosts`).expect(200); + + expect(res.items.length).to.be(2); + }); + }); + + describe('GET /fleet_server_hosts/{itemId}', () => { + it('should return the requested fleet server host', async () => { + const { body: fleetServerHost } = await supertest + .get(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`) + .expect(200); + + expect(fleetServerHost).to.eql({ + item: { + id: 'test-default-123', + name: 'Default', + is_default: true, + host_urls: ['https://test.fr:8080', 'https://test.fr:8081'], + is_preconfigured: false, + }, + }); + }); + + it('should return a 404 when retrieving a non existing fleet server host', async function () { + await supertest.get(`/api/fleet/fleet_server_hosts/idonotexists`).expect(404); + }); + }); + + describe('PUT /fleet_server_hosts/{itemId}', () => { + it('should allow to update an existing fleet server host', async function () { + await supertest + .put(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Default updated', + }) + .expect(200); + + const { + body: { item: fleetServerHost }, + } = await supertest + .get(`/api/fleet/fleet_server_hosts/${defaultFleetServerHostId}`) + .expect(200); + + expect(fleetServerHost.name).to.eql('Default updated'); + }); + + it('should return a 404 when updating a non existing fleet server host', async function () { + await supertest + .put(`/api/fleet/fleet_server_hosts/idonotexists`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'new host1', + }) + .expect(404); + }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js index a2be789b9a982..bcb728432ef46 100644 --- a/x-pack/test/fleet_api_integration/apis/index.js +++ b/x-pack/test/fleet_api_integration/apis/index.js @@ -58,5 +58,8 @@ export default function ({ loadTestFile, getService }) { // Integrations loadTestFile(require.resolve('./integrations')); + + // Fleet server hosts + loadTestFile(require.resolve('./fleet_server_hosts/crud')); }); }