Skip to content

Commit

Permalink
[Fleet] Enable per policy outputs (elastic#126692)
Browse files Browse the repository at this point in the history
  • Loading branch information
nchaulet authored and lucasfcosta committed Mar 7, 2022
1 parent a3cf64b commit 22841d6
Show file tree
Hide file tree
Showing 19 changed files with 651 additions and 40 deletions.
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/constants/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export const DEFAULT_OUTPUT: NewOutput = {
type: outputType.Elasticsearch,
hosts: [''],
};

export const LICENCE_FOR_PER_POLICY_OUTPUT = 'platinum';
14 changes: 14 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -3838,6 +3838,14 @@
"logs"
]
}
},
"data_output_id": {
"type": "string",
"nullable": true
},
"monitoring_output_id": {
"type": "string",
"nullable": true
}
},
"required": [
Expand Down Expand Up @@ -3994,6 +4002,12 @@
"updated_by": {
"type": "string"
},
"data_output_id": {
"type": "string"
},
"monitoring_output_id": {
"type": "string"
},
"revision": {
"type": "number"
},
Expand Down
10 changes: 10 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2411,6 +2411,12 @@ components:
enum:
- metrics
- logs
data_output_id:
type: string
nullable: true
monitoring_output_id:
type: string
nullable: true
required:
- name
- namespace
Expand Down Expand Up @@ -2510,6 +2516,10 @@ components:
format: date-time
updated_by:
type: string
data_output_id:
type: string
monitoring_output_id:
type: string
revision:
type: number
agents:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ allOf:
format: date-time
updated_by:
type: string
data_output_id:
type: string
monitoring_output_id:
type: string
revision:
type: number
agents:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ properties:
enum:
- metrics
- logs
data_output_id:
type: string
nullable: true
monitoring_output_id:
type: string
nullable: true
required:
- name
- namespace
12 changes: 2 additions & 10 deletions x-pack/plugins/fleet/common/services/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,10 @@ export class LicenseService {
}

public isGoldPlus() {
return (
this.licenseInformation?.isAvailable &&
this.licenseInformation?.isActive &&
this.licenseInformation?.hasAtLeast('gold')
);
return this.hasAtLeast('gold');
}
public isEnterprise() {
return (
this.licenseInformation?.isAvailable &&
this.licenseInformation?.isActive &&
this.licenseInformation?.hasAtLeast('enterprise')
);
return this.hasAtLeast('enterprise');
}
public hasAtLeast(licenseType: LicenseType) {
return (
Expand Down
5 changes: 3 additions & 2 deletions x-pack/plugins/fleet/common/types/models/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ export interface NewAgentPolicy {
monitoring_enabled?: MonitoringType;
unenroll_timeout?: number;
is_preconfigured?: boolean;
data_output_id?: string;
monitoring_output_id?: string;
// Nullable to allow user to reset to default outputs
data_output_id?: string | null;
monitoring_output_id?: string | null;
}

export interface AgentPolicy extends Omit<NewAgentPolicy, 'id'> {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { createFleetTestRendererMock } from '../../../../../../mock';
import type { MockedFleetStartServices } from '../../../../../../mock';
import { useLicense } from '../../../../../../hooks/use_license';
import type { LicenseService } from '../../../../services';

import { useOutputOptions } from './hooks';

jest.mock('../../../../../../hooks/use_license');

const mockedUseLicence = useLicense as jest.MockedFunction<typeof useLicense>;

function defaultHttpClientGetImplementation(path: any) {
if (typeof path !== 'string') {
throw new Error('Invalid request');
}
const err = new Error(`API [GET ${path}] is not MOCKED!`);
// eslint-disable-next-line no-console
console.log(err);
throw err;
}

const mockApiCallsWithOutputs = (http: MockedFleetStartServices['http']) => {
http.get.mockImplementation(async (path) => {
if (typeof path !== 'string') {
throw new Error('Invalid request');
}
if (path === '/api/fleet/outputs') {
return {
data: {
items: [
{
id: 'output1',
name: 'Output 1',
is_default: true,
is_default_monitoring: true,
},
{
id: 'output2',
name: 'Output 2',
is_default: true,
is_default_monitoring: true,
},
{
id: 'output3',
name: 'Output 3',
is_default: true,
is_default_monitoring: true,
},
],
},
};
}

return defaultHttpClientGetImplementation(path);
});
};

describe('useOutputOptions', () => {
it('should generate enabled options if the licence is platinium', async () => {
const testRenderer = createFleetTestRendererMock();
mockedUseLicence.mockReturnValue({
hasAtLeast: () => true,
} as unknown as LicenseService);
mockApiCallsWithOutputs(testRenderer.startServices.http);
const { result, waitForNextUpdate } = testRenderer.renderHook(() => useOutputOptions());
expect(result.current.isLoading).toBeTruthy();

await waitForNextUpdate();
expect(result.current.dataOutputOptions).toMatchInlineSnapshot(`
Array [
Object {
"inputDisplay": "Default (currently Output 1)",
"value": "@@##DEFAULT_OUTPUT_VALUE##@@",
},
Object {
"disabled": false,
"inputDisplay": "Output 1",
"value": "output1",
},
Object {
"disabled": false,
"inputDisplay": "Output 2",
"value": "output2",
},
Object {
"disabled": false,
"inputDisplay": "Output 3",
"value": "output3",
},
]
`);
expect(result.current.monitoringOutputOptions).toMatchInlineSnapshot(`
Array [
Object {
"inputDisplay": "Default (currently Output 1)",
"value": "@@##DEFAULT_OUTPUT_VALUE##@@",
},
Object {
"disabled": false,
"inputDisplay": "Output 1",
"value": "output1",
},
Object {
"disabled": false,
"inputDisplay": "Output 2",
"value": "output2",
},
Object {
"disabled": false,
"inputDisplay": "Output 3",
"value": "output3",
},
]
`);
});

it('should only enable the default options if the licence is not platinium', async () => {
const testRenderer = createFleetTestRendererMock();
mockedUseLicence.mockReturnValue({
hasAtLeast: () => false,
} as unknown as LicenseService);
mockApiCallsWithOutputs(testRenderer.startServices.http);
const { result, waitForNextUpdate } = testRenderer.renderHook(() => useOutputOptions());
expect(result.current.isLoading).toBeTruthy();

await waitForNextUpdate();
expect(result.current.dataOutputOptions).toMatchInlineSnapshot(`
Array [
Object {
"inputDisplay": "Default (currently Output 1)",
"value": "@@##DEFAULT_OUTPUT_VALUE##@@",
},
Object {
"disabled": true,
"inputDisplay": "Output 1",
"value": "output1",
},
Object {
"disabled": true,
"inputDisplay": "Output 2",
"value": "output2",
},
Object {
"disabled": true,
"inputDisplay": "Output 3",
"value": "output3",
},
]
`);
expect(result.current.monitoringOutputOptions).toMatchInlineSnapshot(`
Array [
Object {
"inputDisplay": "Default (currently Output 1)",
"value": "@@##DEFAULT_OUTPUT_VALUE##@@",
},
Object {
"disabled": true,
"inputDisplay": "Output 1",
"value": "output1",
},
Object {
"disabled": true,
"inputDisplay": "Output 2",
"value": "output2",
},
Object {
"disabled": true,
"inputDisplay": "Output 3",
"value": "output3",
},
]
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* 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 { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import type { EuiSuperSelectOption } from '@elastic/eui';

import { useGetOutputs, useLicense } from '../../../../hooks';
import { LICENCE_FOR_PER_POLICY_OUTPUT } from '../../../../../../../common';

// The super select component do not support null or '' as a value
export const DEFAULT_OUTPUT_VALUE = '@@##DEFAULT_OUTPUT_VALUE##@@';

function getDefaultOutput(defaultOutputName?: string) {
return {
inputDisplay: i18n.translate('xpack.fleet.agentPolicy.outputOptions.defaultOutputText', {
defaultMessage: 'Default (currently {defaultOutputName})',
values: { defaultOutputName },
}),
value: DEFAULT_OUTPUT_VALUE,
};
}

export function useOutputOptions() {
const outputsRequest = useGetOutputs();
const licenseService = useLicense();

const isLicenceAllowingPolicyPerOutput = licenseService.hasAtLeast(LICENCE_FOR_PER_POLICY_OUTPUT);

const outputOptions: Array<EuiSuperSelectOption<string>> = useMemo(() => {
if (outputsRequest.isLoading || !outputsRequest.data) {
return [];
}

return outputsRequest.data.items.map((item) => ({
value: item.id,
inputDisplay: item.name,
disabled: !isLicenceAllowingPolicyPerOutput,
}));
}, [outputsRequest, isLicenceAllowingPolicyPerOutput]);

const dataOutputOptions = useMemo(() => {
if (outputsRequest.isLoading || !outputsRequest.data) {
return [];
}

const defaultOutputName = outputsRequest.data.items.find((item) => item.is_default)?.name;
return [getDefaultOutput(defaultOutputName), ...outputOptions];
}, [outputsRequest, outputOptions]);

const monitoringOutputOptions = useMemo(() => {
if (outputsRequest.isLoading || !outputsRequest.data) {
return [];
}

const defaultOutputName = outputsRequest.data.items.find(
(item) => item.is_default_monitoring
)?.name;
return [getDefaultOutput(defaultOutputName), ...outputOptions];
}, [outputsRequest, outputOptions]);

return useMemo(
() => ({
dataOutputOptions,
monitoringOutputOptions,
isLoading: outputsRequest.isLoading,
}),
[dataOutputOptions, monitoringOutputOptions, outputsRequest.isLoading]
);
}
Loading

0 comments on commit 22841d6

Please sign in to comment.