[Fleet] Add crud UI for fleet proxy (#145017)

This commit is contained in:
Nicolas Chaulet 2022-11-17 11:13:40 -05:00 committed by GitHub
parent 59f58e1cbb
commit bca45a1245
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 2259 additions and 131 deletions

View file

@ -88,8 +88,9 @@ describe('checking migration metadata changes on all registered SO types', () =>
"file": "05c14a75e5e20b12ca514a1d7de231f420facf2c",
"file-upload-usage-collection-telemetry": "8478924cf0057bd90df737155b364f98d05420a5",
"fileShare": "3f88784b041bb8728a7f40763a08981828799a75",
"fleet-fleet-server-host": "f00ca963f1bee868806319789cdc33f1f53a97e2",
"fleet-fleet-server-host": "643d15dbf56edb96f7ca65f98409d83ac5792fb6",
"fleet-preconfiguration-deletion-record": "7b28f200513c28ae774f1b7d7d7906954e3c6e16",
"fleet-proxy": "2bbcd9e6d5e30ac07b275c8d489af07a0d550df5",
"graph-workspace": "3342f2cd561afdde8f42f5fb284bf550dee8ebb5",
"guided-onboarding-guide-state": "561db8d481b131a2bbf46b1e534d6ce960255135",
"guided-onboarding-plugin-state": "a802ed58e9d0076b9632c59d7943861ba476f99c",
@ -98,7 +99,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"infrastructure-ui-source": "7c8dbbc0a608911f1b683a944f4a65383f6153ed",
"ingest-agent-policies": "9170cdad95d887c036b87adf0ff38a3f12800c05",
"ingest-download-sources": "1e69dabd6db5e320fe08c5bda8f35f29bafc6b54",
"ingest-outputs": "29b867bf7bfd28b1e17c84697dce5c6d078f9705",
"ingest-outputs": "4888b16d55a452bf5fff2bb407e0361567eae63a",
"ingest-package-policies": "e8707a8c7821ea085e67c2d213e24efa56307393",
"ingest_manager_settings": "6f36714825cc15ea8d7cda06fde7851611a532b4",
"inventory-view": "bc2bd1e7ec7c186159447ab228d269f22bd39056",

View file

@ -59,6 +59,7 @@ const previouslyRegisteredTypes = [
'fleet-enrollment-api-keys',
'fleet-preconfiguration-deletion-record',
'fleet-fleet-server-host',
'fleet-proxy',
'graph-workspace',
'guided-setup-state',
'guided-onboarding-guide-state',

View file

@ -8,3 +8,5 @@
export const FLEET_SERVER_HOST_SAVED_OBJECT_TYPE = 'fleet-fleet-server-host';
export const DEFAULT_FLEET_SERVER_HOST_ID = 'fleet-default-fleet-server-host';
export const FLEET_PROXY_SAVED_OBJECT_TYPE = 'fleet-proxy';

View file

@ -97,6 +97,14 @@ export const FLEET_SERVER_HOST_API_ROUTES = {
DELETE_PATTERN: `${API_ROOT}/fleet_server_hosts/{itemId}`,
};
export const FLEET_PROXY_API_ROUTES = {
LIST_PATTERN: `${API_ROOT}/proxies`,
CREATE_PATTERN: `${API_ROOT}/proxies`,
INFO_PATTERN: `${API_ROOT}/proxies/{itemId}`,
UPDATE_PATTERN: `${API_ROOT}/proxies/{itemId}`,
DELETE_PATTERN: `${API_ROOT}/proxies/{itemId}`,
};
// Settings API routes
export const SETTINGS_API_ROUTES = {
INFO_PATTERN: `${API_ROOT}/settings`,

View file

@ -4094,6 +4094,242 @@
}
]
}
},
"/proxies": {
"get": {
"summary": "Fleet Proxies - List",
"description": "Return a list of Proxies",
"tags": [],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/proxies"
}
},
"total": {
"type": "integer"
},
"page": {
"type": "integer"
},
"perPage": {
"type": "integer"
}
}
}
}
}
}
},
"operationId": "get-fleet-proxies"
},
"post": {
"summary": "Fleet Proxies - Create",
"description": "Create a new Fleet Server Host",
"tags": [],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"item": {
"$ref": "#/components/schemas/proxies"
}
}
}
}
}
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"url": {
"type": "string"
},
"proxy_headers": {
"type": "object"
},
"certificate_authorities": {
"type": "string"
},
"certificate": {
"type": "string"
},
"certificate_key": {
"type": "string"
}
},
"required": [
"name",
"url"
]
}
}
}
},
"operationId": "post-fleet-proxies"
}
},
"/proxies/{itemId}": {
"get": {
"summary": "Fleet Proxies - Info",
"tags": [],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"item": {
"$ref": "#/components/schemas/proxies"
}
},
"required": [
"item"
]
}
}
}
}
},
"operationId": "get-one-fleet-proxies"
},
"parameters": [
{
"schema": {
"type": "string"
},
"name": "itemId",
"in": "path",
"required": true
}
],
"delete": {
"summary": "Fleet Proxies - Delete",
"operationId": "delete-fleet-proxies",
"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 Proxies - Update",
"operationId": "update-fleet-proxies",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"url": {
"type": "string"
},
"proxy_headers": {
"type": "object"
},
"certificate_authorities": {
"type": "string"
},
"certificate": {
"type": "string"
},
"certificate_key": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"item": {
"$ref": "#/components/schemas/proxies"
}
},
"required": [
"item"
]
}
}
}
}
},
"parameters": [
{
"schema": {
"type": "string"
},
"name": "itemId",
"in": "path",
"required": true
},
{
"$ref": "#/components/parameters/kbn_xsrf"
}
]
}
}
},
"components": {
@ -5910,6 +6146,37 @@
"is_preconfigured",
"host_urls"
]
},
"proxies": {
"title": "Fleet Proxy",
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"url": {
"type": "string"
},
"proxy_headers": {
"type": "object"
},
"certificate_authorities": {
"type": "string"
},
"certificate": {
"type": "string"
},
"certificate_key": {
"type": "string"
}
},
"required": [
"name",
"url"
]
}
}
},

View file

@ -2533,6 +2533,153 @@ paths:
in: path
required: true
- $ref: '#/components/parameters/kbn_xsrf'
/proxies:
get:
summary: Fleet Proxies - List
description: Return a list of Proxies
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/proxies'
total:
type: integer
page:
type: integer
perPage:
type: integer
operationId: get-fleet-proxies
post:
summary: Fleet Proxies - Create
description: Create a new Fleet Server Host
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
item:
$ref: '#/components/schemas/proxies'
requestBody:
content:
application/json:
schema:
type: object
properties:
id:
type: string
name:
type: string
url:
type: string
proxy_headers:
type: object
certificate_authorities:
type: string
certificate:
type: string
certificate_key:
type: string
required:
- name
- url
operationId: post-fleet-proxies
/proxies/{itemId}:
get:
summary: Fleet Proxies - Info
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
item:
$ref: '#/components/schemas/proxies'
required:
- item
operationId: get-one-fleet-proxies
parameters:
- schema:
type: string
name: itemId
in: path
required: true
delete:
summary: Fleet Proxies - Delete
operationId: delete-fleet-proxies
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 Proxies - Update
operationId: update-fleet-proxies
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
url:
type: string
proxy_headers:
type: object
certificate_authorities:
type: string
certificate:
type: string
certificate_key:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
item:
$ref: '#/components/schemas/proxies'
required:
- item
parameters:
- schema:
type: string
name: itemId
in: path
required: true
- $ref: '#/components/parameters/kbn_xsrf'
components:
securitySchemes:
basicAuth:
@ -3794,5 +3941,26 @@ components:
- is_default
- is_preconfigured
- host_urls
proxies:
title: Fleet Proxy
type: object
properties:
id:
type: string
name:
type: string
url:
type: string
proxy_headers:
type: object
certificate_authorities:
type: string
certificate:
type: string
certificate_key:
type: string
required:
- name
- url
security:
- basicAuth: []

View file

@ -0,0 +1,20 @@
title: Fleet Proxy
type: object
properties:
id:
type: string
name:
type: string
url:
type: string
proxy_headers:
type: object
certificate_authorities:
type: string
certificate:
type: string
certificate_key:
type: string
required:
- name
- url

View file

@ -133,6 +133,11 @@ paths:
$ref: paths/fleet_server_hosts.yaml
/fleet_server_hosts/{itemId}:
$ref: paths/fleet_server_hosts@{item_id}.yaml
# Fleet proxies
/proxies:
$ref: paths/proxies.yaml
/proxies/{itemId}:
$ref: paths/proxies@{item_id}.yaml
components:
securitySchemes:
basicAuth:

View file

@ -0,0 +1,61 @@
get:
summary: Fleet Proxies - List
description: Return a list of Proxies
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: ../components/schemas/proxies.yaml
total:
type: integer
page:
type: integer
perPage:
type: integer
operationId: get-fleet-proxies
post:
summary: Fleet Proxies - Create
description: 'Create a new Fleet Server Host'
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
item:
$ref: ../components/schemas/proxies.yaml
requestBody:
content:
application/json:
schema:
type: object
properties:
id:
type: string
name:
type: string
url:
type: string
proxy_headers:
type: object
certificate_authorities:
type: string
certificate:
type: string
certificate_key:
type: string
required:
- name
- url
operationId: post-fleet-proxies

View file

@ -0,0 +1,84 @@
get:
summary: Fleet Proxies - Info
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
item:
$ref: ../components/schemas/proxies.yaml
required:
- item
operationId: get-one-fleet-proxies
parameters:
- schema:
type: string
name: itemId
in: path
required: true
delete:
summary: Fleet Proxies - Delete
operationId: delete-fleet-proxies
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 Proxies - Update
operationId: update-fleet-proxies
requestBody:
content:
application/json:
schema:
type: object
properties:
name:
type: string
url:
type: string
proxy_headers:
type: object
certificate_authorities:
type: string
certificate:
type: string
certificate_key:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
item:
$ref: ../components/schemas/proxies.yaml
required:
- item
parameters:
- schema:
type: string
name: itemId
in: path
required: true
- $ref: ../components/headers/kbn_xsrf.yaml

View file

@ -22,6 +22,7 @@ import {
PRECONFIGURATION_API_ROUTES,
DOWNLOAD_SOURCE_API_ROUTES,
FLEET_SERVER_HOST_API_ROUTES,
FLEET_PROXY_API_ROUTES,
} from '../constants';
export const epmRouteService = {
@ -229,6 +230,16 @@ export const outputRoutesService = {
getCreateLogstashApiKeyPath: () => OUTPUT_API_ROUTES.LOGSTASH_API_KEY_PATTERN,
};
export const fleetProxiesRoutesService = {
getInfoPath: (itemId: string) => FLEET_PROXY_API_ROUTES.INFO_PATTERN.replace('{itemId}', itemId),
getUpdatePath: (itemId: string) =>
FLEET_PROXY_API_ROUTES.UPDATE_PATTERN.replace('{itemId}', itemId),
getListPath: () => FLEET_PROXY_API_ROUTES.LIST_PATTERN,
getDeletePath: (itemId: string) =>
FLEET_PROXY_API_ROUTES.DELETE_PATTERN.replace('{itemId}', itemId),
getCreatePath: () => FLEET_PROXY_API_ROUTES.CREATE_PATTERN,
};
export const fleetServerHostsRoutesService = {
getInfoPath: (itemId: string) =>
FLEET_SERVER_HOST_API_ROUTES.INFO_PATTERN.replace('{itemId}', itemId),

View file

@ -0,0 +1,27 @@
/*
* 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.
*/
interface BaseFleetProxy {
name: string;
url: string;
certificate_authorities?: string | null;
certificate?: string | null;
certificate_key?: string | null;
is_preconfigured: boolean;
}
export interface NewFleetProxy extends BaseFleetProxy {
proxy_headers?: Record<string, string | number | boolean> | null;
}
export interface FleetProxy extends NewFleetProxy {
id: string;
}
export interface FleetProxySOAttributes extends BaseFleetProxy {
proxy_headers?: string | null;
}

View file

@ -10,6 +10,7 @@ export interface NewFleetServerHost {
host_urls: string[];
is_default: boolean;
is_preconfigured: boolean;
proxy_id?: string | null;
}
export interface FleetServerHost extends NewFleetServerHost {

View file

@ -17,3 +17,4 @@ export * from './settings';
export * from './preconfiguration';
export * from './download_sources';
export * from './fleet_server_policy_config';
export * from './fleet_proxy';

View file

@ -25,6 +25,7 @@ export interface NewOutput {
certificate?: string;
key?: string;
} | null;
proxy_id?: string | null;
}
export type OutputSOAttributes = NewOutput & {

View file

@ -0,0 +1,34 @@
/*
* 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 { FleetProxy } from '../models';
import type { ListResult } from './common';
export type GetFleetProxiesResponse = ListResult<FleetProxy>;
export interface PostFleetProxiesRequest {
body: {
name: string;
url: string;
proxy_headers?: { [k: string]: string | boolean | number };
certificate_autorithies?: string;
certificate?: string;
certificate_key?: string;
};
}
export interface PutFleetProxiesRequest {
body: {
name?: string;
url?: string;
proxy_headers?: { [k: string]: string | boolean | number };
certificate_autorithies?: string;
certificate?: string;
certificate_key?: string;
};
}

View file

@ -19,6 +19,7 @@ export interface PutFleetServerHostsRequest {
name?: string;
host_urls?: string[];
is_default?: boolean;
proxy_id?: string | null;
};
}
@ -28,6 +29,7 @@ export interface PostFleetServerHostsRequest {
name?: string;
host_urls?: string[];
is_default?: boolean;
proxy_id?: string | null;
};
}

View file

@ -60,6 +60,7 @@ export interface PostOutputRequest {
certificate?: string;
key?: string;
};
proxy_id?: string | null;
};
}

View file

@ -0,0 +1,181 @@
/*
* 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 React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiButton,
EuiForm,
} from '@elastic/eui';
import { FLYOUT_MAX_WIDTH } from '../../constants';
import type { FleetProxy } from '../../../../types';
import { TextInput, TextAreaInput } from '../form';
import { useFleetProxyForm } from './user_fleet_proxy_form';
export interface FleetProxyFlyoutProps {
onClose: () => void;
fleetProxy?: FleetProxy;
}
export const FleetProxyFlyout: React.FunctionComponent<FleetProxyFlyoutProps> = ({
onClose,
fleetProxy,
}) => {
// const { docLinks } = useStartServices();
const form = useFleetProxyForm(fleetProxy, onClose);
const { inputs } = form;
return (
<EuiFlyout maxWidth={FLYOUT_MAX_WIDTH} onClose={onClose}>
<EuiFlyoutHeader hasBorder={true}>
<EuiTitle size="m">
<h2>
{fleetProxy ? (
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.editTitle"
defaultMessage="Edit Proxy"
/>
) : (
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.addTitle"
defaultMessage="Add Proxy"
/>
)}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiForm onSubmit={form.submit}>
<TextInput
label={
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.nameInputLabel"
defaultMessage="Name"
/>
}
inputProps={inputs.nameInput}
data-test-subj="fleetProxyFlyout.nameInput"
placeholder={i18n.translate(
'xpack.fleet.settings.fleetProxyFlyout.nameInputPlaceholder',
{
defaultMessage: 'Specify name',
}
)}
/>
<TextInput
label={
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.urlInputLabel"
defaultMessage="Proxy Url"
/>
}
dataTestSubj="fleetProxyFlyout.urlInput"
inputProps={inputs.urlInput}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetProxyFlyout.urlInputPlaceholder',
{ defaultMessage: 'Specify proxy url' }
)}
/>
<TextAreaInput
label={
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.proxyHeadersLabel"
defaultMessage="Proxy headers"
/>
}
dataTestSubj="fleetProxyFlyout.proxyHeadersInput"
inputProps={inputs.proxyHeadersInput}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetProxyFlyout.proxyHeadersPlaceholder',
{ defaultMessage: 'Specify proxy headers' }
)}
/>
<TextInput
label={
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.certificateAuthoritiesLabel"
defaultMessage="Certificate authorities"
/>
}
dataTestSubj="fleetProxyFlyout.certificateAuthoritiesInput"
inputProps={inputs.certificateAuthoritiesInput}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetProxyFlyout.certificateAuthoritiesPlaceholder',
{ defaultMessage: 'Specify certificate authorities' }
)}
/>
<TextInput
label={
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.certificateLabel"
defaultMessage="Certificate"
/>
}
dataTestSubj="fleetProxyFlyout.certificateInput"
inputProps={inputs.certificateInput}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetProxyFlyout.certificatePlaceholder',
{ defaultMessage: 'Specify certificate' }
)}
/>
<TextInput
label={
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.certificateKeyLabel"
defaultMessage="Certificate key"
/>
}
dataTestSubj="fleetProxyFlyout.certificateKeyInput"
inputProps={inputs.certificateKeyInput}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetProxyFlyout.certificateKeyPlaceholder',
{ defaultMessage: 'Specify certificate key' }
)}
/>
</EuiForm>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => onClose()} flush="left">
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
isLoading={form.isLoading}
isDisabled={form.isDisabled}
onClick={form.submit}
data-test-subj="saveApplySettingsBtn"
>
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.saveButton"
defaultMessage="Save and apply settings"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};

View file

@ -0,0 +1,206 @@
/*
* 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 React, { useCallback, useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { safeDump, safeLoad } from 'js-yaml';
import {
sendPostFleetProxy,
sendPutFleetProxy,
useInput,
useStartServices,
validateInputs,
} from '../../../../hooks';
import { useConfirmModal } from '../../hooks/use_confirm_modal';
import type { FleetProxy } from '../../../../types';
const URL_REGEX = /^(http)(s)?:\/\/[^\s$.?#].[^\s]*$/gm;
const ConfirmTitle = () => (
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.confirmModalTitle"
defaultMessage="Save and deploy changes?"
/>
);
const ConfirmDescription: React.FunctionComponent = ({}) => (
<FormattedMessage
id="xpack.fleet.settings.fleetProxyFlyout.confirmModalText"
defaultMessage="This action will update agent policies using that proxies. This action can not be undone. Are you sure you wish to continue?"
/>
);
function validateUrl(value: string) {
if (!value || value === '') {
return [
i18n.translate('xpack.fleet.settings.fleetProxyFlyoutUrlRequired', {
defaultMessage: 'URL is required',
}),
];
}
if (!value.match(URL_REGEX)) {
return [
i18n.translate('xpack.fleet.settings.fleetProxyFlyoutUrlError', {
defaultMessage: 'Invalid URL',
}),
];
}
}
function validateProxyHeaders(value: string) {
if (value && value !== '') {
const res = safeLoad(value);
if (
typeof res !== 'object' ||
Object.values(res).some((val) => {
const valType = typeof val;
return valType !== 'string' && valType !== 'number' && valType !== 'boolean';
})
) {
return [
i18n.translate('xpack.fleet.settings.fleetProxy.proxyHeadersErrorMessage', {
defaultMessage: 'Proxy headers is not a valid key: value object.',
}),
];
}
}
}
export function validateName(value: string) {
if (!value || value === '') {
return [
i18n.translate('xpack.fleet.settings.fleetProxy.nameIsRequiredErrorMessage', {
defaultMessage: 'Name is required',
}),
];
}
}
export function useFleetProxyForm(fleetProxy: FleetProxy | undefined, onSuccess: () => void) {
const [isLoading, setIsLoading] = useState(false);
const { notifications } = useStartServices();
const { confirm } = useConfirmModal();
const isPreconfigured = fleetProxy?.is_preconfigured ?? false;
const nameInput = useInput(fleetProxy?.name ?? '', validateName, isPreconfigured);
const urlInput = useInput(fleetProxy?.url ?? '', validateUrl, isPreconfigured);
const proxyHeadersInput = useInput(
fleetProxy?.proxy_headers ? safeDump(fleetProxy.proxy_headers) : '',
validateProxyHeaders,
isPreconfigured
);
const certificateAuthoritiesInput = useInput(
fleetProxy?.certificate_authorities ?? '',
() => undefined,
isPreconfigured
);
const certificateInput = useInput(
fleetProxy?.certificate ?? '',
() => undefined,
isPreconfigured
);
const certificateKeyInput = useInput(
fleetProxy?.certificate_key ?? '',
() => undefined,
isPreconfigured
);
const inputs = useMemo(
() => ({
nameInput,
urlInput,
proxyHeadersInput,
certificateAuthoritiesInput,
certificateInput,
certificateKeyInput,
}),
[
nameInput,
urlInput,
proxyHeadersInput,
certificateAuthoritiesInput,
certificateInput,
certificateKeyInput,
]
);
const validate = useCallback(() => validateInputs(inputs), [inputs]);
const submit = useCallback(async () => {
try {
if (!validate()) {
return;
}
if (fleetProxy && !(await confirm(<ConfirmTitle />, <ConfirmDescription />))) {
return;
}
setIsLoading(true);
const data = {
name: nameInput.value,
url: urlInput.value,
proxy_headers:
proxyHeadersInput.value === '' ? undefined : safeLoad(proxyHeadersInput.value),
certificate_authorities: certificateAuthoritiesInput.value,
certificate: certificateInput.value,
certificate_key: certificateKeyInput.value,
};
if (fleetProxy) {
const res = await sendPutFleetProxy(fleetProxy.id, data);
if (res.error) {
throw res.error;
}
} else {
const res = await sendPostFleetProxy(data);
if (res.error) {
throw res.error;
}
}
notifications.toasts.addSuccess(
i18n.translate('xpack.fleet.settings.fleetProxyFlyout.successToastTitle', {
defaultMessage: 'Fleet proxy saved',
})
);
setIsLoading(false);
await onSuccess();
} catch (error) {
setIsLoading(false);
notifications.toasts.addError(error, {
title: i18n.translate('xpack.fleet.settings.fleetProxyFlyout.errorToastTitle', {
defaultMessage: 'An error happened while saving Fleet Server host',
}),
});
}
}, [
fleetProxy,
nameInput.value,
urlInput.value,
proxyHeadersInput.value,
certificateAuthoritiesInput.value,
certificateInput.value,
certificateKeyInput.value,
validate,
notifications,
confirm,
onSuccess,
]);
const hasChanged = Object.values(inputs).some((input) => input.hasChanged);
const isDisabled =
isLoading || !hasChanged || nameInput.props.isInvalid || urlInput.props.isInvalid;
return {
isLoading,
isDisabled,
submit,
inputs,
};
}

View file

@ -29,7 +29,9 @@ const mockedUsedFleetStatus = useFleetStatus as jest.MockedFunction<typeof useFl
function renderFlyout(output?: Output) {
const renderer = createFleetTestRendererMock();
const utils = renderer.render(<EditOutputFlyout output={output} onClose={() => {}} />);
const utils = renderer.render(
<EditOutputFlyout proxies={[]} output={output} onClose={() => {}} />
);
return { utils };
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFlyout,
@ -26,11 +26,12 @@ import {
EuiCallOut,
EuiSpacer,
EuiLink,
EuiComboBox,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { MultiRowInput } from '../multi_row_input';
import type { Output } from '../../../../types';
import type { Output, FleetProxy } from '../../../../types';
import { FLYOUT_MAX_WIDTH } from '../../constants';
import { LogstashInstructions } from '../logstash_instructions';
import { useBreadcrumbs, useStartServices } from '../../../../hooks';
@ -42,6 +43,7 @@ import { EncryptionKeyRequiredCallout } from './encryption_key_required_callout'
export interface EditOutputFlyoutProps {
output?: Output;
onClose: () => void;
proxies: FleetProxy[];
}
const OUTPUT_TYPE_OPTIONS = [
@ -52,12 +54,18 @@ const OUTPUT_TYPE_OPTIONS = [
export const EditOutputFlyout: React.FunctionComponent<EditOutputFlyoutProps> = ({
onClose,
output,
proxies,
}) => {
useBreadcrumbs('settings');
const form = useOutputForm(onClose, output);
const inputs = form.inputs;
const { docLinks } = useStartServices();
const proxiesOptions = useMemo(
() => proxies.map((proxy) => ({ value: proxy.id, label: proxy.name })),
[proxies]
);
const isLogstashOutput = inputs.typeInput.value === 'logstash';
const isESOutput = inputs.typeInput.value === 'elasticsearch';
@ -301,6 +309,36 @@ export const EditOutputFlyout: React.FunctionComponent<EditOutputFlyoutProps> =
/>
</EuiFormRow>
)}
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.proxyIdLabel"
defaultMessage="Proxy"
/>
}
>
<EuiComboBox
fullWidth
data-test-subj="settingsOutputsFlyout.proxyIdInput"
{...inputs.proxyIdInput.props}
onChange={(options) => inputs.proxyIdInput.setValue(options?.[0]?.value ?? '')}
selectedOptions={
inputs.proxyIdInput.value !== ''
? proxiesOptions.filter((option) => option.value === inputs.proxyIdInput.value)
: []
}
options={proxiesOptions}
singleSelection={{ asPlainText: true }}
isClearable={true}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.proxyIdPlaceholder',
{
defaultMessage: 'Select proxy',
}
)}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.fleet.settings.editOutputFlyout.yamlConfigInputLabel', {
defaultMessage: 'Advanced YAML configuration',

View file

@ -96,6 +96,8 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
isPreconfigured
);
const proxyIdInput = useInput(output?.proxy_id ?? '', () => undefined, isPreconfigured);
const sslKeyInput = useInput(output?.ssl?.key ?? '', validateSSLKey, isPreconfigured);
const isLogstash = typeInput.value === 'logstash';
@ -112,6 +114,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
sslCertificateInput,
sslKeyInput,
sslCertificateAuthoritiesInput,
proxyIdInput,
};
const hasChanged = Object.values(inputs).some((input) => input.hasChanged);
@ -161,6 +164,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
}
setIsloading(true);
const proxyIdValue = proxyIdInput.value !== '' ? proxyIdInput.value : null;
const data: PostOutputRequest['body'] = isLogstash
? {
name: nameInput.value,
@ -176,6 +180,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
(val) => val !== ''
),
},
proxy_id: proxyIdValue,
}
: {
name: nameInput.value,
@ -185,6 +190,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
is_default_monitoring: defaultMonitoringOutputInput.value,
config_yaml: additionalYamlConfigInput.value,
ca_trusted_fingerprint: caTrustedFingerprintInput.value,
proxy_id: proxyIdValue,
};
if (output) {
@ -231,6 +237,7 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
sslKeyInput.value,
nameInput.value,
typeInput.value,
proxyIdInput.value,
notifications.toasts,
onSucess,

View file

@ -0,0 +1,107 @@
/*
* 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 React, { useMemo } from 'react';
import styled from 'styled-components';
import { EuiBasicTable, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useLink } from '../../../../hooks';
import type { FleetProxy } from '../../../../types';
export interface FleetProxiesTableProps {
proxies: FleetProxy[];
deleteFleetProxy: (ds: FleetProxy) => void;
}
const NameFlexItemWithMaxWidth = styled(EuiFlexItem)`
max-width: 250px;
`;
export const FleetProxiesTable: React.FunctionComponent<FleetProxiesTableProps> = ({
proxies,
deleteFleetProxy,
}) => {
const { getHref } = useLink();
const columns = useMemo((): Array<EuiBasicTableColumn<FleetProxy>> => {
return [
{
render: (fleetProxy: FleetProxy) => (
<EuiFlexGroup alignItems="center" gutterSize="xs">
<NameFlexItemWithMaxWidth grow={false}>
<p
title={fleetProxy.name}
className={`eui-textTruncate`}
data-test-subj="fleetProxiesTable.name"
>
{fleetProxy.name}
</p>
</NameFlexItemWithMaxWidth>
</EuiFlexGroup>
),
width: '288px',
name: i18n.translate('xpack.fleet.settings.fleetProxiesTable.nameColumnTitle', {
defaultMessage: 'Name',
}),
},
{
truncateText: true,
field: 'url',
name: i18n.translate('xpack.fleet.settings.fleetProxiesTable.urlColumnTitle', {
defaultMessage: 'Url',
}),
},
{
width: '68px',
render: (fleetProxy: FleetProxy) => {
const isDeleteVisible = true;
return (
<EuiFlexGroup gutterSize="s" justifyContent="flexEnd">
<EuiFlexItem grow={false}>
{isDeleteVisible && (
<EuiButtonIcon
color="text"
iconType="trash"
onClick={() => deleteFleetProxy(fleetProxy)}
title={i18n.translate(
'xpack.fleet.settings.fleetProxiesTable.deleteButtonTitle',
{
defaultMessage: 'Delete',
}
)}
data-test-subj="fleetProxiesTable.delete.btn"
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
color="text"
iconType="pencil"
href={getHref('settings_edit_fleet_proxy', {
itemId: fleetProxy.id,
})}
title={i18n.translate('xpack.fleet.settings.fleetProxiesTable.editButtonTitle', {
defaultMessage: 'Edit',
})}
data-test-subj="fleetProxiesTable.edit.btn"
/>
</EuiFlexItem>
</EuiFlexGroup>
);
},
name: i18n.translate('xpack.fleet.settings.fleetProxiesTable.actionsColumnTitle', {
defaultMessage: 'Actions',
}),
},
];
}, [deleteFleetProxy, getHref]);
return <EuiBasicTable columns={columns} items={proxies} data-test-subj="fleetProxiesTable" />;
};

View file

@ -32,7 +32,7 @@ const fleetServerHost = {
export const FleetServerHostsFlyout = ({ width }: Args) => {
return (
<div style={{ width }}>
<Component onClose={() => {}} fleetServerHost={fleetServerHost} />
<Component proxies={[]} onClose={() => {}} fleetServerHost={fleetServerHost} />
</div>
);
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React from 'react';
import React, { useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
@ -23,31 +23,39 @@ import {
EuiSpacer,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiSwitch,
EuiComboBox,
} from '@elastic/eui';
import { MultiRowInput } from '../multi_row_input';
import { useStartServices } from '../../../../hooks';
import { FLYOUT_MAX_WIDTH } from '../../constants';
import type { FleetServerHost } from '../../../../types';
import type { FleetServerHost, FleetProxy } from '../../../../types';
import { TextInput } from '../form';
import { useFleetServerHostsForm } from './use_fleet_server_host_form';
export interface FleetServerHostsFlyoutProps {
onClose: () => void;
fleetServerHost?: FleetServerHost;
proxies: FleetProxy[];
}
export const FleetServerHostsFlyout: React.FunctionComponent<FleetServerHostsFlyoutProps> = ({
onClose,
fleetServerHost,
proxies,
}) => {
const { docLinks } = useStartServices();
const form = useFleetServerHostsForm(fleetServerHost, onClose);
const { inputs } = form;
const proxiesOptions = useMemo(
() => proxies.map((proxy) => ({ value: proxy.id, label: proxy.name })),
[proxies]
);
return (
<EuiFlyout maxWidth={FLYOUT_MAX_WIDTH} onClose={onClose}>
<EuiFlyoutHeader hasBorder={true}>
@ -69,28 +77,20 @@ export const FleetServerHostsFlyout: React.FunctionComponent<FleetServerHostsFly
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiForm onSubmit={form.submit}>
<EuiFormRow
fullWidth
<TextInput
label={
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.nameInputLabel"
defaultMessage="Name"
/>
}
{...inputs.nameInput.formRowProps}
>
<EuiFieldText
data-test-subj="fleetServerHostsFlyout.nameInput"
fullWidth
{...inputs.nameInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHostsFlyout.nameInputPlaceholder',
{
defaultMessage: 'Specify name',
}
)}
/>
</EuiFormRow>
inputProps={inputs.nameInput}
dataTestSubj="fleetServerHostsFlyout.nameInput"
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHostsFlyout.nameInputPlaceholder',
{ defaultMessage: 'Specify name' }
)}
/>
<EuiFormRow
fullWidth
label={
@ -134,6 +134,36 @@ export const FleetServerHostsFlyout: React.FunctionComponent<FleetServerHostsFly
/>
</>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.fleetServerHostsFlyout.proxyIdLabel"
defaultMessage="Proxy"
/>
}
>
<EuiComboBox
fullWidth
data-test-subj="fleetServerHostsFlyout.proxyIdInput"
{...inputs.proxyIdInput.props}
onChange={(options) => inputs.proxyIdInput.setValue(options?.[0]?.value ?? '')}
selectedOptions={
inputs.proxyIdInput.value !== ''
? proxiesOptions.filter((option) => option.value === inputs.proxyIdInput.value)
: []
}
options={proxiesOptions}
singleSelection={{ asPlainText: true }}
isClearable={true}
placeholder={i18n.translate(
'xpack.fleet.settings.fleetServerHostsFlyout.proxyIdPlaceholder',
{
defaultMessage: 'Select proxy',
}
)}
/>
</EuiFormRow>
<EuiFormRow fullWidth {...inputs.isDefaultInput.formRowProps}>
<EuiSwitch
data-test-subj="fleetServerHostsFlyout.isDefaultSwitch"

View file

@ -5,7 +5,7 @@
* 2.0.
*/
// copy this one
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
@ -17,6 +17,7 @@ import {
useInput,
useStartServices,
useSwitchInput,
validateInputs,
} from '../../../../hooks';
import { isDiffPathProtocol } from '../../../../../../../common/services';
import { useConfirmModal } from '../../hooks/use_confirm_modal';
@ -133,12 +134,20 @@ export function useFleetServerHostsForm(
validateFleetServerHosts,
isPreconfigured
);
const proxyIdInput = useInput(fleetServerHost?.proxy_id ?? '', () => undefined, isPreconfigured);
const validate = useCallback(
() => hostUrlsInput.validate() && nameInput.validate(),
[hostUrlsInput, nameInput]
const inputs = useMemo(
() => ({
nameInput,
isDefaultInput,
hostUrlsInput,
proxyIdInput,
}),
[nameInput, isDefaultInput, hostUrlsInput, proxyIdInput]
);
const validate = useCallback(() => validateInputs(inputs), [inputs]);
const submit = useCallback(async () => {
try {
if (!validate()) {
@ -148,21 +157,19 @@ export function useFleetServerHostsForm(
return;
}
setIsLoading(true);
const data = {
name: nameInput.value,
host_urls: hostUrlsInput.value,
is_default: isDefaultInput.value,
proxy_id: proxyIdInput.value !== '' ? proxyIdInput.value : null,
};
if (fleetServerHost) {
const res = await sendPutFleetServerHost(fleetServerHost.id, {
name: nameInput.value,
host_urls: hostUrlsInput.value,
is_default: isDefaultInput.value,
});
const res = await sendPutFleetServerHost(fleetServerHost.id, data);
if (res.error) {
throw res.error;
}
} else {
const res = await sendPostFleetServerHost({
name: nameInput.value,
host_urls: hostUrlsInput.value,
is_default: isDefaultInput.value,
});
const res = await sendPostFleetServerHost(data);
if (res.error) {
throw res.error;
}
@ -187,6 +194,7 @@ export function useFleetServerHostsForm(
nameInput.value,
hostUrlsInput.value,
isDefaultInput.value,
proxyIdInput.value,
validate,
notifications,
confirm,
@ -195,7 +203,10 @@ export function useFleetServerHostsForm(
const isDisabled =
isLoading ||
(!hostUrlsInput.hasChanged && !isDefaultInput.hasChanged && !nameInput.hasChanged) ||
(!hostUrlsInput.hasChanged &&
!isDefaultInput.hasChanged &&
!nameInput.hasChanged &&
!proxyIdInput.hasChanged) ||
hostUrlsInput.props.isInvalid ||
nameInput.props.isInvalid;
@ -203,10 +214,6 @@ export function useFleetServerHostsForm(
isLoading,
isDisabled,
submit,
inputs: {
hostUrlsInput,
nameInput,
isDefaultInput,
},
inputs,
};
}

View file

@ -0,0 +1,61 @@
/*
* 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 React from 'react';
import type { ReactNode } from 'react';
import { EuiFieldText, EuiFormRow, EuiTextArea } from '@elastic/eui';
interface InputProps {
label: ReactNode;
placeholder?: string;
dataTestSubj?: string;
inputProps: {
props: {
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
value: string;
isInvalid?: boolean;
disabled?: boolean;
};
formRowProps: {
error?: string[];
isInvalid?: boolean;
};
};
}
export const TextInput: React.FunctionComponent<InputProps> = ({
label,
inputProps,
placeholder,
dataTestSubj,
}) => (
<EuiFormRow fullWidth label={label} {...inputProps.formRowProps}>
<EuiFieldText
data-test-subj={dataTestSubj}
fullWidth
{...inputProps.props}
placeholder={placeholder}
/>
</EuiFormRow>
);
export const TextAreaInput: React.FunctionComponent<InputProps> = ({
label,
inputProps,
placeholder,
dataTestSubj,
}) => (
<EuiFormRow fullWidth label={label} {...inputProps.formRowProps}>
<EuiTextArea
fullWidth
rows={5}
data-test-subj={dataTestSubj}
{...inputProps.props}
placeholder={placeholder}
/>
</EuiFormRow>
);

View file

@ -0,0 +1,59 @@
/*
* 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 React from 'react';
import { EuiTitle, EuiText, EuiSpacer, EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useLink } from '../../../../hooks';
import type { FleetProxy } from '../../../../types';
import { FleetProxiesTable } from '../fleet_proxies_table';
export interface FleetProxiesSectionProps {
proxies: FleetProxy[];
deleteFleetProxy: (proxy: FleetProxy) => void;
}
export const FleetProxiesSection: React.FunctionComponent<FleetProxiesSectionProps> = ({
proxies,
deleteFleetProxy,
}) => {
const { getHref } = useLink();
return (
<>
<EuiTitle size="s">
<h4>
<FormattedMessage
id="xpack.fleet.settings.fleetProxiesSection.title"
defaultMessage="Proxies"
/>
</h4>
</EuiTitle>
<EuiSpacer size="xs" />
<EuiText color="subdued" size="m">
<FormattedMessage
id="xpack.fleet.settings.fleetProxiesSection.subtitle"
defaultMessage="Specify any proxy URLs to be used in Fleet servers or Outputs."
/>
</EuiText>
<EuiSpacer size="m" />
<FleetProxiesTable proxies={proxies} deleteFleetProxy={deleteFleetProxy} />
<EuiSpacer size="s" />
<EuiButtonEmpty
iconType="plusInCircle"
href={getHref('settings_create_fleet_proxy')}
data-test-subj="addProxyBtn"
>
<FormattedMessage
id="xpack.fleet.settings.fleetProxiesSection.CreateButtonLabel"
defaultMessage="Add proxy"
/>
</EuiButtonEmpty>
</>
);
};

View file

@ -8,28 +8,33 @@
import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import type { Output, DownloadSource, FleetServerHost } from '../../../../types';
import type { Output, DownloadSource, FleetServerHost, FleetProxy } from '../../../../types';
import { FleetServerHostsSection } from './fleet_server_hosts_section';
import { OutputSection } from './output_section';
import { AgentBinarySection } from './agent_binary_section';
import { FleetProxiesSection } from './fleet_proxies_section';
export interface SettingsPageProps {
outputs: Output[];
proxies: FleetProxy[];
fleetServerHosts: FleetServerHost[];
deleteOutput: (output: Output) => void;
deleteFleetServerHost: (fleetServerHost: FleetServerHost) => void;
downloadSources: DownloadSource[];
deleteDownloadSource: (ds: DownloadSource) => void;
deleteFleetProxy: (proxy: FleetProxy) => void;
}
export const SettingsPage: React.FunctionComponent<SettingsPageProps> = ({
outputs,
proxies,
fleetServerHosts,
deleteOutput,
deleteFleetServerHost,
downloadSources,
deleteDownloadSource,
deleteFleetProxy,
}) => {
return (
<>
@ -45,6 +50,8 @@ export const SettingsPage: React.FunctionComponent<SettingsPageProps> = ({
downloadSources={downloadSources}
deleteDownloadSource={deleteDownloadSource}
/>
<EuiSpacer size="m" />
<FleetProxiesSection proxies={proxies} deleteFleetProxy={deleteFleetProxy} />
</>
);
};

View file

@ -4,3 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './use_delete_proxy';
export * from './use_delete_output';
export * from './use_delete_fleet_server_host';

View file

@ -0,0 +1,70 @@
/*
* 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 React, { useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { sendDeleteFleetProxy, useStartServices } from '../../../hooks';
import type { FleetProxy } from '../../../types';
import { useConfirmModal } from './use_confirm_modal';
const ConfirmTitle = () => (
<FormattedMessage
id="xpack.fleet.settings.deleteFleetProxy.confirmModalTitle"
defaultMessage="Delete and deploy changes?"
/>
);
const ConfirmDescription: React.FunctionComponent = ({}) => (
<FormattedMessage
id="xpack.fleet.settings.deleteFleetProxy.confirmModalText"
defaultMessage="This action will change agent policies currently using that proxy. Are you sure you wish to continue?"
/>
);
export function useDeleteProxy(onSuccess: () => void) {
const { confirm } = useConfirmModal();
const { notifications } = useStartServices();
const deleteFleetProxy = useCallback(
async (fleetProxy: FleetProxy) => {
try {
const isConfirmed = await confirm(<ConfirmTitle />, <ConfirmDescription />, {
buttonColor: 'danger',
confirmButtonText: i18n.translate(
'xpack.fleet.settings.deleteFleetProxy.confirmButtonLabel',
{
defaultMessage: 'Delete and deploy changes',
}
),
});
if (!isConfirmed) {
return;
}
const res = await sendDeleteFleetProxy(fleetProxy.id);
if (res.error) {
throw res.error;
}
onSuccess();
} catch (err) {
notifications.toasts.addError(err, {
title: i18n.translate('xpack.fleet.settings.deleteFleetProxy.errorToastTitle', {
defaultMessage: 'Error deleting proxy',
}),
});
}
},
[confirm, notifications.toasts, onSuccess]
);
return { deleteFleetProxy };
}

View file

@ -15,6 +15,7 @@ import {
useGetDownloadSources,
useGetFleetServerHosts,
useFlyoutContext,
useGetFleetProxies,
} from '../../hooks';
import { FLEET_ROUTING_PATHS, pagePathGetters } from '../../constants';
import { DefaultLayout } from '../../layouts';
@ -26,39 +27,51 @@ import { SettingsPage } from './components/settings_page';
import { withConfirmModalProvider } from './hooks/use_confirm_modal';
import { FleetServerHostsFlyout } from './components/fleet_server_hosts_flyout';
import { EditOutputFlyout } from './components/edit_output_flyout';
import { useDeleteOutput } from './hooks/use_delete_output';
import { useDeleteFleetServerHost } from './hooks/use_delete_fleet_server_host';
import { useDeleteOutput, useDeleteFleetServerHost, useDeleteProxy } from './hooks';
import { EditDownloadSourceFlyout } from './components/download_source_flyout';
import { useDeleteDownloadSource } from './components/download_source_flyout/use_delete_download_source';
import { FleetProxyFlyout } from './components/edit_fleet_proxy_flyout';
function useSettingsAppData() {
const outputs = useGetOutputs();
const fleetServerHosts = useGetFleetServerHosts();
const downloadSources = useGetDownloadSources();
const proxies = useGetFleetProxies();
return { outputs, fleetServerHosts, downloadSources, proxies };
}
export const SettingsApp = withConfirmModalProvider(() => {
useBreadcrumbs('settings');
const history = useHistory();
const outputs = useGetOutputs();
const fleetServerHosts = useGetFleetServerHosts();
const downloadSources = useGetDownloadSources();
const flyoutContext = useFlyoutContext();
const { outputs, fleetServerHosts, downloadSources, proxies } = useSettingsAppData();
const { deleteOutput } = useDeleteOutput(outputs.resendRequest);
const { deleteDownloadSource } = useDeleteDownloadSource(downloadSources.resendRequest);
const { deleteFleetServerHost } = useDeleteFleetServerHost(fleetServerHosts.resendRequest);
const { deleteFleetProxy } = useDeleteProxy(proxies.resendRequest);
const resendOutputRequest = outputs.resendRequest;
const resendDownloadSourceRequest = downloadSources.resendRequest;
const resendFleetServerHostsRequest = fleetServerHosts.resendRequest;
const resendProxiesRequest = proxies.resendRequest;
const onCloseCallback = useCallback(() => {
flyoutContext.closeFleetServerFlyout();
resendOutputRequest();
resendDownloadSourceRequest();
resendFleetServerHostsRequest();
resendProxiesRequest();
history.replace(pagePathGetters.settings()[1]);
}, [
flyoutContext,
resendOutputRequest,
resendDownloadSourceRequest,
resendFleetServerHostsRequest,
resendProxiesRequest,
history,
]);
@ -68,7 +81,9 @@ export const SettingsApp = withConfirmModalProvider(() => {
(fleetServerHosts.isLoading && fleetServerHosts.isInitialRequest) ||
!fleetServerHosts.data?.items ||
(downloadSources.isLoading && downloadSources.isInitialRequest) ||
!downloadSources.data?.items
!downloadSources.data?.items ||
(proxies.isLoading && proxies.isInitialRequest) ||
!proxies.data?.items
) {
return (
<DefaultLayout section="settings">
@ -93,6 +108,7 @@ export const SettingsApp = withConfirmModalProvider(() => {
return (
<EuiPortal>
<FleetServerHostsFlyout
proxies={proxies.data?.items ?? []}
onClose={onCloseCallback}
fleetServerHost={fleetServerHost}
/>
@ -107,9 +123,29 @@ export const SettingsApp = withConfirmModalProvider(() => {
</Route>
<Route path={FLEET_ROUTING_PATHS.settings_create_outputs}>
<EuiPortal>
<EditOutputFlyout onClose={onCloseCallback} />
<EditOutputFlyout proxies={proxies.data.items} onClose={onCloseCallback} />
</EuiPortal>
</Route>
<Route path={FLEET_ROUTING_PATHS.settings_create_fleet_proxy}>
<EuiPortal>
<FleetProxyFlyout onClose={onCloseCallback} />
</EuiPortal>
</Route>
<Route path={FLEET_ROUTING_PATHS.settings_edit_fleet_proxy}>
{(route: { match: { params: { itemId: string } } }) => {
const fleetProxy = proxies.data?.items.find(
(item) => route.match.params.itemId === item.id
);
if (!fleetProxy) {
return <Redirect to={FLEET_ROUTING_PATHS.settings} />;
}
return (
<EuiPortal>
<FleetProxyFlyout onClose={onCloseCallback} fleetProxy={fleetProxy} />
</EuiPortal>
);
}}
</Route>
<Route path={FLEET_ROUTING_PATHS.settings_edit_outputs}>
{(route: { match: { params: { outputId: string } } }) => {
const output = outputs.data?.items.find((o) => route.match.params.outputId === o.id);
@ -119,7 +155,11 @@ export const SettingsApp = withConfirmModalProvider(() => {
return (
<EuiPortal>
<EditOutputFlyout onClose={onCloseCallback} output={output} />
<EditOutputFlyout
proxies={proxies.data?.items ?? []}
onClose={onCloseCallback}
output={output}
/>
</EuiPortal>
);
}}
@ -151,6 +191,8 @@ export const SettingsApp = withConfirmModalProvider(() => {
</Switch>
</Router>
<SettingsPage
deleteFleetProxy={deleteFleetProxy}
proxies={proxies.data.items}
outputs={outputs.data.items}
fleetServerHosts={fleetServerHosts.data.items}
deleteOutput={deleteOutput}

View file

@ -19,6 +19,7 @@ export type StaticPage =
| 'settings_create_outputs'
| 'settings_create_download_sources'
| 'settings_create_fleet_server_hosts'
| 'settings_create_fleet_proxy'
| 'debug';
export type DynamicPage =
@ -44,7 +45,8 @@ export type DynamicPage =
| 'agent_details_diagnostics'
| 'settings_edit_outputs'
| 'settings_edit_download_sources'
| 'settings_edit_fleet_server_hosts';
| 'settings_edit_fleet_server_hosts'
| 'settings_edit_fleet_proxy';
export type Page = StaticPage | DynamicPage;
@ -77,6 +79,8 @@ export const FLEET_ROUTING_PATHS = {
settings_create_outputs: '/settings/create-outputs',
settings_edit_outputs: '/settings/outputs/:outputId',
settings_create_download_sources: '/settings/create-download-sources',
settings_create_fleet_proxy: '/settings/create-fleet-proxy',
settings_edit_fleet_proxy: '/settings/fleet-proxies/:itemId',
settings_edit_download_sources: '/settings/downloadSources/:downloadSourceId',
debug: '/_debug',
@ -213,6 +217,14 @@ export const pagePathGetters: {
FLEET_BASE_PATH,
FLEET_ROUTING_PATHS.settings_create_fleet_server_hosts,
],
settings_create_fleet_proxy: () => [
FLEET_BASE_PATH,
FLEET_ROUTING_PATHS.settings_create_fleet_proxy,
],
settings_edit_fleet_proxy: ({ itemId }) => [
FLEET_BASE_PATH,
FLEET_ROUTING_PATHS.settings_edit_fleet_proxy.replace(':itemId', itemId.toString()),
],
settings_edit_outputs: ({ outputId }) => [
FLEET_BASE_PATH,
FLEET_ROUTING_PATHS.settings_edit_outputs.replace(':outputId', outputId as string),

View file

@ -9,6 +9,18 @@ import { useState, useCallback, useEffect } from 'react';
import type React from 'react';
import type { EuiSwitchEvent } from '@elastic/eui';
export interface FormInput {
validate: () => boolean;
}
export function validateInputs(inputs: { [k: string]: FormInput }) {
return Object.values(inputs).reduce((acc, input) => {
const res = input.validate();
return acc === false ? acc : res;
}, true);
}
export function useInput(
defaultValue = '',
validate?: (value: string) => string[] | undefined,
@ -88,6 +100,8 @@ export function useSwitchInput(defaultValue = false, disabled = false) {
setValue(newValue);
};
const validate = useCallback(() => true, []);
return {
value,
props: {
@ -95,6 +109,7 @@ export function useSwitchInput(defaultValue = false, disabled = false) {
checked: value,
disabled,
},
validate,
formRowProps: {},
setValue,
hasChanged,

View file

@ -0,0 +1,38 @@
/*
* 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 { fleetProxiesRoutesService } from '../../../common/services';
import type {
GetFleetProxiesResponse,
PostFleetProxiesRequest,
PutFleetProxiesRequest,
} from '../../../common/types/rest_spec/fleet_proxies';
import { sendRequest, useRequest } from './use_request';
export function useGetFleetProxies() {
return useRequest<GetFleetProxiesResponse>({
method: 'get',
path: fleetProxiesRoutesService.getListPath(),
});
}
export function sendDeleteFleetProxy(proxyId: string) {
return sendRequest({ method: 'delete', path: fleetProxiesRoutesService.getDeletePath(proxyId) });
}
export function sendPostFleetProxy(body: PostFleetProxiesRequest['body']) {
return sendRequest({ method: 'post', path: fleetProxiesRoutesService.getCreatePath(), body });
}
export function sendPutFleetProxy(proxyId: string, body: PutFleetProxiesRequest['body']) {
return sendRequest({
method: 'put',
path: fleetProxiesRoutesService.getUpdatePath(proxyId),
body,
});
}

View file

@ -19,3 +19,4 @@ export * from './app';
export * from './ingest_pipelines';
export * from './download_source';
export * from './fleet_server_hosts';
export * from './fleet_proxies';

View file

@ -25,6 +25,7 @@ export type {
Output,
DownloadSource,
FleetServerHost,
FleetProxy,
DataStream,
Settings,
ActionStatus,

View file

@ -67,6 +67,8 @@ export {
// Fleet server host
DEFAULT_FLEET_SERVER_HOST_ID,
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
// Proxy
FLEET_PROXY_SAVED_OBJECT_TYPE,
// Authz
ENDPOINT_PRIVILEGES,
} from '../../common/constants';

View file

@ -77,6 +77,7 @@ export class OutputLicenceError extends FleetError {}
export class DownloadSourceError extends FleetError {}
export class FleetServerHostUnauthorizedError extends FleetError {}
export class FleetProxyUnauthorizedError extends FleetError {}
export class ArtifactsClientError extends FleetError {}
export class ArtifactsClientAccessDeniedError extends FleetError {

View file

@ -67,22 +67,7 @@ import {
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
} from './constants';
import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects';
import {
registerEPMRoutes,
registerPackagePolicyRoutes,
registerDataStreamRoutes,
registerAgentPolicyRoutes,
registerSetupRoutes,
registerAgentAPIRoutes,
registerEnrollmentApiKeyRoutes,
registerOutputRoutes,
registerSettingsRoutes,
registerAppRoutes,
registerPreconfigurationRoutes,
registerDownloadSourcesRoutes,
registerHealthCheckRoutes,
registerFleetServerHostRoutes,
} from './routes';
import { registerRoutes } from './routes';
import type { ExternalCallback, FleetRequestHandlerContext } from './types';
import type {
@ -402,29 +387,7 @@ export class FleetPlugin
makeRouterWithFleetAuthz(router);
core.http.registerOnPostAuth(fleetAuthzOnPostAuthHandler);
// Always register app routes for permissions checking
registerAppRoutes(fleetAuthzRouter);
// The upload package route is only authorized for the superuser
registerEPMRoutes(fleetAuthzRouter);
registerSetupRoutes(fleetAuthzRouter, config);
registerAgentPolicyRoutes(fleetAuthzRouter);
registerPackagePolicyRoutes(fleetAuthzRouter);
registerOutputRoutes(fleetAuthzRouter);
registerSettingsRoutes(fleetAuthzRouter);
registerDataStreamRoutes(fleetAuthzRouter);
registerPreconfigurationRoutes(fleetAuthzRouter);
registerFleetServerHostRoutes(fleetAuthzRouter);
registerDownloadSourcesRoutes(fleetAuthzRouter);
registerHealthCheckRoutes(fleetAuthzRouter);
// Conditional config routes
if (config.agents.enabled) {
registerAgentAPIRoutes(fleetAuthzRouter, config);
registerEnrollmentApiKeyRoutes(fleetAuthzRouter);
}
registerRoutes(fleetAuthzRouter, config);
this.telemetryEventsSender.setup(deps.telemetry);
this.bulkActionsResolver = new BulkActionsResolver(deps.taskManager, core);

View file

@ -0,0 +1,135 @@
/*
* 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 { RequestHandler } from '@kbn/core/server';
import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { TypeOf } from '@kbn/config-schema';
import {
listFleetProxies,
createFleetProxy,
deleteFleetProxy,
getFleetProxy,
updateFleetProxy,
} from '../../services/fleet_proxies';
import { defaultFleetErrorHandler } from '../../errors';
import type {
GetOneFleetProxyRequestSchema,
PostFleetProxyRequestSchema,
PutFleetProxyRequestSchema,
} from '../../types';
export const postFleetProxyHandler: RequestHandler<
undefined,
undefined,
TypeOf<typeof PostFleetProxyRequestSchema.body>
> = async (context, request, response) => {
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client;
try {
const { id, ...data } = request.body;
const proxy = await createFleetProxy(soClient, { ...data, is_preconfigured: false }, { id });
const body = {
item: proxy,
};
return response.ok({ body });
} catch (error) {
return defaultFleetErrorHandler({ error, response });
}
};
export const putFleetProxyHandler: RequestHandler<
TypeOf<typeof PutFleetProxyRequestSchema.params>,
undefined,
TypeOf<typeof PutFleetProxyRequestSchema.body>
> = async (context, request, response) => {
try {
const coreContext = await await context.core;
const soClient = coreContext.savedObjects.client;
const item = await updateFleetProxy(soClient, request.params.itemId, request.body);
const body = {
item,
};
// TODO bump policies on update
return response.ok({ body });
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound({
body: { message: `Proxy ${request.params.itemId} not found` },
});
}
return defaultFleetErrorHandler({ error, response });
}
};
export const getAllFleetProxyHandler: RequestHandler = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
try {
const res = await listFleetProxies(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 });
}
};
export const deleteFleetProxyHandler: RequestHandler<
TypeOf<typeof GetOneFleetProxyRequestSchema.params>
> = async (context, request, response) => {
try {
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client;
await deleteFleetProxy(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 proxy ${request.params.itemId} not found` },
});
}
return defaultFleetErrorHandler({ error, response });
}
};
export const getFleetProxyHandler: RequestHandler<
TypeOf<typeof GetOneFleetProxyRequestSchema.params>
> = async (context, request, response) => {
const soClient = (await context.core).savedObjects.client;
try {
const item = await getFleetProxy(soClient, request.params.itemId);
const body = {
item,
};
return response.ok({ body });
} catch (error) {
if (SavedObjectsErrorHelpers.isNotFoundError(error)) {
return response.notFound({
body: { message: `Fleet proxy ${request.params.itemId} not found` },
});
}
return defaultFleetErrorHandler({ error, response });
}
};

View file

@ -0,0 +1,79 @@
/*
* 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_PROXY_API_ROUTES } from '../../../common/constants';
import {
GetOneFleetProxyRequestSchema,
PostFleetProxyRequestSchema,
PutFleetProxyRequestSchema,
} from '../../types';
import type { FleetAuthzRouter } from '../security';
import {
getAllFleetProxyHandler,
postFleetProxyHandler,
deleteFleetProxyHandler,
getFleetProxyHandler,
putFleetProxyHandler,
} from './handler';
export const registerRoutes = (router: FleetAuthzRouter) => {
router.get(
{
path: FLEET_PROXY_API_ROUTES.LIST_PATTERN,
validate: false,
fleetAuthz: {
fleet: { all: true },
},
},
getAllFleetProxyHandler
);
router.post(
{
path: FLEET_PROXY_API_ROUTES.CREATE_PATTERN,
validate: PostFleetProxyRequestSchema,
fleetAuthz: {
fleet: { all: true },
},
},
postFleetProxyHandler
);
router.put(
{
path: FLEET_PROXY_API_ROUTES.UPDATE_PATTERN,
validate: PutFleetProxyRequestSchema,
fleetAuthz: {
fleet: { all: true },
},
},
putFleetProxyHandler
);
router.get(
{
path: FLEET_PROXY_API_ROUTES.DELETE_PATTERN,
validate: GetOneFleetProxyRequestSchema,
fleetAuthz: {
fleet: { all: true },
},
},
getFleetProxyHandler
);
router.delete(
{
path: FLEET_PROXY_API_ROUTES.DELETE_PATTERN,
validate: GetOneFleetProxyRequestSchema,
fleetAuthz: {
fleet: { all: true },
},
},
deleteFleetProxyHandler
);
};

View file

@ -5,17 +5,47 @@
* 2.0.
*/
export { registerRoutes as registerAgentPolicyRoutes } from './agent_policy';
export { registerRoutes as registerPackagePolicyRoutes } from './package_policy';
export { registerRoutes as registerDataStreamRoutes } from './data_streams';
export { registerRoutes as registerEPMRoutes } from './epm';
export { registerRoutes as registerSetupRoutes } from './setup';
export { registerAPIRoutes as registerAgentAPIRoutes } from './agent';
export { registerRoutes as registerEnrollmentApiKeyRoutes } from './enrollment_api_key';
export { registerRoutes as registerOutputRoutes } from './output';
export { registerRoutes as registerSettingsRoutes } from './settings';
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';
import type { FleetConfigType } from '../config';
import { registerRoutes as registerAgentPolicyRoutes } from './agent_policy';
import { registerRoutes as registerPackagePolicyRoutes } from './package_policy';
import { registerRoutes as registerDataStreamRoutes } from './data_streams';
import { registerRoutes as registerEPMRoutes } from './epm';
import { registerRoutes as registerSetupRoutes } from './setup';
import { registerAPIRoutes as registerAgentAPIRoutes } from './agent';
import { registerRoutes as registerEnrollmentApiKeyRoutes } from './enrollment_api_key';
import { registerRoutes as registerOutputRoutes } from './output';
import { registerRoutes as registerSettingsRoutes } from './settings';
import { registerRoutes as registerAppRoutes } from './app';
import { registerRoutes as registerPreconfigurationRoutes } from './preconfiguration';
import { registerRoutes as registerDownloadSourcesRoutes } from './download_source';
import { registerRoutes as registerHealthCheckRoutes } from './health_check';
import { registerRoutes as registerFleetServerHostRoutes } from './fleet_server_policy_config';
import { registerRoutes as registerFleetProxiesRoutes } from './fleet_proxies';
import type { FleetAuthzRouter } from './security';
export async function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: FleetConfigType) {
// Always register app routes for permissions checking
registerAppRoutes(fleetAuthzRouter);
// The upload package route is only authorized for the superuser
registerEPMRoutes(fleetAuthzRouter);
registerSetupRoutes(fleetAuthzRouter, config);
registerAgentPolicyRoutes(fleetAuthzRouter);
registerPackagePolicyRoutes(fleetAuthzRouter);
registerOutputRoutes(fleetAuthzRouter);
registerSettingsRoutes(fleetAuthzRouter);
registerDataStreamRoutes(fleetAuthzRouter);
registerPreconfigurationRoutes(fleetAuthzRouter);
registerFleetServerHostRoutes(fleetAuthzRouter);
registerFleetProxiesRoutes(fleetAuthzRouter);
registerDownloadSourcesRoutes(fleetAuthzRouter);
registerHealthCheckRoutes(fleetAuthzRouter);
// Conditional config routes
if (config.agents.enabled) {
registerAgentAPIRoutes(fleetAuthzRouter, config);
registerEnrollmentApiKeyRoutes(fleetAuthzRouter);
}
}

View file

@ -19,6 +19,7 @@ import {
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
DOWNLOAD_SOURCE_SAVED_OBJECT_TYPE,
FLEET_SERVER_HOST_SAVED_OBJECT_TYPE,
FLEET_PROXY_SAVED_OBJECT_TYPE,
} from '../constants';
import {
@ -136,6 +137,7 @@ const getSavedObjectTypes = (
config_yaml: { type: 'text' },
is_preconfigured: { type: 'boolean', index: false },
ssl: { type: 'binary' },
proxy_id: { type: 'keyword' },
},
},
migrations: {
@ -362,6 +364,26 @@ const getSavedObjectTypes = (
is_default: { type: 'boolean' },
host_urls: { type: 'keyword', index: false },
is_preconfigured: { type: 'boolean' },
proxy_id: { type: 'keyword' },
},
},
},
[FLEET_PROXY_SAVED_OBJECT_TYPE]: {
name: FLEET_PROXY_SAVED_OBJECT_TYPE,
hidden: false,
namespaceType: 'agnostic',
management: {
importableAndExportable: false,
},
mappings: {
properties: {
name: { type: 'keyword' },
url: { type: 'keyword', index: false },
proxy_headers: { type: 'text', index: false },
certificate_authorities: { type: 'keyword', index: false },
certificate: { type: 'keyword', index: false },
certificate_key: { type: 'keyword', index: false },
is_preconfigured: { type: 'boolean' },
},
},
},
@ -395,6 +417,7 @@ export function registerEncryptedSavedObjects(
'config',
'config_yaml',
'is_preconfigured',
'proxy_id',
]),
});
// Encrypted saved objects

View file

@ -0,0 +1,149 @@
/*
* 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, SavedObject } from '@kbn/core/server';
import { FLEET_PROXY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../constants';
import { FleetProxyUnauthorizedError } from '../errors';
import type { FleetProxy, FleetProxySOAttributes, NewFleetProxy } from '../types';
function savedObjectToFleetProxy(so: SavedObject<FleetProxySOAttributes>): FleetProxy {
const { proxy_headers: proxyHeaders, ...rest } = so.attributes;
return {
id: so.id,
proxy_headers: proxyHeaders && proxyHeaders !== '' ? JSON.parse(proxyHeaders) : undefined,
...rest,
};
}
function fleetProxyDataToSOAttribute(data: NewFleetProxy): FleetProxySOAttributes;
function fleetProxyDataToSOAttribute(data: Partial<NewFleetProxy>): Partial<FleetProxySOAttributes>;
function fleetProxyDataToSOAttribute(
data: Partial<NewFleetProxy> | NewFleetProxy
): Partial<FleetProxySOAttributes> | Partial<FleetProxySOAttributes> {
const { proxy_headers: proxyHeaders, ...rest } = data;
return {
proxy_headers: proxyHeaders ? JSON.stringify(proxyHeaders) : null,
...rest,
};
}
export async function listFleetProxies(soClient: SavedObjectsClientContract) {
const res = await soClient.find<FleetProxySOAttributes>({
type: FLEET_PROXY_SAVED_OBJECT_TYPE,
perPage: SO_SEARCH_LIMIT,
});
return {
items: res.saved_objects.map<FleetProxy>(savedObjectToFleetProxy),
total: res.total,
page: res.page,
perPage: res.per_page,
};
}
export async function createFleetProxy(
soClient: SavedObjectsClientContract,
data: NewFleetProxy,
options?: { id?: string; overwrite?: boolean; fromPreconfiguration?: boolean }
): Promise<FleetProxy> {
const res = await soClient.create<FleetProxySOAttributes>(
FLEET_PROXY_SAVED_OBJECT_TYPE,
fleetProxyDataToSOAttribute(data),
{
id: options?.id,
overwrite: options?.overwrite,
}
);
return savedObjectToFleetProxy(res);
}
export async function getFleetProxy(
soClient: SavedObjectsClientContract,
id: string
): Promise<FleetProxy> {
const res = await soClient.get<FleetProxySOAttributes>(FLEET_PROXY_SAVED_OBJECT_TYPE, id);
return savedObjectToFleetProxy(res);
}
export async function deleteFleetProxy(
soClient: SavedObjectsClientContract,
id: string,
options?: { fromPreconfiguration?: boolean }
) {
const fleetServerHost = await getFleetProxy(soClient, id);
if (fleetServerHost.is_preconfigured && !options?.fromPreconfiguration) {
throw new FleetProxyUnauthorizedError(`Cannot delete ${id} preconfigured proxy`);
}
// TODO remove from all outputs and fleet server
// await agentPolicyService.removeFleetServerHostFromAll(soClient, esClient, id);
return await soClient.delete(FLEET_PROXY_SAVED_OBJECT_TYPE, id);
}
export async function updateFleetProxy(
soClient: SavedObjectsClientContract,
id: string,
data: Partial<FleetProxy>,
options?: { fromPreconfiguration?: boolean }
) {
const originalItem = await getFleetProxy(soClient, id);
if (data.is_preconfigured && !options?.fromPreconfiguration) {
throw new FleetProxyUnauthorizedError(`Cannot update ${id} preconfigured proxy`);
}
await soClient.update<FleetProxySOAttributes>(
FLEET_PROXY_SAVED_OBJECT_TYPE,
id,
fleetProxyDataToSOAttribute(data)
);
return {
...originalItem,
...data,
};
}
export async function bulkGetFleetProxies(
soClient: SavedObjectsClientContract,
ids: string[],
{ ignoreNotFound = false } = { ignoreNotFound: true }
) {
if (ids.length === 0) {
return [];
}
const res = await soClient.bulkGet<FleetProxySOAttributes>(
ids.map((id) => ({
id,
type: FLEET_PROXY_SAVED_OBJECT_TYPE,
}))
);
return res.saved_objects
.map((so) => {
if (so.error) {
if (!ignoreNotFound || so.error.statusCode !== 404) {
throw so.error;
}
return undefined;
}
return savedObjectToFleetProxy(so);
})
.filter(
(fleetProxyOrUndefined): fleetProxyOrUndefined is FleetProxy =>
typeof fleetProxyOrUndefined !== 'undefined'
);
}

View file

@ -5,7 +5,11 @@
* 2.0.
*/
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import type {
ElasticsearchClient,
SavedObjectsClientContract,
SavedObject,
} from '@kbn/core/server';
import { normalizeHostsForAgents } from '../../common/services';
import {
@ -26,6 +30,16 @@ import { FleetServerHostUnauthorizedError } from '../errors';
import { agentPolicyService } from './agent_policy';
function savedObjectToFleetServerHost(so: SavedObject<FleetServerHostSOAttributes>) {
const data = { ...so.attributes };
if (data.proxy_id === null) {
delete data.proxy_id;
}
return { id: so.id, ...data };
}
export async function createFleetServerHost(
soClient: SavedObjectsClientContract,
data: NewFleetServerHost,
@ -53,10 +67,7 @@ export async function createFleetServerHost(
{ id: options?.id, overwrite: options?.overwrite }
);
return {
id: res.id,
...res.attributes,
};
return savedObjectToFleetServerHost(res);
}
export async function getFleetServerHost(
@ -68,10 +79,7 @@ export async function getFleetServerHost(
id
);
return {
id: res.id,
...res.attributes,
};
return savedObjectToFleetServerHost(res);
}
export async function listFleetServerHosts(soClient: SavedObjectsClientContract) {
@ -81,10 +89,7 @@ export async function listFleetServerHosts(soClient: SavedObjectsClientContract)
});
return {
items: res.saved_objects.map<FleetServerHost>((so) => ({
id: so.id,
...so.attributes,
})),
items: res.saved_objects.map<FleetServerHost>(savedObjectToFleetServerHost),
total: res.total,
page: res.page,
perPage: res.per_page,
@ -181,10 +186,7 @@ export async function bulkGetFleetServerHosts(
return undefined;
}
return {
id: so.id,
...so.attributes,
};
return savedObjectToFleetServerHost(so);
})
.filter(
(fleetServerHostOrUndefined): fleetServerHostOrUndefined is FleetServerHost =>
@ -223,10 +225,7 @@ export async function getDefaultFleetServerHost(
return null;
}
return {
id: res.saved_objects[0].id,
...res.saved_objects[0].attributes,
};
return savedObjectToFleetServerHost(res.saved_objects[0]);
}
/**

View file

@ -65,13 +65,14 @@ export function outputIdToUuid(id: string) {
return uuid(id, uuid.DNS);
}
function outputSavedObjectToOutput(so: SavedObject<OutputSOAttributes>) {
const { output_id: outputId, ssl, ...atributes } = so.attributes;
function outputSavedObjectToOutput(so: SavedObject<OutputSOAttributes>): Output {
const { output_id: outputId, ssl, proxy_id: proxyId, ...atributes } = so.attributes;
return {
id: outputId ?? so.id,
...atributes,
...(ssl ? { ssl: JSON.parse(ssl as string) } : {}),
...(proxyId ? { proxy_id: proxyId } : {}),
};
}

View file

@ -39,6 +39,9 @@ export type {
NewFleetServerHost,
FleetServerHost,
FleetServerHostSOAttributes,
NewFleetProxy,
FleetProxy,
FleetProxySOAttributes,
Installation,
EpmPackageInstallStatus,
InstallationStatus,

View file

@ -54,6 +54,7 @@ const OutputBaseSchema = {
key: schema.maybe(schema.string()),
})
),
proxy_id: schema.nullable(schema.string()),
};
export const NewOutputSchema = schema.object({ ...OutputBaseSchema });
@ -81,6 +82,7 @@ export const UpdateOutputSchema = schema.object({
key: schema.maybe(schema.string()),
})
),
proxy_id: schema.nullable(schema.string()),
});
export const OutputSchema = schema.object({

View file

@ -0,0 +1,46 @@
/*
* 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 PostFleetProxyRequestSchema = {
body: schema.object({
id: schema.maybe(schema.string()),
url: schema.string(),
name: schema.string(),
proxy_headers: schema.maybe(
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()),
}),
};
export const PutFleetProxyRequestSchema = {
params: schema.object({ itemId: schema.string() }),
body: schema.object({
name: schema.maybe(schema.string()),
url: schema.maybe(schema.string()),
proxy_headers: schema.nullable(
schema.recordOf(
schema.string(),
schema.oneOf([schema.string(), schema.boolean(), schema.number()])
)
),
certificate_authorities: schema.nullable(schema.string()),
certificate: schema.nullable(schema.string()),
certificate_key: schema.nullable(schema.string()),
}),
};
export const GetOneFleetProxyRequestSchema = {
params: schema.object({ itemId: schema.string() }),
};

View file

@ -13,6 +13,7 @@ export const PostFleetServerHostRequestSchema = {
name: schema.string(),
host_urls: schema.arrayOf(schema.string(), { minSize: 1 }),
is_default: schema.boolean({ defaultValue: false }),
proxy_id: schema.nullable(schema.string()),
}),
};
@ -26,6 +27,7 @@ export const PutFleetServerHostRequestSchema = {
name: schema.maybe(schema.string()),
host_urls: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
is_default: schema.maybe(schema.boolean({ defaultValue: false })),
proxy_id: schema.nullable(schema.string()),
}),
};

View file

@ -12,6 +12,7 @@ export * from './package_policy';
export * from './epm';
export * from './enrollment_api_key';
export * from './fleet_server_policy_config';
export * from './fleet_proxies';
export * from './output';
export * from './preconfiguration';
export * from './settings';

View file

@ -0,0 +1,106 @@
/*
* 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_proxies_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);
const existingId = 'test-default-123';
before(async function () {
await kibanaServer.savedObjects.clean({
types: ['fleet-proxy'],
});
await supertest
.post(`/api/fleet/proxies`)
.set('kbn-xsrf', 'xxxx')
.send({
id: existingId,
name: 'Test 123',
url: 'https://test.fr:3232',
})
.expect(200);
});
after(async () => {
await kibanaServer.savedObjects.cleanStandardList();
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
});
describe('GET /proxies', () => {
it('should list the fleet proxies', async () => {
const { body: res } = await supertest.get(`/api/fleet/proxies`).expect(200);
expect(res.items.length).to.be(1);
});
});
describe('GET /proxies/{itemId}', () => {
it('should return the requested fleet proxy', async () => {
const { body: fleetServerHost } = await supertest
.get(`/api/fleet/proxies/${existingId}`)
.expect(200);
expect(fleetServerHost).to.eql({
item: {
id: 'test-default-123',
name: 'Test 123',
url: 'https://test.fr:3232',
is_preconfigured: false,
},
});
});
it('should return a 404 when retrieving a non existing fleet proxy', async function () {
await supertest.get(`/api/fleet/proxies/idonotexists`).expect(404);
});
});
describe('PUT /proxies/{itemId}', () => {
it('should allow to update an existing fleet proxy', async function () {
await supertest
.put(`/api/fleet/proxies/${existingId}`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Test 123 updated',
})
.expect(200);
const {
body: { item: fleetServerHost },
} = await supertest.get(`/api/fleet/proxies/${existingId}`).expect(200);
expect(fleetServerHost.name).to.eql('Test 123 updated');
});
it('should return a 404 when updating a non existing fleet proxy', async function () {
await supertest
.put(`/api/fleet/proxies/idonotexists`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'new name',
})
.expect(404);
});
});
});
}

View file

@ -61,5 +61,8 @@ export default function ({ loadTestFile, getService }) {
// Fleet server hosts
loadTestFile(require.resolve('./fleet_server_hosts/crud'));
// Fleet proxies
loadTestFile(require.resolve('./fleet_proxies/crud'));
});
}