Commit e5397eea authored by Zamir Martins Filho's avatar Zamir Martins Filho Committed by Mark Florian

Add Fluentd into cluster apps page

Fluentd with port, host and protocol
as settings which can be updated
parent 981cefbf
......@@ -49,6 +49,7 @@ export default class Clusters {
installElasticStackPath,
installCrossplanePath,
installPrometheusPath,
installFluentdPath,
managePrometheusPath,
clusterEnvironmentsPath,
hasRbac,
......@@ -102,6 +103,7 @@ export default class Clusters {
updateKnativeEndpoint: updateKnativePath,
installElasticStackEndpoint: installElasticStackPath,
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
installFluentdEndpoint: installFluentdPath,
});
this.installApplication = this.installApplication.bind(this);
......@@ -265,6 +267,7 @@ export default class Clusters {
eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data));
eventHub.$on('setIngressModSecurityMode', data => this.setIngressModSecurityMode(data));
eventHub.$on('resetIngressModSecurityChanges', id => this.resetIngressModSecurityChanges(id));
eventHub.$on('setFluentdSettings', data => this.setFluentdSettings(data));
// Add event listener to all the banner close buttons
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
......@@ -281,6 +284,7 @@ export default class Clusters {
eventHub.$off('setIngressModSecurityEnabled');
eventHub.$off('setIngressModSecurityMode');
eventHub.$off('resetIngressModSecurityChanges');
eventHub.$off('setFluentdSettings');
}
initPolling(method, successCallback, errorCallback) {
......@@ -506,6 +510,12 @@ export default class Clusters {
});
}
setFluentdSettings({ id: appId, port, protocol, host }) {
this.store.updateAppProperty(appId, 'port', port);
this.store.updateAppProperty(appId, 'protocol', protocol);
this.store.updateAppProperty(appId, 'host', host);
}
toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
if (externalIp !== newExternalIp) {
this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
......
......@@ -14,6 +14,7 @@ import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
import fluentdLogo from 'images/cluster_app_logos/fluentd.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
......@@ -22,6 +23,7 @@ import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../con
import eventHub from '~/clusters/event_hub';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
import IngressModsecuritySettings from './ingress_modsecurity_settings.vue';
import FluentdOutputSettings from './fluentd_output_settings.vue';
export default {
components: {
......@@ -31,6 +33,7 @@ export default {
KnativeDomainEditor,
CrossplaneProviderStack,
IngressModsecuritySettings,
FluentdOutputSettings,
},
props: {
type: {
......@@ -102,6 +105,7 @@ export default {
meltanoLogo,
prometheusLogo,
elasticStackLogo,
fluentdLogo,
}),
computed: {
isProjectCluster() {
......@@ -670,6 +674,41 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
</p>
</div>
</application-row>
<application-row
id="fluentd"
:logo-url="fluentdLogo"
:title="applications.fluentd.title"
:status="applications.fluentd.status"
:status-reason="applications.fluentd.statusReason"
:request-status="applications.fluentd.requestStatus"
:request-reason="applications.fluentd.requestReason"
:installed="applications.fluentd.installed"
:install-failed="applications.fluentd.installFailed"
:install-application-request-params="{
host: applications.fluentd.host,
port: applications.fluentd.port,
protocol: applications.fluentd.protocol,
}"
:uninstallable="applications.fluentd.uninstallable"
:uninstall-successful="applications.fluentd.uninstallSuccessful"
:uninstall-failed="applications.fluentd.uninstallFailed"
:disabled="!helmInstalled"
:updateable="false"
title-link="https://github.com/helm/charts/tree/master/stable/fluentd"
>
<div slot="description">
<p>
{{
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.`,
)
}}
</p>
<fluentd-output-settings :fluentd="applications.fluentd" />
</div>
</application-row>
</div>
</section>
</template>
<script>
import { __ } from '~/locale';
import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
import { GlAlert, GlDeprecatedButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
export default {
components: {
GlAlert,
GlDeprecatedButton,
GlDropdown,
GlDropdownItem,
},
props: {
fluentd: {
type: Object,
required: true,
},
protocols: {
type: Array,
required: false,
default: () => ['TCP', 'UDP'],
},
},
computed: {
isSaving() {
return [UPDATING].includes(this.fluentd.status);
},
saveButtonDisabled() {
return [UNINSTALLING, UPDATING, INSTALLING].includes(this.fluentd.status);
},
saveButtonLabel() {
return this.isSaving ? __('Saving') : __('Save changes');
},
/**
* Returns true either when:
* - The application is getting updated.
* - The user has changed some of the settings for an application which is
* neither getting installed nor updated.
*/
showButtons() {
return (
this.isSaving ||
(this.fluentd.isEditingSettings && [INSTALLED, UPDATED].includes(this.fluentd.status))
);
},
protocolName() {
if (this.fluentd.protocol !== null && this.fluentd.protocol !== undefined) {
return this.fluentd.protocol.toUpperCase();
}
return __('Protocol');
},
fluentdPort: {
get() {
return this.fluentd.port;
},
set(port) {
this.setFluentSettings({ port });
},
},
fluentdHost: {
get() {
return this.fluentd.host;
},
set(host) {
this.setFluentSettings({ host });
},
},
},
methods: {
updateApplication() {
eventHub.$emit('updateApplication', {
id: FLUENTD,
params: {
port: this.fluentd.port,
protocol: this.fluentd.protocol,
host: this.fluentd.host,
},
});
this.resetStatus();
},
resetStatus() {
this.fluentd.isEditingSettings = false;
},
selectProtocol(protocol) {
this.setFluentSettings({ protocol });
},
setFluentSettings({ port, protocol, host }) {
this.fluentd.isEditingSettings = true;
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', {
id: FLUENTD,
port: newPort,
protocol: newProtocol,
host: newHost,
});
},
},
};
</script>
<template>
<div>
<gl-alert v-if="fluentd.updateFailed" class="mb-3" variant="danger" :dismissible="false">
{{
s__(
'ClusterIntegration|Something went wrong while trying to save your settings. Please try again.',
)
}}
</gl-alert>
<div class="form-horizontal">
<div class="form-group">
<label for="fluentd-host">
<strong>{{ s__('ClusterIntegration|SIEM Hostname') }}</strong>
</label>
<input id="fluentd-host" v-model="fluentdHost" type="text" class="form-control" />
</div>
<div class="form-group">
<label for="fluentd-port">
<strong>{{ s__('ClusterIntegration|SIEM Port') }}</strong>
</label>
<input id="fluentd-port" v-model="fluentdPort" type="text" class="form-control" />
</div>
<div class="form-group">
<label for="fluentd-protocol">
<strong>{{ s__('ClusterIntegration|SIEM Protocol') }}</strong>
</label>
<gl-dropdown :text="protocolName" class="w-100">
<gl-dropdown-item
v-for="(value, index) in protocols"
:key="index"
@click="selectProtocol(value)"
>
{{ value }}
</gl-dropdown-item>
</gl-dropdown>
</div>
<div v-if="showButtons" class="mt-3">
<gl-deprecated-button
ref="saveBtn"
class="mr-1"
variant="success"
:loading="isSaving"
:disabled="saveButtonDisabled"
@click="updateApplication"
>
{{ saveButtonLabel }}
</gl-deprecated-button>
<gl-deprecated-button ref="cancelBtn" :disabled="saveButtonDisabled" @click="resetStatus">
{{ __('Cancel') }}
</gl-deprecated-button>
</div>
</div>
</div>
</template>
......@@ -53,6 +53,7 @@ export const CERT_MANAGER = 'cert_manager';
export const CROSSPLANE = 'crossplane';
export const PROMETHEUS = 'prometheus';
export const ELASTIC_STACK = 'elastic_stack';
export const FLUENTD = 'fluentd';
export const APPLICATIONS = [
HELM,
......@@ -63,6 +64,7 @@ export const APPLICATIONS = [
CERT_MANAGER,
PROMETHEUS,
ELASTIC_STACK,
FLUENTD,
];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
......
......@@ -13,6 +13,7 @@ export default class ClusterService {
jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint,
elastic_stack: this.options.installElasticStackEndpoint,
fluentd: this.options.installFluentdEndpoint,
};
this.appUpdateEndpointMap = {
knative: this.options.updateKnativeEndpoint,
......
......@@ -13,6 +13,7 @@ import {
UPDATE_EVENT,
UNINSTALL_EVENT,
ELASTIC_STACK,
FLUENTD,
} from '../constants';
import transitionApplicationState from '../services/application_state_machine';
......@@ -103,6 +104,14 @@ export default class ClusterStore {
...applicationInitialState,
title: s__('ClusterIntegration|Elastic Stack'),
},
fluentd: {
...applicationInitialState,
title: s__('ClusterIntegration|Fluentd'),
host: null,
port: null,
protocol: null,
isEditingSettings: false,
},
},
environments: [],
fetchingEnvironments: false,
......@@ -253,6 +262,12 @@ export default class ClusterStore {
} else if (appId === ELASTIC_STACK) {
this.state.applications.elastic_stack.version = version;
this.state.applications.elastic_stack.updateAvailable = updateAvailable;
} else if (appId === FLUENTD) {
if (!this.state.applications.fluentd.isEditingSettings) {
this.state.applications.fluentd.port = serverAppEntry.port;
this.state.applications.fluentd.host = serverAppEntry.host;
this.state.applications.fluentd.protocol = serverAppEntry.protocol;
}
}
});
}
......
......@@ -17,6 +17,7 @@
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative),
install_elastic_stack_path: clusterable.install_applications_cluster_path(@cluster, :elastic_stack),
install_fluentd_path: clusterable.install_applications_cluster_path(@cluster, :fluentd),
cluster_environments_path: cluster_environments_path,
toggle_status: @cluster.enabled? ? 'true': 'false',
has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false',
......
---
title: Add Fluentd into cluster apps page
merge_request: 28847
author:
type: changed
......@@ -556,8 +556,6 @@ To enable Fluentd:
1. Provide the host domain name or URL in **SIEM Hostname**.
1. Provide the host port number in **SIEM Port**.
1. Select a **SIEM Protocol**.
1. Check **Send ModSecurity Logs**. If you do not select this checkbox, the **Install**
button is disabled.
1. Click **Save changes**.
![Fluentd input fields](img/fluentd_v12_10.png)
......
......@@ -4485,6 +4485,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
msgid "ClusterIntegration|Fluentd"
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."
msgstr ""
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
......@@ -4791,6 +4797,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
msgid "ClusterIntegration|SIEM Hostname"
msgstr ""
msgid "ClusterIntegration|SIEM Port"
msgstr ""
msgid "ClusterIntegration|SIEM Protocol"
msgstr ""
msgid "ClusterIntegration|Save changes"
msgstr ""
......@@ -16597,6 +16612,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
msgid "Protocol"
msgstr ""
msgid "Provider"
msgstr ""
......
......@@ -8,6 +8,7 @@ import eventHub from '~/clusters/event_hub';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
describe('Applications', () => {
let vm;
......@@ -67,6 +68,10 @@ describe('Applications', () => {
it('renders a row for Elastic Stack', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
});
it('renders a row for Fluentd', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
});
});
describe('Group cluster applications', () => {
......@@ -112,6 +117,10 @@ describe('Applications', () => {
it('renders a row for Elastic Stack', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
});
it('renders a row for Fluentd', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
});
});
describe('Instance cluster applications', () => {
......@@ -157,6 +166,10 @@ describe('Applications', () => {
it('renders a row for Elastic Stack', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
});
it('renders a row for Fluentd', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
});
});
describe('Helm application', () => {
......@@ -240,6 +253,7 @@ describe('Applications', () => {
jupyter: { title: 'JupyterHub', hostname: '' },
knative: { title: 'Knative', hostname: '' },
elastic_stack: { title: 'Elastic Stack' },
fluentd: { title: 'Fluentd' },
},
});
......@@ -539,4 +553,23 @@ describe('Applications', () => {
});
});
});
describe('Fluentd application', () => {
const propsData = {
applications: {
...APPLICATIONS_MOCK_STATE,
},
};
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Applications, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('renders the correct Component', () => {
expect(wrapper.contains(FluentdOutputSettings)).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
import { GlAlert, GlDropdown } from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
const { UPDATING } = APPLICATION_STATUS;
describe('FluentdOutputSettings', () => {
let wrapper;
const defaultProps = {
status: 'installable',
installed: false,
updateAvailable: false,
protocol: 'tcp',
host: '127.0.0.1',
port: 514,
isEditingSettings: false,
};
const createComponent = (props = {}) => {
wrapper = shallowMount(FluentdOutputSettings, {
propsData: {
fluentd: {
...defaultProps,
...props,
},
},
});
};
const findSaveButton = () => wrapper.find({ ref: 'saveBtn' });
const findCancelButton = () => wrapper.find({ ref: 'cancelBtn' });
const findProtocolDropdown = () => wrapper.find(GlDropdown);
describe('when fluentd is installed', () => {
beforeEach(() => {
createComponent({ installed: true, status: 'installed' });
jest.spyOn(eventHub, '$emit');
});
it('does not render save and cancel buttons', () => {
expect(findSaveButton().exists()).toBe(false);
expect(findCancelButton().exists()).toBe(false);
});
describe('with protocol dropdown changed by the user', () => {
beforeEach(() => {
findProtocolDropdown().vm.$children[1].$emit('click');
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', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('setFluentdSettings', {
id: FLUENTD,
host: '127.0.0.1',
port: 514,
protocol: 'UDP',
});
});
describe('and the save changes button is clicked', () => {
beforeEach(() => {
findSaveButton().vm.$emit('click');
});
it('triggers save event and pass current values', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: FLUENTD,
params: {
host: '127.0.0.1',
port: 514,
protocol: 'udp',
},
});
});
});
describe('and the cancel button is clicked', () => {
beforeEach(() => {
findCancelButton().vm.$emit('click');
wrapper.setProps({
fluentd: {
...defaultProps,
installed: true,
status: 'installed',
protocol: 'udp',
isEditingSettings: false,
},
});
});
it('triggers reset event and hides both cancel and save changes button', () => {
expect(findSaveButton().exists()).toBe(false);
expect(findCancelButton().exists()).toBe(false);
});
});
});
describe(`when fluentd status is ${UPDATING}`, () => {
beforeEach(() => {
createComponent({ installed: true, status: UPDATING });
});
it('renders loading spinner in save button', () => {
expect(findSaveButton().props('loading')).toBe(true);
});
it('renders disabled save button', () => {
expect(findSaveButton().props('disabled')).toBe(true);
});
it('renders save button with "Saving" label', () => {
expect(findSaveButton().text()).toBe('Saving');
});
});
describe('when fluentd fails to update', () => {
beforeEach(() => {
createComponent({ updateFailed: true });
});
it('displays a error message', () => {
expect(wrapper.contains(GlAlert)).toBe(true);
});
});
});
describe('when fluentd is not installed', () => {
beforeEach(() => {
createComponent();
});
it('does not render the save button', () => {
expect(findSaveButton().exists()).toBe(false);
expect(findCancelButton().exists()).toBe(false);
});
});
});
......@@ -159,6 +159,7 @@ const APPLICATIONS_MOCK_STATE = {
jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' },
knative: { title: 'Knative ', status: 'installable', hostname: '' },
elastic_stack: { title: 'Elastic Stack', status: 'installable' },
fluentd: { title: 'Fluentd', status: 'installable' },
};
export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE };
......@@ -121,6 +121,22 @@ describe('Clusters Store', () => {
uninstallFailed: false,
validationError: null,
},
fluentd: {
title: 'Fluentd',
status: null,
statusReason: null,
requestReason: null,
port: null,
host: null,
protocol: null,
installed: false,
isEditingSettings: false,
installFailed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
......
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