[BeatsCM] Beat config status added and other bug fixes (#24246)

* fix spelling

* Adds config status feature, fixes a number of other bugs

* fix typos and move beat status to OK, ERROR, and UNKNOWN

* tweak security again

* Fix typo

* remove invalid expect

* Removed cruft

* add missing field to template, renamed said field

* tweaks

* Update x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js

Co-Authored-By: mattapperson <me@mattapperson.com>
This commit is contained in:
Matt Apperson 2018-10-22 09:51:42 -04:00 committed by GitHub
parent ee334c4555
commit a353979ebb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 346 additions and 162 deletions

View file

@ -57,6 +57,7 @@ export interface ReturnedConfigurationBlock
export interface CMBeat {
id: string;
config_status: 'OK' | 'UNKNOWN' | 'ERROR';
enrollment_token: string;
active: boolean;
access_token: string;
@ -66,7 +67,7 @@ export interface CMBeat {
host_ip: string;
host_name: string;
ephemeral_id?: string;
last_updated?: string;
last_checkin?: Date;
event_rate?: string;
local_configuration_yml?: string;
tags?: string[];

View file

@ -78,6 +78,7 @@ class FieldText extends Component<
className,
disabled,
helpText,
placeholder,
} = this.props;
const { allowError } = this.state;
@ -94,6 +95,7 @@ class FieldText extends Component<
<EuiFieldText
id={id}
name={name}
placeholder={placeholder}
value={getValue() || ''}
isInvalid={!disabled && error}
onChange={this.handleChange}

View file

@ -80,6 +80,7 @@ class MultiFieldText extends Component<
className,
disabled,
helpText,
placeholder,
} = this.props;
const { allowError } = this.state;
@ -98,6 +99,7 @@ class MultiFieldText extends Component<
name={name}
value={getValue() ? getValue().join('\n') : ''}
isInvalid={!disabled && error}
placeholder={placeholder}
onChange={this.handleChange}
onBlur={this.handleBlur}
fullWidth={fullWidth}

View file

@ -61,10 +61,10 @@ export const tagConfigAssignmentOptions: AssignmentControlSchema[] = [
type: AssignmentComponentType.Action,
danger: true,
grow: false,
name: 'Detach beat(s)',
name: 'Remove tag(s)',
showWarning: true,
warningHeading: 'Detatch beats',
warningMessage: 'This will detatch the selected beat(s) from this tag.',
warningHeading: 'Remove tag(s)',
warningMessage: 'Remove the tag from the selected beat(s)?',
action: AssignmentActionType.Delete,
},
];

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiToolTip, IconColor } from '@elastic/eui';
import { first, sortBy, sortByOrder, uniq } from 'lodash';
import moment from 'moment';
import React from 'react';
@ -83,12 +83,41 @@ export const BeatsTableType: TableType = {
},
{
// TODO: update to use actual metadata field
field: 'event_rate',
name: 'Event rate',
sortable: true,
field: 'config_status',
name: 'Config Status',
render: (value: string, beat: CMPopulatedBeat) => {
let color: IconColor = 'success';
let statusText = 'OK';
let tooltipText = 'Beat successfully applied latest config';
switch (beat.config_status) {
case 'UNKNOWN':
color = 'subdued';
statusText = 'Offline';
if (moment().diff(beat.last_checkin, 'minutes') >= 10) {
tooltipText = 'This Beat has not connected to kibana in over 10min';
} else {
tooltipText = 'This Beat has not yet been started.';
}
break;
case 'ERROR':
color = 'danger';
statusText = 'Error';
tooltipText = 'Please check the logs of this Beat for error details';
break;
}
return (
<EuiFlexGroup wrap responsive={true} gutterSize="xs">
<EuiToolTip content={tooltipText}>
<EuiHealth color={color}>{statusText}</EuiHealth>
</EuiToolTip>
</EuiFlexGroup>
);
},
sortable: false,
},
{
// TODO: update to use actual metadata field
field: 'full_tags',
name: 'Last config update',
render: (tags: BeatTag[]) =>

View file

@ -74,8 +74,8 @@ interface ComponentProps {
values: ConfigurationBlock;
schema: YamlConfigSchema[];
id: string;
onSubmit?: (modal: any) => any;
canSubmit(canIt: boolean): any;
onSubmit(modal: any): any;
}
export class ConfigForm extends React.Component<ComponentProps, any> {
@ -101,11 +101,14 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
this.props.canSubmit(false);
};
public submit = () => {
if (this.form.current) {
if (this.form.current && this.props.onSubmit) {
this.form.current.click();
}
};
public onValidSubmit = <ModelType extends any>(model: ModelType) => {
if (!this.props.onSubmit) {
return;
}
const processed = JSON.parse(JSON.stringify(model), (key, value) => {
return _.isObject(value) && !_.isArray(value)
? _.mapKeys(value, (v, k: string) => {
@ -139,9 +142,15 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
<FormsyEuiFieldText
key={schema.id}
id={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
defaultValue={get(
this.props,
`values.configs[0].${schema.id}`,
schema.defaultValue
)}
name={schema.id}
disabled={!this.props.onSubmit}
helpText={schema.ui.helpText}
placeholder={schema.ui.placeholder}
label={schema.ui.label}
validations={schema.validations}
validationError={schema.error}
@ -153,8 +162,14 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
<FormsyEuiPasswordText
key={schema.id}
id={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
disabled={!this.props.onSubmit}
defaultValue={get(
this.props,
`values.configs[0].${schema.id}`,
schema.defaultValue
)}
name={schema.id}
placeholder={schema.ui.placeholder}
helpText={schema.ui.helpText}
label={schema.ui.label}
validations={schema.validations}
@ -167,8 +182,14 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
<FormsyEuiMultiFieldText
key={schema.id}
id={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
disabled={!this.props.onSubmit}
defaultValue={get(
this.props,
`values.configs[0].${schema.id}`,
schema.defaultValue
)}
name={schema.id}
placeholder={schema.ui.placeholder}
helpText={schema.ui.helpText}
label={schema.ui.label}
validations={schema.validations}
@ -182,7 +203,12 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
key={schema.id}
id={schema.id}
name={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
disabled={!this.props.onSubmit}
defaultValue={get(
this.props,
`values.configs[0].${schema.id}`,
schema.defaultValue
)}
helpText={schema.ui.helpText}
label={schema.ui.label}
options={[{ value: '', text: 'Please Select An Option' }].concat(
@ -198,8 +224,13 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
<FormsyEuiCodeEditor
key={`${schema.id}-${this.props.id}`}
mode="yaml"
disabled={!this.props.onSubmit}
id={schema.id}
defaultValue={get(this.props, `values.configs[0].${schema.id}`)}
defaultValue={get(
this.props,
`values.configs[0].${schema.id}`,
schema.defaultValue
)}
name={schema.id}
helpText={schema.ui.helpText}
label={schema.ui.label}
@ -211,12 +242,14 @@ export class ConfigForm extends React.Component<ComponentProps, any> {
);
}
})}
<button
type="submit"
style={{ display: 'none' }}
disabled={!this.state.canSubmit}
ref={this.form}
/>
{this.props.onSubmit && (
<button
type="submit"
style={{ display: 'none' }}
disabled={!this.state.canSubmit}
ref={this.form}
/>
)}
</Formsy>
</div>
);

View file

@ -35,7 +35,7 @@ import { ConfigForm } from './config_form';
interface ComponentProps {
configBlock?: ConfigurationBlock;
onClose(): any;
onSave(config: ConfigurationBlock): any;
onSave?(config: ConfigurationBlock): any;
}
export class ConfigView extends React.Component<ComponentProps, any> {
@ -66,11 +66,17 @@ export class ConfigView extends React.Component<ComponentProps, any> {
<EuiFlyout onClose={this.props.onClose}>
<EuiFlyoutHeader>
<EuiTitle size="m">
<h2>{this.editMode ? 'Edit Configuration' : 'Add Configuration'}</h2>
<h2>
{this.editMode
? this.props.onSave
? 'Edit configuration'
: 'View configuration'
: 'Add configuration'}
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFormRow label="Configuration type">
<EuiFormRow label="Type">
<EuiSelect
options={supportedConfigs}
value={this.state.configBlock.type}
@ -78,31 +84,37 @@ export class ConfigView extends React.Component<ComponentProps, any> {
onChange={this.onValueChange('type')}
/>
</EuiFormRow>
<EuiFormRow label="Configuration description">
<EuiFormRow label="Description">
<EuiFieldText
value={this.state.configBlock.description}
disabled={!this.props.onSave}
onChange={this.onValueChange('description')}
placeholder="Description (optional)"
/>
</EuiFormRow>
<h3>
Config for&nbsp;
{
(supportedConfigs.find(config => this.state.configBlock.type === config.value) as any)
.text
}
&nbsp;configuration
</h3>
<EuiHorizontalRule />
<ConfigForm
// tslint:disable-next-line:no-console
onSubmit={data => {
this.props.onSave({
...this.state.configBlock,
configs: [data],
});
this.props.onClose();
}}
onSubmit={
this.props.onSave
? data => {
if (this.props.onSave) {
this.props.onSave({
...this.state.configBlock,
configs: [data],
});
}
this.props.onClose();
}
: undefined
}
canSubmit={canIt => this.setState({ valid: canIt })}
ref={this.form}
values={this.state.configBlock}
@ -123,19 +135,21 @@ export class ConfigView extends React.Component<ComponentProps, any> {
Close
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
disabled={!this.state.valid}
fill
onClick={() => {
if (this.form.current) {
this.form.current.submit();
}
}}
>
Save
</EuiButton>
</EuiFlexItem>
{this.props.onSave && (
<EuiFlexItem grow={false}>
<EuiButton
disabled={!this.state.valid}
fill
onClick={() => {
if (this.form.current) {
this.form.current.submit();
}
}}
>
Save
</EuiButton>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>

View file

@ -65,11 +65,7 @@ export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
<h3>Tag details</h3>
</EuiTitle>
<EuiText color="subdued">
<p>
Tags will apply the configurations below to all beats assigned this tag.
<br />
The tag type defines the options available.
</p>
<p>Tags will apply the configurations below to all beats assigned this tag.</p>
</EuiText>
<div>
<TagBadge tag={{ color: tag.color || '#FF0', id: tag.id }} />
@ -113,9 +109,8 @@ export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
</EuiTitle>
<EuiText color="subdued">
<p>
Tags can contain multiple configurations. These configurations can repeat or mix
types as necessary. For example, you may utilize three metricbeat configurations
alongside one input and filebeat configuration.
Tags can have configurations for different types of Beats. For example, a tag can
have two Metricbeat configurations and one Filebeat input configuration.
</p>
</EuiText>
</EuiFlexItem>
@ -156,7 +151,7 @@ export class TagEdit extends React.PureComponent<TagEditProps, TagEditState> {
<EuiHorizontalRule />
<EuiTitle size="xs">
<h3>Attached Beats</h3>
<h3>Beats assigned this tag</h3>
</EuiTitle>
<Table
assignmentOptions={{

View file

@ -12,6 +12,8 @@ const filebeatInputConfig: YamlConfigSchema[] = [
ui: {
label: 'Paths',
type: 'multi-input',
helpText: 'Put each of the paths on a seperate line',
placeholder: `first/path/to/file.json second/path/to/otherfile.json`,
},
validations: 'isPaths',
error: 'One file path per line',
@ -22,9 +24,10 @@ const filebeatInputConfig: YamlConfigSchema[] = [
ui: {
label: 'Other Config',
type: 'code',
helpText: 'Use YAML format to specify other settings for the Filebeat Input',
},
validations: 'isYaml',
error: 'Config entered must be in valid YAML format',
error: 'Use valid YAML format',
},
];
@ -113,9 +116,10 @@ const filebeatModuleConfig: YamlConfigSchema[] = [
ui: {
label: 'Other Config',
type: 'code',
helpText: 'Use YAML format to specify other settings for the Filebeat Module',
},
validations: 'isYaml',
error: 'Config entered must be in valid YAML format',
error: 'Use valid YAML format',
},
];
@ -276,6 +280,8 @@ const metricbeatModuleConfig: YamlConfigSchema[] = [
ui: {
label: 'Hosts',
type: 'multi-input',
helpText: 'Put each of the paths on a seperate line',
placeholder: `somehost.local otherhost.local`,
},
validations: 'isHosts',
error: 'One file host per line',
@ -297,9 +303,10 @@ const metricbeatModuleConfig: YamlConfigSchema[] = [
ui: {
label: 'Other Config',
type: 'code',
helpText: 'Use YAML format to specify other settings for the Metricbeat Module',
},
validations: 'isYaml',
error: 'Config entered must be in valid YAML format',
error: 'Use valid YAML format',
},
];

View file

@ -54,9 +54,7 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
};
public render = (component: React.ReactElement<any>) => {
if (this.hadValidLicense() && this.securityEnabled()) {
this.rootComponent = component;
}
this.rootComponent = component;
};
public hadValidLicense() {
@ -75,10 +73,10 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
}
public registerManagementSection(pluginId: string, displayName: string, basePath: string) {
if (this.hadValidLicense() && this.securityEnabled()) {
this.register(this.uiModule);
this.register(this.uiModule);
this.hookAngular(() => {
this.hookAngular(() => {
if (this.hadValidLicense() && this.securityEnabled()) {
const registerSection = () =>
this.management.register(pluginId, {
display: 'Beats', // TODO these need to be config options not hard coded in the adapter
@ -95,8 +93,8 @@ export class KibanaFrameworkAdapter implements FrameworkAdapter {
order: 30,
url: `#${basePath}`,
});
});
}
}
});
}
private manageAngularLifecycle($scope: any, $route: any, elem: any) {

View file

@ -29,6 +29,7 @@ export interface YamlConfigSchema {
label: string;
type: 'input' | 'multi-input' | 'select' | 'code' | 'password';
helpText?: string;
placeholder?: string;
transform?: 'removed';
};
options?: Array<{ value: string; text: string }>;

View file

@ -16,88 +16,119 @@ import {
import { flatten, get } from 'lodash';
import React from 'react';
import { TABLE_CONFIG } from '../../../common/constants';
import { BeatTag, CMPopulatedBeat } from '../../../common/domain_types';
import { BeatTag, CMPopulatedBeat, ConfigurationBlock } from '../../../common/domain_types';
import { ConnectedLink } from '../../components/connected_link';
import { TagBadge } from '../../components/tag';
import { ConfigView } from '../../components/tag/config_view/index';
import { supportedConfigs } from '../../config_schemas';
interface BeatDetailPageProps {
interface PageProps {
beat: CMPopulatedBeat | undefined;
}
export const BeatDetailPage = (props: BeatDetailPageProps) => {
const { beat } = props;
if (!beat) {
return <div>Beat not found</div>;
}
const configurationBlocks = flatten(
beat.full_tags.map((tag: BeatTag) => {
return tag.configuration_blocks.map(configuration => ({
// @ts-ignore one of the types on ConfigurationBlock doesn't define a "module" property
module: configuration.configs[0].module || null,
tagId: tag.id,
tagColor: tag.color,
...beat,
...configuration,
displayValue: get(
supportedConfigs.find(config => config.value === configuration.type),
'text',
null
),
}));
})
);
interface PageState {
selectedConfig: ConfigurationBlock | null;
}
const columns = [
{
field: 'displayValue',
name: 'Type',
sortable: true,
render: (value: string | null, configuration: any) => (
<EuiLink href="#">{value || configuration.type}</EuiLink>
),
},
{
field: 'module',
name: 'Module',
sortable: true,
},
{
field: 'description',
name: 'Description',
sortable: true,
},
{
field: 'tagId',
name: 'Tag',
render: (id: string, block: any) => (
<ConnectedLink path={`/tag/edit/${id}`}>
<TagBadge
maxIdRenderSize={TABLE_CONFIG.TRUNCATE_TAG_LENGTH_SMALL}
tag={{ color: block.tagColor, id }}
export class BeatDetailPage extends React.PureComponent<PageProps, PageState> {
constructor(props: PageProps) {
super(props);
this.state = {
selectedConfig: null,
};
}
public render() {
const props = this.props;
const { beat } = props;
if (!beat) {
return <div>Beat not found</div>;
}
const configurationBlocks = flatten(
beat.full_tags.map((tag: BeatTag) => {
return tag.configuration_blocks.map(configuration => ({
// @ts-ignore one of the types on ConfigurationBlock doesn't define a "module" property
module: configuration.configs[0].module || null,
tagId: tag.id,
tagColor: tag.color,
...beat,
...configuration,
displayValue: get(
supportedConfigs.find(config => config.value === configuration.type),
'text',
null
),
}));
})
);
const columns = [
{
field: 'displayValue',
name: 'Type',
sortable: true,
render: (value: string | null, configuration: any) => (
<EuiLink
onClick={() => {
this.setState({
selectedConfig: configuration,
});
}}
>
{value || configuration.type}
</EuiLink>
),
},
{
field: 'module',
name: 'Module',
sortable: true,
},
{
field: 'description',
name: 'Description',
sortable: true,
},
{
field: 'tagId',
name: 'Tag',
render: (id: string, block: any) => (
<ConnectedLink path={`/tag/edit/${id}`}>
<TagBadge
maxIdRenderSize={TABLE_CONFIG.TRUNCATE_TAG_LENGTH_SMALL}
tag={{ color: block.tagColor, id }}
/>
</ConnectedLink>
),
sortable: true,
},
];
return (
<React.Fragment>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h4>Configurations</h4>
</EuiTitle>
<EuiText size="s">
<p>
You can have multiple configurations applied to an individual tag. These
configurations can repeat or mix types as necessary. For example, you may utilize
three metricbeat configurations alongside one input and filebeat configuration.
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiInMemoryTable columns={columns} items={configurationBlocks} />
</EuiFlexItem>
</EuiFlexGroup>
{this.state.selectedConfig && (
<ConfigView
configBlock={this.state.selectedConfig}
onClose={() => this.setState({ selectedConfig: null })}
/>
</ConnectedLink>
),
sortable: true,
},
];
return (
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h4>Configurations</h4>
</EuiTitle>
<EuiText size="s">
<p>
You can have multiple configurations applied to an individual tag. These configurations
can repeat or mix types as necessary. For example, you may utilize three metricbeat
configurations alongside one input and filebeat configuration.
</p>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiInMemoryTable columns={columns} items={configurationBlocks} />
</EuiFlexItem>
</EuiFlexGroup>
);
};
)}
</React.Fragment>
);
}
}

View file

@ -117,7 +117,7 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>Select your beat type:</h3>
<h3>Beat type:</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
@ -140,7 +140,7 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>Select your platform:</h3>
<h3>Platform:</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
@ -175,7 +175,10 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>Run the following command to enroll your beat</h3>
<h3>
On the host where your {capitalize(this.state.beatType)} is installed,
run:
</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
@ -200,7 +203,7 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h3>Waiting for enroll command to be run...</h3>
<h3>Waiting for {capitalize(this.state.beatType)} to enroll...</h3>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
@ -214,8 +217,8 @@ export class EnrollBeat extends React.Component<BeatsProps, any> {
</React.Fragment>
)}
{this.state.enrolledBeat && (
<EuiModalBody style={{ textAlign: 'center' }}>
A Beat was enrolled with the following data:
<EuiModalBody>
The Beat is now enrolled in central management:
<br />
<br />
<br />

View file

@ -5,7 +5,6 @@
*/
import {
EuiCode,
// @ts-ignore
EuiTab,
// @ts-ignore
@ -78,7 +77,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
const tabs = [
{
id: '/overview/beats',
name: 'Beats List',
name: 'Enrolled Beats',
disabled: false,
},
// {
@ -88,7 +87,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
// },
{
id: '/overview/tags',
name: 'Configuration Tags',
name: 'Configuration tags',
disabled: false,
},
];
@ -102,7 +101,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
},
{
id: '/overview/initial/tag',
name: 'Create Configuration Tag',
name: 'Create tag',
disabled: false,
page: CreateTagPageFragment,
},
@ -117,7 +116,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
if (this.props.location.pathname === '/overview/initial/help') {
return (
<NoDataLayout
title="Welcome to Beats Central Management"
title="Beats central management"
actionSection={
<ConnectedLink path="/overview/initial/beats">
<EuiButton color="primary" fill>
@ -126,10 +125,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
</ConnectedLink>
}
>
<p>
You dont have any Beat configured to use Central Management, click on{' '}
<EuiCode>Enroll Beat</EuiCode> to add one now.
</p>
<p>Manage your configurations in a central location.</p>
</NoDataLayout>
);
}
@ -137,7 +133,7 @@ class MainPagesComponent extends React.PureComponent<MainPagesProps, MainPagesSt
if (this.props.location.pathname.includes('/overview/initial')) {
return (
<WalkthroughLayout
title="Get Started With Beats Centeral Management"
title="Get started with Beats central management"
walkthroughSteps={walkthroughSteps}
goTo={this.props.goTo}
activePath={this.props.location.pathname}

View file

@ -36,6 +36,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter {
}
public async insert(user: FrameworkUser, beat: CMBeat) {
beat.config_status = 'UNKNOWN';
const body = {
beat,
type: 'beat',
@ -230,6 +231,7 @@ export class ElasticsearchBeatsAdapter implements CMBeatsAdapter {
refresh: 'wait_for',
type: '_doc',
});
// console.log(response.items[0].update.error);
return _get<any>(response, 'items', []).map((item: any, resultIdx: any) => ({
idxInRequest: assignments[resultIdx].idxInRequest,
result: item.update.result,

View file

@ -39,6 +39,7 @@ describe('Beats Domain Lib', () => {
beatsDB = [
{
access_token: '9a6c99ae0fd84b068819701169cd8a4b',
config_status: 'OK',
active: true,
enrollment_token: '23423423423',
host_ip: '1.2.3.4',
@ -49,6 +50,7 @@ describe('Beats Domain Lib', () => {
{
access_token: '188255eb560a4448b72656c5e99cae6f',
active: true,
config_status: 'OK',
enrollment_token: 'reertrte',
host_ip: '22.33.11.44',
host_name: 'baz.bar.com',
@ -59,6 +61,7 @@ describe('Beats Domain Lib', () => {
access_token: '93c4a4dd08564c189a7ec4e4f046b975',
active: true,
enrollment_token: '23s423423423',
config_status: 'OK',
host_ip: '1.2.3.4',
host_name: 'foo.bar.com',
id: 'foo',
@ -70,6 +73,7 @@ describe('Beats Domain Lib', () => {
access_token: '3c4a4dd08564c189a7ec4e4f046b9759',
enrollment_token: 'gdfsgdf',
active: true,
config_status: 'OK',
host_ip: '11.22.33.44',
host_name: 'foo.com',
id: 'bar',

View file

@ -13,7 +13,7 @@ const internalUser: FrameworkInternalUser = { kind: 'internal' };
describe('Beats Domain Lib', () => {
let libs: CMServerLibs;
let beatsDB: CMBeat[] = [];
let beatsDB: Array<Partial<CMBeat>> = [];
let tagsDB: BeatTag[] = [];
describe('remove_tags_from_beats', () => {

View file

@ -74,6 +74,7 @@ describe('Beats Domain lib', () => {
it('should return an invalid message if token validation fails', async () => {
const beatToFind: CMBeat = {
id: beatId,
config_status: 'OK',
enrollment_token: '',
active: true,
access_token: token.token || '',
@ -93,6 +94,7 @@ describe('Beats Domain lib', () => {
it('should update the beat when a valid token is provided', async () => {
const beatToFind: CMBeat = {
id: beatId,
config_status: 'OK',
enrollment_token: '',
active: true,
access_token: token.token || '',

View file

@ -5,7 +5,7 @@
*/
import Joi from 'joi';
import { omit } from 'lodash';
import { BeatTag, ConfigurationBlock } from '../../../common/domain_types';
import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types';
import { CMServerLibs } from '../../lib/lib';
import { wrapEsError } from '../../utils/error_wrappers';
import { ReturnedConfigurationBlock } from './../../../common/domain_types';
@ -18,6 +18,9 @@ export const createGetBeatConfigurationRoute = (libs: CMServerLibs) => ({
headers: Joi.object({
'kbn-beats-access-token': Joi.string().required(),
}).options({ allowUnknown: true }),
query: Joi.object({
validSetting: Joi.boolean().default(true),
}),
},
auth: false,
},
@ -38,6 +41,16 @@ export const createGetBeatConfigurationRoute = (libs: CMServerLibs) => ({
return reply({ message: 'Invalid access token' }).code(401);
}
let newStatus: CMBeat['config_status'] = 'OK';
if (!request.query.validSetting) {
newStatus = 'ERROR';
}
await libs.beats.update(libs.framework.internalUser, beat.id, {
config_status: newStatus,
last_checkin: new Date(),
});
tags = await libs.tags.getTagsWithIds(libs.framework.internalUser, beat.tags || []);
} catch (err) {
return reply(wrapEsError(err));

View file

@ -58,9 +58,15 @@
"id": {
"type": "keyword"
},
"config_status": {
"type": "keyword"
},
"active": {
"type": "boolean"
},
"last_checkin": {
"type": "date"
},
"enrollment_token": {
"type": "keyword"
},

View file

@ -69,6 +69,7 @@ export default function ({ getService }) {
expect(esResponse._source.beat).to.have.property('verified_on');
expect(esResponse._source.beat).to.have.property('host_ip');
expect(esResponse._source.beat.config_status).to.eql('UNKNOWN');
});
it('should contain an access token in the response', async () => {

View file

@ -5,10 +5,12 @@
*/
import expect from 'expect.js';
import { ES_INDEX_NAME, ES_TYPE_NAME } from './constants';
export default function ({ getService }) {
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const es = getService('es');
describe('get_beat_configuration', () => {
const archive = 'beats/list';
@ -47,5 +49,47 @@ export default function ({ getService }) {
'node.namespace': 'node',
});
});
it('A config check with a validSetting of false should set an error status on the beat', async () => {
// 1. Initial request
await supertest
.get('/api/beats/agent/foo/configuration?validSetting=true')
.set(
'kbn-beats-access-token',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' +
'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' +
'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI'
)
.expect(200);
let esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `beat:foo`,
});
let beat = esResponse._source.beat;
expect(beat.config_status).to.be('OK');
// 2. Beat polls reporting an error
await supertest
.get('/api/beats/agent/foo/configuration?validSetting=false')
.set(
'kbn-beats-access-token',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' +
'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' +
'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI'
)
.expect(200);
esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `beat:foo`,
});
beat = esResponse._source.beat;
expect(beat.config_status).to.be('ERROR');
});
});
}