diff options
20 files changed, 343 insertions, 40 deletions
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 72963fb08c2..426cb9d6d8c 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.21.0 +8.22.0 diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index b0de4dc8628..2427e25a17d 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -92,10 +92,10 @@ export default class TemplateSelector { } startLoadingSpinner() { - this.$dropdownIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down'); + this.$dropdownIcon.addClass('spinner').removeClass('fa-chevron-down'); } stopLoadingSpinner() { - this.$dropdownIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin'); + this.$dropdownIcon.addClass('fa-chevron-down').removeClass('spinner'); } } diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index b764348eb3c..d35dca7b939 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -255,6 +255,7 @@ export default class Clusters { eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data)); eventHub.$on('uninstallApplication', data => this.uninstallApplication(data)); eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data)); + eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data)); // Add event listener to all the banner close buttons this.addBannerCloseHandler(this.unreachableContainer, 'unreachable'); this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure'); @@ -268,6 +269,7 @@ export default class Clusters { eventHub.$off('setKnativeHostname'); eventHub.$off('setCrossplaneProviderStack'); eventHub.$off('uninstallApplication'); + eventHub.$off('setIngressModSecurityEnabled'); } initPolling(method, successCallback, errorCallback) { @@ -513,6 +515,11 @@ export default class Clusters { this.store.updateAppProperty(appId, 'validationError', null); } + setIngressModSecurityEnabled({ id, modSecurityEnabled }) { + this.store.updateAppProperty(id, 'isEditingModSecurityEnabled', true); + this.store.updateAppProperty(id, 'modsecurity_enabled', modSecurityEnabled); + } + destroy() { this.destroyed = true; diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index fe2ad562ad5..9429e10e6ed 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -21,6 +21,7 @@ import KnativeDomainEditor from './knative_domain_editor.vue'; import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants'; import eventHub from '~/clusters/event_hub'; import CrossplaneProviderStack from './crossplane_provider_stack.vue'; +import IngressModsecuritySettings from './ingress_modsecurity_settings.vue'; export default { components: { @@ -29,6 +30,7 @@ export default { GlLoadingIcon, KnativeDomainEditor, CrossplaneProviderStack, + IngressModsecuritySettings, }, props: { type: { @@ -129,18 +131,6 @@ export default { crossplaneInstalled() { return this.applications.crossplane.status === APPLICATION_STATUS.INSTALLED; }, - ingressModSecurityDescription() { - const escapedUrl = _.escape(this.ingressModSecurityHelpPath); - - return sprintf( - s__('ClusterIntegration|Learn more about %{startLink}ModSecurity%{endLink}'), - { - startLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`, - endLink: '</a>', - }, - false, - ); - }, ingressDescription() { return sprintf( _.escape( @@ -241,6 +231,9 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity } return null; }, + ingress() { + return this.applications.ingress; + }, }, created() { this.helmInstallIllustration = helmInstallIllustration; @@ -329,6 +322,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity :uninstall-successful="applications.ingress.uninstallSuccessful" :uninstall-failed="applications.ingress.uninstallFailed" :disabled="!helmInstalled" + :updateable="false" title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" > <div slot="description"> @@ -340,25 +334,10 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity }} </p> - <template> - <div class="form-group"> - <div class="form-check form-check-inline"> - <input - v-model="applications.ingress.modsecurity_enabled" - :disabled="ingressInstalled" - type="checkbox" - autocomplete="off" - class="form-check-input" - /> - <label class="form-check-label label-bold" for="ingress-enable-modsecurity"> - {{ s__('ClusterIntegration|Enable Web Application Firewall') }} - </label> - </div> - <p class="form-text text-muted"> - <strong v-html="ingressModSecurityDescription"></strong> - </p> - </div> - </template> + <ingress-modsecurity-settings + :ingress="ingress" + :ingress-mod-security-help-path="ingressModSecurityHelpPath" + /> <template v-if="ingressInstalled"> <div class="form-group"> diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue new file mode 100644 index 00000000000..c30015f31de --- /dev/null +++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue @@ -0,0 +1,116 @@ +<script> +import _ from 'lodash'; +import { __ } from '../../locale'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants'; +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import eventHub from '~/clusters/event_hub'; + +const { UPDATING, UNINSTALLING } = APPLICATION_STATUS; + +export default { + components: { + LoadingButton, + GlAlert, + GlSprintf, + GlLink, + }, + props: { + ingress: { + type: Object, + required: true, + }, + ingressModSecurityHelpPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + modSecurityEnabled: { + get() { + return this.ingress.modsecurity_enabled; + }, + set(isEnabled) { + eventHub.$emit('setIngressModSecurityEnabled', { + id: INGRESS, + modSecurityEnabled: isEnabled, + }); + }, + }, + ingressModSecurityDescription() { + return _.escape(this.ingressModSecurityHelpPath); + }, + saving() { + return [UPDATING].includes(this.ingress.status); + }, + saveButtonDisabled() { + return [UNINSTALLING, UPDATING].includes(this.ingress.status); + }, + saveButtonLabel() { + return this.saving ? __('Saving') : __('Save changes'); + }, + ingressInstalled() { + return this.ingress.installed; + }, + }, + methods: { + updateApplication() { + eventHub.$emit('updateApplication', { + id: INGRESS, + params: { modsecurity_enabled: this.ingress.modsecurity_enabled }, + }); + }, + }, +}; +</script> + +<template> + <div> + <gl-alert + v-if="ingress.updateFailed" + class="mb-3" + variant="danger" + :dismissible="false" + @dismiss="alert = null" + > + {{ + s__('ClusterIntegration|Something went wrong while updating the Web Application Firewall.') + }} + </gl-alert> + <div class="form-group"> + <div class="form-check form-check-inline"> + <input + v-model="modSecurityEnabled" + type="checkbox" + autocomplete="off" + class="form-check-input" + /> + <label class="form-check-label label-bold" for="ingress-enable-modsecurity"> + {{ s__('ClusterIntegration|Enable Web Application Firewall') }} + </label> + </div> + <p class="form-text text-muted"> + <strong> + <gl-sprintf + :message="s__('ClusterIntegration|Learn more about %{linkStart}ModSecurity%{linkEnd}')" + > + <template #link="{ content }"> + <gl-link :href="ingressModSecurityDescription" target="_blank" + >{{ content }} + </gl-link> + </template> + </gl-sprintf> + </strong> + </p> + <loading-button + v-if="ingressInstalled" + class="btn-success mt-1" + :loading="saving" + :disabled="saveButtonDisabled" + :label="saveButtonLabel" + @click="updateApplication" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 939c396e1b9..ffe71455b2d 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -54,6 +54,8 @@ export default class ClusterStore { modsecurity_enabled: false, externalIp: null, externalHostname: null, + isEditingModSecurityEnabled: false, + updateFailed: false, }, cert_manager: { ...applicationInitialState, @@ -208,8 +210,11 @@ export default class ClusterStore { if (appId === INGRESS) { this.state.applications.ingress.externalIp = serverAppEntry.external_ip; this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname; - this.state.applications.ingress.modsecurity_enabled = - serverAppEntry.modsecurity_enabled || this.state.applications.ingress.modsecurity_enabled; + if (!this.state.applications.ingress.isEditingModSecurityEnabled) { + this.state.applications.ingress.modsecurity_enabled = + serverAppEntry.modsecurity_enabled || + this.state.applications.ingress.modsecurity_enabled; + } } else if (appId === CERT_MANAGER) { this.state.applications.cert_manager.email = this.state.applications.cert_manager.email || serverAppEntry.email; diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue index 2bc93c3f1c1..78ae719ba0d 100644 --- a/app/assets/javascripts/repository/components/preview/index.vue +++ b/app/assets/javascripts/repository/components/preview/index.vue @@ -1,4 +1,6 @@ <script> +import $ from 'jquery'; +import '~/behaviors/markdown/render_gfm'; import { GlLink, GlLoadingIcon } from '@gitlab/ui'; import getReadmeQuery from '../../queries/getReadme.query.graphql'; @@ -30,6 +32,15 @@ export default { loading: 0, }; }, + watch: { + readme(newVal) { + if (newVal) { + this.$nextTick(() => { + $(this.$refs.readme).renderGFM(); + }); + } + }, + }, }; </script> @@ -45,7 +56,7 @@ export default { </div> <div class="blob-viewer"> <gl-loading-icon v-if="loading > 0" size="md" color="dark" class="my-4 mx-auto" /> - <div v-else-if="readme" v-html="readme.html"></div> + <div v-else-if="readme" ref="readme" v-html="readme.html"></div> </div> </article> </template> diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 41f3603506f..005e5efbdaf 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -150,6 +150,12 @@ right: 8px; } + .spinner { + position: absolute; + top: 9px; + right: 8px; + } + .ic-chevron-down { position: absolute; top: $gl-padding-8; diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 958dc27984f..80c7a803392 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -97,7 +97,7 @@ class GroupsController < Groups::ApplicationController end def edit - @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id)) + @badge_api_endpoint = expose_path(api_v4_groups_badges_path(id: @group.id)) end def projects diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 31b86946ca2..045aa38230c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -50,7 +50,7 @@ class ProjectsController < Projects::ApplicationController # rubocop: enable CodeReuse/ActiveRecord def edit - @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id)) + @badge_api_endpoint = expose_path(api_v4_projects_badges_path(id: @project.id)) render_edit end diff --git a/changelogs/unreleased/allow_toggle_modsecurity_settings.yml b/changelogs/unreleased/allow_toggle_modsecurity_settings.yml new file mode 100644 index 00000000000..7535d00c58d --- /dev/null +++ b/changelogs/unreleased/allow_toggle_modsecurity_settings.yml @@ -0,0 +1,5 @@ +--- +title: Allow enabling/disabling modsecurity from UI +merge_request: 24747 +author: +type: added diff --git a/changelogs/unreleased/rk4bir-master-patch-77755.yml b/changelogs/unreleased/rk4bir-master-patch-77755.yml new file mode 100644 index 00000000000..b904dad6660 --- /dev/null +++ b/changelogs/unreleased/rk4bir-master-patch-77755.yml @@ -0,0 +1,5 @@ +--- +title: Migrated from .fa-spinner to .spinner in app/assets/javascripts/blob/template_selector.js +merge_request: 25045 +author: Raihan Kabir (gitlab/rk4bir) +type: changed diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb index 0a8fbb9a673..3940b3fca4b 100644 --- a/lib/gitlab/diff/highlight_cache.rb +++ b/lib/gitlab/diff/highlight_cache.rb @@ -17,6 +17,14 @@ module Gitlab buckets [100, 1000, 10000, 100000, 1000000, 10000000] end + define_counter :gitlab_redis_diff_caching_hit do + docstring 'Redis diff caching hits' + end + + define_counter :gitlab_redis_diff_caching_miss do + docstring 'Redis diff caching misses' + end + def initialize(diff_collection) @diff_collection = diff_collection end diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index cbde713e3b9..ba1a474c523 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -124,6 +124,8 @@ module Gitlab self.__send__("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend end + record_metric_blob_size + # Retain the actual size before it is encoded @loaded_size = @data.bytesize if @data @loaded_all_data = @loaded_size == size @@ -202,6 +204,12 @@ module Gitlab private + def record_metric_blob_size + return unless size + + self.class.gitlab_blob_size.observe({}, size) + end + def has_lfs_version_key? !empty? && text_in_repo? && data.start_with?("version https://git-lfs.github.com/spec") end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index eb5adf8070b..e0209671160 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4310,7 +4310,7 @@ msgstr "" msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}." msgstr "" -msgid "ClusterIntegration|Learn more about %{startLink}ModSecurity%{endLink}" +msgid "ClusterIntegration|Learn more about %{linkStart}ModSecurity%{linkEnd}" msgstr "" msgid "ClusterIntegration|Learn more about %{startLink}Regions %{externalLinkIcon}%{endLink}." @@ -4595,6 +4595,9 @@ msgstr "" msgid "ClusterIntegration|Something went wrong while updating Knative domain name." msgstr "" +msgid "ClusterIntegration|Something went wrong while updating the Web Application Firewall." +msgstr "" + msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain." msgstr "" diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js index c3336edfe59..3e25c825fe8 100644 --- a/spec/frontend/clusters/components/applications_spec.js +++ b/spec/frontend/clusters/components/applications_spec.js @@ -7,6 +7,7 @@ import { APPLICATIONS_MOCK_STATE } from '../services/mock_data'; 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'; describe('Applications', () => { let vm; @@ -156,6 +157,30 @@ describe('Applications', () => { }); describe('Ingress application', () => { + describe('with nested component', () => { + const propsData = { + applications: { + ...APPLICATIONS_MOCK_STATE, + ingress: { + title: 'Ingress', + status: 'installed', + }, + }, + }; + + let wrapper; + beforeEach(() => { + wrapper = shallowMount(Applications, { propsData }); + }); + afterEach(() => { + wrapper.destroy(); + }); + it('renders IngressModsecuritySettings', () => { + const modsecuritySettings = wrapper.find(IngressModsecuritySettings); + expect(modsecuritySettings.exists()).toBe(true); + }); + }); + describe('when installed', () => { describe('with ip address', () => { it('renders ip address with a clipboard button', () => { diff --git a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js new file mode 100644 index 00000000000..e7d2b7bf5c5 --- /dev/null +++ b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js @@ -0,0 +1,107 @@ +import { shallowMount } from '@vue/test-utils'; +import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants'; +import { GlAlert } from '@gitlab/ui'; +import eventHub from '~/clusters/event_hub'; + +const { UPDATING } = APPLICATION_STATUS; + +describe('IngressModsecuritySettings', () => { + let wrapper; + + const defaultProps = { + modsecurity_enabled: false, + status: 'installable', + installed: false, + }; + + const createComponent = (props = defaultProps) => { + wrapper = shallowMount(IngressModsecuritySettings, { + propsData: { + ingress: { + ...defaultProps, + ...props, + }, + }, + }); + }; + + const findSaveButton = () => wrapper.find(LoadingButton); + const findModSecurityCheckbox = () => wrapper.find('input').element; + + describe('when ingress is installed', () => { + beforeEach(() => { + createComponent({ installed: true }); + jest.spyOn(eventHub, '$emit'); + }); + + it('renders save button', () => { + expect(findSaveButton().exists()).toBe(true); + expect(findModSecurityCheckbox().checked).toBe(false); + }); + + describe('and the save changes button is clicked', () => { + beforeEach(() => { + findSaveButton().vm.$emit('click'); + }); + + it('triggers save event and pass current modsecurity value', () => + wrapper.vm.$nextTick().then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', { + id: INGRESS, + params: { modsecurity_enabled: false }, + }); + })); + }); + + it('triggers set event to be propagated with the current modsecurity value', () => { + wrapper.setData({ modSecurityEnabled: true }); + return wrapper.vm.$nextTick().then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('setIngressModSecurityEnabled', { + id: INGRESS, + modSecurityEnabled: true, + }); + }); + }); + + describe(`when ingress 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().props('label')).toBe('Saving'); + }); + }); + + describe('when ingress fails to update', () => { + beforeEach(() => { + createComponent({ updateFailed: true }); + }); + + it('displays a error message', () => { + expect(wrapper.find(GlAlert).exists()).toBe(true); + }); + }); + }); + + describe('when ingress is not installed', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not render the save button', () => { + expect(findSaveButton().exists()).toBe(false); + expect(findModSecurityCheckbox().checked).toBe(false); + }); + }); +}); diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js index f2dbdd0638b..d3775c6cfba 100644 --- a/spec/frontend/clusters/stores/clusters_store_spec.js +++ b/spec/frontend/clusters/stores/clusters_store_spec.js @@ -81,8 +81,10 @@ describe('Clusters Store', () => { externalIp: null, externalHostname: null, installed: false, + isEditingModSecurityEnabled: false, installFailed: true, uninstallable: false, + updateFailed: false, uninstallSuccessful: false, uninstallFailed: false, validationError: null, diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb index 218c393c409..eb8072a554e 100644 --- a/spec/lib/gitlab/diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb @@ -149,5 +149,13 @@ describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do it 'defines :gitlab_redis_diff_caching_memory_usage_bytes histogram' do expect(described_class).to respond_to(:gitlab_redis_diff_caching_memory_usage_bytes) end + + it 'defines :gitlab_redis_diff_caching_hit' do + expect(described_class).to respond_to(:gitlab_redis_diff_caching_hit) + end + + it 'defines :gitlab_redis_diff_caching_miss' do + expect(described_class).to respond_to(:gitlab_redis_diff_caching_miss) + end end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 521c03058ca..3277e02aafa 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -12,10 +12,18 @@ describe Gitlab::Git::Blob, :seed_helper do let(:blob) { Gitlab::Git::Blob.new(name: 'test') } it 'handles nil data' do + expect(described_class).not_to receive(:gitlab_blob_size) + expect(blob.name).to eq('test') expect(blob.size).to eq(nil) expect(blob.loaded_size).to eq(nil) end + + it 'records blob size' do + expect(described_class).to receive(:gitlab_blob_size).and_call_original + + Gitlab::Git::Blob.new(name: 'test', size: 1234) + end end shared_examples '.find' do |