Commit 48c5ecd0 authored by Zamir Martins Filho's avatar Zamir Martins Filho Committed by Paul Slaughter

Update fluentd ui controls in cluster app page

Add a new checkbox option which controls
if cilium logs will be considered by
Fluentd.

https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29511
parent 4095185d
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
INGRESS_DOMAIN_SUFFIX, INGRESS_DOMAIN_SUFFIX,
CROSSPLANE, CROSSPLANE,
KNATIVE, KNATIVE,
FLUENTD,
} from './constants'; } from './constants';
import ClustersService from './services/clusters_service'; import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store'; import ClustersStore from './stores/clusters_store';
...@@ -510,10 +511,10 @@ export default class Clusters { ...@@ -510,10 +511,10 @@ export default class Clusters {
}); });
} }
setFluentdSettings({ id: appId, port, protocol, host }) { setFluentdSettings(settings = {}) {
this.store.updateAppProperty(appId, 'port', port); Object.entries(settings).forEach(([key, value]) => {
this.store.updateAppProperty(appId, 'protocol', protocol); this.store.updateAppProperty(FLUENTD, key, value);
this.store.updateAppProperty(appId, 'host', host); });
} }
toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) { toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
......
...@@ -689,6 +689,8 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity ...@@ -689,6 +689,8 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
host: applications.fluentd.host, host: applications.fluentd.host,
port: applications.fluentd.port, port: applications.fluentd.port,
protocol: applications.fluentd.protocol, protocol: applications.fluentd.protocol,
waf_log_enabled: applications.fluentd.wafLogEnabled,
cilium_log_enabled: applications.fluentd.ciliumLogEnabled,
}" }"
:uninstallable="applications.fluentd.uninstallable" :uninstallable="applications.fluentd.uninstallable"
:uninstall-successful="applications.fluentd.uninstallSuccessful" :uninstall-successful="applications.fluentd.uninstallSuccessful"
...@@ -701,12 +703,20 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity ...@@ -701,12 +703,20 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
<p> <p>
{{ {{
s__( s__(
`ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. Export Web Application Firewall logs to your favorite SIEM.`, `ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed.`,
) )
}} }}
</p> </p>
<fluentd-output-settings :fluentd="applications.fluentd" /> <fluentd-output-settings
:port="applications.fluentd.port"
:protocol="applications.fluentd.protocol"
:host="applications.fluentd.host"
:waf-log-enabled="applications.fluentd.wafLogEnabled"
:cilium-log-enabled="applications.fluentd.ciliumLogEnabled"
:status="applications.fluentd.status"
:update-failed="applications.fluentd.updateFailed"
/>
</div> </div>
</application-row> </application-row>
</div> </div>
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants'; import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
import { GlAlert, GlDeprecatedButton, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import {
GlAlert,
GlDeprecatedButton,
GlDropdown,
GlDropdownItem,
GlFormCheckbox,
} from '@gitlab/ui';
import eventHub from '~/clusters/event_hub'; import eventHub from '~/clusters/event_hub';
import { mapValues } from 'lodash';
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS; const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
...@@ -12,24 +19,62 @@ export default { ...@@ -12,24 +19,62 @@ export default {
GlDeprecatedButton, GlDeprecatedButton,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlFormCheckbox,
}, },
props: { props: {
fluentd: {
type: Object,
required: true,
},
protocols: { protocols: {
type: Array, type: Array,
required: false, required: false,
default: () => ['TCP', 'UDP'], default: () => ['TCP', 'UDP'],
}, },
status: {
type: String,
required: false,
default: '',
},
updateFailed: {
type: Boolean,
required: false,
},
protocol: {
type: String,
required: false,
default: () => __('Protocol'),
},
port: {
type: Number,
required: false,
default: 514,
},
host: {
type: String,
required: false,
default: '',
},
wafLogEnabled: {
type: Boolean,
required: false,
},
ciliumLogEnabled: {
type: Boolean,
required: false,
},
}, },
data: () => ({
currentServerSideSettings: {
host: null,
port: null,
protocol: null,
wafLogEnabled: null,
ciliumLogEnabled: null,
},
}),
computed: { computed: {
isSaving() { isSaving() {
return [UPDATING].includes(this.fluentd.status); return [UPDATING].includes(this.status);
}, },
saveButtonDisabled() { saveButtonDisabled() {
return [UNINSTALLING, UPDATING, INSTALLING].includes(this.fluentd.status); return [UNINSTALLING, UPDATING, INSTALLING].includes(this.status);
}, },
saveButtonLabel() { saveButtonLabel() {
return this.isSaving ? __('Saving') : __('Save changes'); return this.isSaving ? __('Saving') : __('Save changes');
...@@ -41,32 +86,23 @@ export default { ...@@ -41,32 +86,23 @@ export default {
* neither getting installed nor updated. * neither getting installed nor updated.
*/ */
showButtons() { showButtons() {
return ( return this.isSaving || (this.changedByUser && [INSTALLED, UPDATED].includes(this.status));
this.isSaving ||
(this.fluentd.isEditingSettings && [INSTALLED, UPDATED].includes(this.fluentd.status))
);
}, },
protocolName() { protocolName() {
if (this.fluentd.protocol !== null && this.fluentd.protocol !== undefined) { if (this.protocol) {
return this.fluentd.protocol.toUpperCase(); return this.protocol.toUpperCase();
} }
return __('Protocol'); return __('Protocol');
}, },
fluentdPort: { changedByUser() {
get() { return Object.entries(this.currentServerSideSettings).some(([key, value]) => {
return this.fluentd.port; return value !== null && value !== this[key];
}, });
set(port) { },
this.setFluentSettings({ port }); },
}, watch: {
}, status() {
fluentdHost: { this.resetCurrentServerSideSettings();
get() {
return this.fluentd.host;
},
set(host) {
this.setFluentSettings({ host });
},
}, },
}, },
methods: { methods: {
...@@ -74,38 +110,64 @@ export default { ...@@ -74,38 +110,64 @@ export default {
eventHub.$emit('updateApplication', { eventHub.$emit('updateApplication', {
id: FLUENTD, id: FLUENTD,
params: { params: {
port: this.fluentd.port, port: this.port,
protocol: this.fluentd.protocol, protocol: this.protocol,
host: this.fluentd.host, host: this.host,
waf_log_enabled: this.wafLogEnabled,
cilium_log_enabled: this.ciliumLogEnabled,
}, },
}); });
this.resetStatus(); },
resetCurrentServerSideSettings() {
this.currentServerSideSettings = mapValues(this.currentServerSideSettings, () => {
return null;
});
}, },
resetStatus() { resetStatus() {
this.fluentd.isEditingSettings = false; const newSettings = mapValues(this.currentServerSideSettings, (value, key) => {
return value === null ? this[key] : value;
});
eventHub.$emit('setFluentdSettings', {
...newSettings,
isEditingSettings: false,
});
}, },
selectProtocol(protocol) { updateCurrentServerSideSettings(settings) {
this.setFluentSettings({ protocol }); Object.keys(settings).forEach(key => {
if (this.currentServerSideSettings[key] === null) {
this.currentServerSideSettings[key] = this[key];
}
});
}, },
setFluentSettings({ port, protocol, host }) { setFluentdSettings(settings) {
this.fluentd.isEditingSettings = true; this.updateCurrentServerSideSettings(settings);
const newPort = port !== undefined ? port : this.fluentd.port;
const newProtocol = protocol !== undefined ? protocol : this.fluentd.protocol;
const newHost = host !== undefined ? host : this.fluentd.host;
eventHub.$emit('setFluentdSettings', { eventHub.$emit('setFluentdSettings', {
id: FLUENTD, ...settings,
port: newPort, isEditingSettings: true,
protocol: newProtocol,
host: newHost,
}); });
}, },
selectProtocol(protocol) {
this.setFluentdSettings({ protocol });
},
hostChanged(host) {
this.setFluentdSettings({ host });
},
portChanged(port) {
this.setFluentdSettings({ port: Number(port) });
},
wafLogChanged(wafLogEnabled) {
this.setFluentdSettings({ wafLogEnabled });
},
ciliumLogChanged(ciliumLogEnabled) {
this.setFluentdSettings({ ciliumLogEnabled });
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<gl-alert v-if="fluentd.updateFailed" class="mb-3" variant="danger" :dismissible="false"> <gl-alert v-if="updateFailed" class="mb-3" variant="danger" :dismissible="false">
{{ {{
s__( s__(
'ClusterIntegration|Something went wrong while trying to save your settings. Please try again.', 'ClusterIntegration|Something went wrong while trying to save your settings. Please try again.',
...@@ -117,13 +179,25 @@ export default { ...@@ -117,13 +179,25 @@ export default {
<label for="fluentd-host"> <label for="fluentd-host">
<strong>{{ s__('ClusterIntegration|SIEM Hostname') }}</strong> <strong>{{ s__('ClusterIntegration|SIEM Hostname') }}</strong>
</label> </label>
<input id="fluentd-host" v-model="fluentdHost" type="text" class="form-control" /> <input
id="fluentd-host"
:value="host"
type="text"
class="form-control"
@input="hostChanged($event.target.value)"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="fluentd-port"> <label for="fluentd-port">
<strong>{{ s__('ClusterIntegration|SIEM Port') }}</strong> <strong>{{ s__('ClusterIntegration|SIEM Port') }}</strong>
</label> </label>
<input id="fluentd-port" v-model="fluentdPort" type="text" class="form-control" /> <input
id="fluentd-port"
:value="port"
type="number"
class="form-control"
@input="portChanged($event.target.value)"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="fluentd-protocol"> <label for="fluentd-protocol">
...@@ -133,12 +207,20 @@ export default { ...@@ -133,12 +207,20 @@ export default {
<gl-dropdown-item <gl-dropdown-item
v-for="(value, index) in protocols" v-for="(value, index) in protocols"
:key="index" :key="index"
@click="selectProtocol(value)" @click="selectProtocol(value.toLowerCase())"
> >
{{ value }} {{ value }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</div> </div>
<div class="form-group flex flex-wrap">
<gl-form-checkbox :checked="wafLogEnabled" @input="wafLogChanged">
<strong>{{ s__('ClusterIntegration|Send ModSecurity Logs') }}</strong>
</gl-form-checkbox>
<gl-form-checkbox :checked="ciliumLogEnabled" @input="ciliumLogChanged">
<strong>{{ s__('ClusterIntegration|Send Cilium Logs') }}</strong>
</gl-form-checkbox>
</div>
<div v-if="showButtons" class="mt-3"> <div v-if="showButtons" class="mt-3">
<gl-deprecated-button <gl-deprecated-button
ref="saveBtn" ref="saveBtn"
......
...@@ -110,6 +110,8 @@ export default class ClusterStore { ...@@ -110,6 +110,8 @@ export default class ClusterStore {
host: null, host: null,
port: null, port: null,
protocol: null, protocol: null,
wafLogEnabled: null,
ciliumLogEnabled: null,
isEditingSettings: false, isEditingSettings: false,
}, },
}, },
...@@ -267,6 +269,8 @@ export default class ClusterStore { ...@@ -267,6 +269,8 @@ export default class ClusterStore {
this.state.applications.fluentd.port = serverAppEntry.port; this.state.applications.fluentd.port = serverAppEntry.port;
this.state.applications.fluentd.host = serverAppEntry.host; this.state.applications.fluentd.host = serverAppEntry.host;
this.state.applications.fluentd.protocol = serverAppEntry.protocol; this.state.applications.fluentd.protocol = serverAppEntry.protocol;
this.state.applications.fluentd.wafLogEnabled = serverAppEntry.waf_log_enabled;
this.state.applications.fluentd.ciliumLogEnabled = serverAppEntry.cilium_log_enabled;
} }
} }
}); });
......
---
title: Add Cilium to Fluentd UI controls on the Cluster Application page
merge_request: 29511
author:
type: changed
...@@ -570,9 +570,10 @@ To enable Fluentd: ...@@ -570,9 +570,10 @@ To enable Fluentd:
1. Provide the host domain name or URL in **SIEM Hostname**. 1. Provide the host domain name or URL in **SIEM Hostname**.
1. Provide the host port number in **SIEM Port**. 1. Provide the host port number in **SIEM Port**.
1. Select a **SIEM Protocol**. 1. Select a **SIEM Protocol**.
1. Select at least one of the available logs (such as WAF or Cilium).
1. Click **Save changes**. 1. Click **Save changes**.
![Fluentd input fields](img/fluentd_v12_10.png) ![Fluentd input fields](img/fluentd_v13_0.png)
### Future apps ### Future apps
......
...@@ -4542,7 +4542,7 @@ msgstr "" ...@@ -4542,7 +4542,7 @@ msgstr ""
msgid "ClusterIntegration|Fluentd" msgid "ClusterIntegration|Fluentd"
msgstr "" msgstr ""
msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. Export Web Application Firewall logs to your favorite SIEM." msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
msgstr "" msgstr ""
msgid "ClusterIntegration|GitLab Integration" msgid "ClusterIntegration|GitLab Integration"
...@@ -4956,6 +4956,12 @@ msgstr "" ...@@ -4956,6 +4956,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type" msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "" msgstr ""
msgid "ClusterIntegration|Send Cilium Logs"
msgstr ""
msgid "ClusterIntegration|Send ModSecurity Logs"
msgstr ""
msgid "ClusterIntegration|Service Token" msgid "ClusterIntegration|Service Token"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue'; import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants'; import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
import { GlAlert, GlDropdown } from '@gitlab/ui'; import { GlAlert, GlDropdown, GlFormCheckbox } from '@gitlab/ui';
import eventHub from '~/clusters/event_hub'; import eventHub from '~/clusters/event_hub';
const { UPDATING } = APPLICATION_STATUS; const { UPDATING } = APPLICATION_STATUS;
...@@ -9,34 +9,58 @@ const { UPDATING } = APPLICATION_STATUS; ...@@ -9,34 +9,58 @@ const { UPDATING } = APPLICATION_STATUS;
describe('FluentdOutputSettings', () => { describe('FluentdOutputSettings', () => {
let wrapper; let wrapper;
const defaultProps = { const defaultSettings = {
status: 'installable',
installed: false,
updateAvailable: false,
protocol: 'tcp', protocol: 'tcp',
host: '127.0.0.1', host: '127.0.0.1',
port: 514, port: 514,
isEditingSettings: false, wafLogEnabled: true,
ciliumLogEnabled: false,
};
const defaultProps = {
status: 'installable',
updateFailed: false,
...defaultSettings,
}; };
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(FluentdOutputSettings, { wrapper = shallowMount(FluentdOutputSettings, {
propsData: { propsData: {
fluentd: { ...defaultProps,
...defaultProps, ...props,
...props,
},
}, },
}); });
}; };
const updateComponentPropsFromEvent = () => {
const { isEditingSettings, ...props } = eventHub.$emit.mock.calls[0][1];
wrapper.setProps(props);
};
const findSaveButton = () => wrapper.find({ ref: 'saveBtn' }); const findSaveButton = () => wrapper.find({ ref: 'saveBtn' });
const findCancelButton = () => wrapper.find({ ref: 'cancelBtn' }); const findCancelButton = () => wrapper.find({ ref: 'cancelBtn' });
const findProtocolDropdown = () => wrapper.find(GlDropdown); const findProtocolDropdown = () => wrapper.find(GlDropdown);
const findCheckbox = name =>
wrapper.findAll(GlFormCheckbox).wrappers.find(x => x.text() === name);
const findHost = () => wrapper.find('#fluentd-host');
const findPort = () => wrapper.find('#fluentd-port');
const changeCheckbox = checkbox => {
const currentValue = checkbox.attributes('checked')?.toString() === 'true';
checkbox.vm.$emit('input', !currentValue);
};
const changeInput = ({ element }, val) => {
element.value = val;
element.dispatchEvent(new Event('input'));
};
const changePort = val => changeInput(findPort(), val);
const changeHost = val => changeInput(findHost(), val);
const changeProtocol = idx => findProtocolDropdown().vm.$children[idx].$emit('click');
const toApplicationSettings = ({ wafLogEnabled, ciliumLogEnabled, ...settings }) => ({
...settings,
waf_log_enabled: wafLogEnabled,
cilium_log_enabled: ciliumLogEnabled,
});
describe('when fluentd is installed', () => { describe('when fluentd is installed', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ installed: true, status: 'installed' }); createComponent({ status: 'installed' });
jest.spyOn(eventHub, '$emit'); jest.spyOn(eventHub, '$emit');
}); });
...@@ -45,73 +69,77 @@ describe('FluentdOutputSettings', () => { ...@@ -45,73 +69,77 @@ describe('FluentdOutputSettings', () => {
expect(findCancelButton().exists()).toBe(false); expect(findCancelButton().exists()).toBe(false);
}); });
describe('with protocol dropdown changed by the user', () => { describe.each`
desc | changeFn | key | value
${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'}
${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'}
${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123}
${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send ModSecurity Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled}
${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Cilium Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled}
`('$desc', ({ changeFn, key, value }) => {
beforeEach(() => { beforeEach(() => {
findProtocolDropdown().vm.$children[1].$emit('click'); changeFn();
wrapper.setProps({
fluentd: {
...defaultProps,
installed: true,
status: 'installed',
protocol: 'udp',
isEditingSettings: true,
},
});
});
it('renders save and cancel buttons', () => {
expect(findSaveButton().exists()).toBe(true);
expect(findCancelButton().exists()).toBe(true);
});
it('enables related toggle and buttons', () => {
expect(findSaveButton().attributes().disabled).toBeUndefined();
expect(findCancelButton().attributes().disabled).toBeUndefined();
}); });
it('triggers set event to be propagated with the current value', () => { it('triggers set event to be propagated with the current value', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('setFluentdSettings', { expect(eventHub.$emit).toHaveBeenCalledWith('setFluentdSettings', {
id: FLUENTD, [key]: value,
host: '127.0.0.1', isEditingSettings: true,
port: 514,
protocol: 'UDP',
}); });
}); });
describe('and the save changes button is clicked', () => { describe('when value is updated from store', () => {
beforeEach(() => { beforeEach(() => {
findSaveButton().vm.$emit('click'); updateComponentPropsFromEvent();
});
it('enables save and cancel buttons', () => {
expect(findSaveButton().exists()).toBe(true);
expect(findSaveButton().attributes().disabled).toBeUndefined();
expect(findCancelButton().exists()).toBe(true);
expect(findCancelButton().attributes().disabled).toBeUndefined();
}); });
it('triggers save event and pass current values', () => { describe('and the save changes button is clicked', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', { beforeEach(() => {
id: FLUENTD, eventHub.$emit.mockClear();
params: { findSaveButton().vm.$emit('click');
host: '127.0.0.1', });
port: 514,
protocol: 'udp', it('triggers save event and pass current values', () => {
}, expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: FLUENTD,
params: toApplicationSettings({
...defaultSettings,
[key]: value,
}),
});
}); });
}); });
});
describe('and the cancel button is clicked', () => { describe('and the cancel button is clicked', () => {
beforeEach(() => { beforeEach(() => {
findCancelButton().vm.$emit('click'); eventHub.$emit.mockClear();
wrapper.setProps({ findCancelButton().vm.$emit('click');
fluentd: { });
...defaultProps,
installed: true, it('triggers reset event', () => {
status: 'installed', expect(eventHub.$emit).toHaveBeenCalledWith('setFluentdSettings', {
protocol: 'udp', ...defaultSettings,
isEditingSettings: false, isEditingSettings: false,
}, });
}); });
});
it('triggers reset event and hides both cancel and save changes button', () => { describe('when value is updated from store', () => {
expect(findSaveButton().exists()).toBe(false); beforeEach(() => {
expect(findCancelButton().exists()).toBe(false); updateComponentPropsFromEvent();
});
it('does not render save and cancel buttons', () => {
expect(findSaveButton().exists()).toBe(false);
expect(findCancelButton().exists()).toBe(false);
});
});
}); });
}); });
}); });
......
...@@ -127,6 +127,7 @@ describe('Clusters Store', () => { ...@@ -127,6 +127,7 @@ describe('Clusters Store', () => {
statusReason: null, statusReason: null,
requestReason: null, requestReason: null,
port: null, port: null,
ciliumLogEnabled: null,
host: null, host: null,
protocol: null, protocol: null,
installed: false, installed: false,
...@@ -136,6 +137,7 @@ describe('Clusters Store', () => { ...@@ -136,6 +137,7 @@ describe('Clusters Store', () => {
uninstallSuccessful: false, uninstallSuccessful: false,
uninstallFailed: false, uninstallFailed: false,
validationError: null, validationError: null,
wafLogEnabled: null,
}, },
jupyter: { jupyter: {
title: 'JupyterHub', title: 'JupyterHub',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment