Commit 976c4e52 authored by Mark Florian's avatar Mark Florian

Merge branch 'add_fluentd_into_cluster_app_page' into 'master'

Add Fluentd into cluster apps page

See merge request gitlab-org/gitlab!28847
parents 0ded9e25 e5397eea
......@@ -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)
......
......@@ -4488,6 +4488,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 ""
......@@ -4794,6 +4800,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 ""
......@@ -16594,6 +16609,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