diff options
53 files changed, 911 insertions, 507 deletions
diff --git a/.gitlab/issue_templates/Basic Proposal.md b/.gitlab/issue_templates/Basic Proposal.md index 4232561354c..8d47e87f8a3 100644 --- a/.gitlab/issue_templates/Basic Proposal.md +++ b/.gitlab/issue_templates/Basic Proposal.md @@ -6,6 +6,6 @@ <!-- Consider adding related issues and epics to this issue. You can also reference the Feature Proposal Template (https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md) for additional details to consider adding to this issue. Additionally, as a data oriented organization, when your feature exits planning breakdown, consider adding the `What does success look like, and how can we measure that?` section. -/label ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Starter"/~"GitLab Premium"/~"GitLab Ultimate" +/label ~"group::" ~"section::" ~"Category::" ~"GitLab Core"/~"GitLab Premium"/~"GitLab Ultimate" --> diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 1090a04ad36..2cdf2341c88 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -97,7 +97,7 @@ Create tracking issue using the the Snowplow event tracking template. See https: ### What is the type of buyer? <!-- What is the buyer persona for this feature? See https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/buyer-persona/ -In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#four-tiers --> +In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#three-tiers --> ### Is this a cross-stage feature? diff --git a/.gitlab/issue_templates/Lean Feature Proposal.md b/.gitlab/issue_templates/Lean Feature Proposal.md index a1f217fc624..fb9ac306f31 100644 --- a/.gitlab/issue_templates/Lean Feature Proposal.md +++ b/.gitlab/issue_templates/Lean Feature Proposal.md @@ -96,7 +96,7 @@ Define both the success metrics and acceptance criteria. Note that success metri ### What is the type of buyer? What is the buyer persona for this feature? See https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/buyer-persona/ -In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#four-tiers +In which enterprise tier should this feature go? See https://about.gitlab.com/handbook/product/pricing/#three-tiers ### Is this a cross-stage feature? diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index c675e6a366e..f690344ee76 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -f319db221de49c979f3606e3934bd76c218f813d +2d3b680ab7d7652377c17c962c2628d98a6ffc9a diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 4e93bd8dcf1..4362263b0dd 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -2,21 +2,24 @@ import { GlButton, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui'; import { getParameterByName } from '~/lib/utils/common_utils'; import eventHub from '~/pipelines/event_hub'; -import pipelinesMixin from '~/pipelines/mixins/pipelines'; -import PipelinesPaginationApiMixin from '~/pipelines/mixins/pipelines_pagination_api_mixin'; +import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin'; import PipelinesService from '~/pipelines/services/pipelines_service'; import PipelineStore from '~/pipelines/stores/pipelines_store'; +import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; +import SvgBlankState from '~/pipelines/components/pipelines_list/blank_state.vue'; export default { components: { - TablePagination, GlButton, + GlLink, GlLoadingIcon, GlModal, - GlLink, + PipelinesTableComponent, + TablePagination, + SvgBlankState, }, - mixins: [pipelinesMixin, PipelinesPaginationApiMixin], + mixins: [PipelinesMixin], props: { endpoint: { type: String, diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue index b114f6e7278..39852eba71a 100644 --- a/app/assets/javascripts/issuable_list/components/issuable_item.vue +++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue @@ -4,6 +4,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlTooltipDirective } from '@gi import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { isScopedLabel } from '~/lib/utils/common_utils'; import { getTimeago } from '~/lib/utils/datetime_utility'; +import { isExternal, setUrlFragment } from '~/lib/utils/url_utility'; import { __, sprintf } from '~/locale'; import IssuableAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; @@ -47,17 +48,14 @@ export default { author() { return this.issuable.author; }, + webUrl() { + return this.issuable.gitlabWebUrl || this.issuable.webUrl; + }, authorId() { return getIdFromGraphQLId(`${this.author.id}`); }, isIssuableUrlExternal() { - // Check if URL is relative, which means it is internal. - if (!/^https?:\/\//g.test(this.issuable.webUrl)) { - return false; - } - // In case URL is absolute, it may or may not be internal, - // hence use `gon.gitlab_url` which is current instance domain. - return !this.issuable.webUrl.includes(gon.gitlab_url); + return isExternal(this.webUrl); }, labels() { return this.issuable.labels?.nodes || this.issuable.labels || []; @@ -91,6 +89,9 @@ export default { this.hasSlotContents('status') || this.showDiscussions || this.issuable.assignees, ); }, + issuableNotesLink() { + return setUrlFragment(this.webUrl, 'notes'); + }, }, methods: { hasSlotContents(slotName) { @@ -144,7 +145,7 @@ export default { name="eye-slash" :title="__('Confidential')" /> - <gl-link :href="issuable.webUrl" v-bind="issuableTitleProps" + <gl-link :href="webUrl" v-bind="issuableTitleProps" >{{ issuable.title }}<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" /></gl-link> @@ -206,7 +207,7 @@ export default { <gl-link v-gl-tooltip:tooltipcontainer.top :title="__('Comments')" - :href="`${issuable.webUrl}#notes`" + :href="issuableNotesLink" :class="{ 'no-comments': !issuable.userDiscussionsCount }" class="gl-reset-color!" > diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue index 3a9d181066c..a45adc766e3 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue @@ -1,5 +1,5 @@ <script> -import { GlIcon } from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon } from '@gitlab/ui'; import { isEqual } from 'lodash'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { getParameterByName } from '~/lib/utils/common_utils'; @@ -7,22 +7,28 @@ import { __, s__ } from '~/locale'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue'; import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING, FILTER_TAG_IDENTIFIER } from '../../constants'; -import pipelinesMixin from '../../mixins/pipelines'; -import PipelinesPaginationApiMixin from '../../mixins/pipelines_pagination_api_mixin'; +import PipelinesMixin from '../../mixins/pipelines_mixin'; import PipelinesService from '../../services/pipelines_service'; import { validateParams } from '../../utils'; +import EmptyState from './empty_state.vue'; import NavigationControls from './nav_controls.vue'; import PipelinesFilteredSearch from './pipelines_filtered_search.vue'; +import PipelinesTableComponent from './pipelines_table.vue'; +import SvgBlankState from './blank_state.vue'; export default { components: { - TablePagination, + EmptyState, + GlIcon, + GlLoadingIcon, NavigationTabs, NavigationControls, PipelinesFilteredSearch, - GlIcon, + PipelinesTableComponent, + SvgBlankState, + TablePagination, }, - mixins: [pipelinesMixin, PipelinesPaginationApiMixin], + mixins: [PipelinesMixin], props: { store: { type: Object, @@ -217,6 +223,20 @@ export default { this.requestData = { page: this.page, scope: this.scope, ...this.validatedParams }; }, methods: { + onChangeTab(scope) { + if (this.scope === scope) { + return; + } + + let params = { + scope, + page: '1', + }; + + params = this.onChangeWithFilter(params); + + this.updateContent(params); + }, successCallback(resp) { // Because we are polling & the user is interacting verify if the response received // matches the last request made diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js index bff986261ee..8444d43513b 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js @@ -1,21 +1,13 @@ -import { GlLoadingIcon } from '@gitlab/ui'; import Visibility from 'visibilityjs'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import Poll from '~/lib/utils/poll'; +import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils'; +import { validateParams } from '~/pipelines/utils'; import { __ } from '~/locale'; -import SvgBlankState from '../components/pipelines_list/blank_state.vue'; -import EmptyState from '../components/pipelines_list/empty_state.vue'; -import PipelinesTableComponent from '../components/pipelines_list/pipelines_table.vue'; import { CANCEL_REQUEST } from '../constants'; import eventHub from '../event_hub'; export default { - components: { - PipelinesTableComponent, - SvgBlankState, - EmptyState, - GlLoadingIcon, - }, data() { return { isLoading: false, @@ -76,6 +68,25 @@ export default { this.poll.stop(); }, methods: { + updateInternalState(parameters) { + this.poll.stop(); + + const queryString = Object.keys(parameters) + .map((parameter) => { + const value = parameters[parameter]; + // update internal state for UI + this[parameter] = value; + return `${parameter}=${encodeURIComponent(value)}`; + }) + .join('&'); + + // update polling parameters + this.requestData = parameters; + + historyPushState(buildUrlWithCurrentLocation(`?${queryString}`)); + + this.isLoading = true; + }, /** * Handles URL and query parameter changes. * When the user uses the pagination or the tabs, @@ -184,5 +195,23 @@ export default { }) .finally(() => this.store.toggleIsRunningPipeline(false)); }, + onChangePage(page) { + /* URLS parameters are strings, we need to parse to match types */ + let params = { + page: Number(page).toString(), + }; + + if (this.scope) { + params.scope = this.scope; + } + + params = this.onChangeWithFilter(params); + + this.updateContent(params); + }, + + onChangeWithFilter(params) { + return { ...params, ...validateParams(this.requestData) }; + }, }, }; diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_pagination_api_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_pagination_api_mixin.js deleted file mode 100644 index b62fe196a6f..00000000000 --- a/app/assets/javascripts/pipelines/mixins/pipelines_pagination_api_mixin.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * API callbacks for pagination and tabs - * - * Components need to have `scope`, `page` and `requestData` - */ -import { validateParams } from '~/pipelines/utils'; -import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils'; - -export default { - methods: { - onChangeTab(scope) { - if (this.scope === scope) { - return; - } - - let params = { - scope, - page: '1', - }; - - params = this.onChangeWithFilter(params); - - this.updateContent(params); - }, - - onChangePage(page) { - /* URLS parameters are strings, we need to parse to match types */ - let params = { - page: Number(page).toString(), - }; - - if (this.scope) { - params.scope = this.scope; - } - - params = this.onChangeWithFilter(params); - - this.updateContent(params); - }, - - onChangeWithFilter(params) { - return { ...params, ...validateParams(this.requestData) }; - }, - - updateInternalState(parameters) { - // stop polling - this.poll.stop(); - - const queryString = Object.keys(parameters) - .map((parameter) => { - const value = parameters[parameter]; - // update internal state for UI - this[parameter] = value; - return `${parameter}=${encodeURIComponent(value)}`; - }) - .join('&'); - - // update polling parameters - this.requestData = parameters; - - historyPushState(buildUrlWithCurrentLocation(`?${queryString}`)); - - this.isLoading = true; - }, - }, -}; diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue index e4b5a66c3fc..2577664a5e8 100644 --- a/app/assets/javascripts/terraform/components/states_table.vue +++ b/app/assets/javascripts/terraform/components/states_table.vue @@ -80,6 +80,7 @@ export default { lockingState: s__('Terraform|Locking state'), name: s__('Terraform|Name'), pipeline: s__('Terraform|Pipeline'), + removing: s__('Terraform|Removing'), unknownUser: s__('Terraform|Unknown User'), unlockingState: s__('Terraform|Unlocking state'), updatedUser: s__('Terraform|%{user} updated %{timeAgo}'), @@ -141,6 +142,15 @@ export default { </p> </div> + <div v-else-if="item.loadingRemove" class="gl-mx-3"> + <p + class="gl-display-flex gl-justify-content-start gl-align-items-baseline gl-m-0 gl-text-red-500" + > + <gl-loading-icon class="gl-pr-1" /> + {{ $options.i18n.removing }} + </p> + </div> + <div v-else-if="item.lockedAt" :id="`terraformLockedBadgeContainer${item.name}`" diff --git a/app/assets/javascripts/terraform/components/states_table_actions.vue b/app/assets/javascripts/terraform/components/states_table_actions.vue index 65dcc430f9d..c4fd97188de 100644 --- a/app/assets/javascripts/terraform/components/states_table_actions.vue +++ b/app/assets/javascripts/terraform/components/states_table_actions.vue @@ -9,7 +9,7 @@ import { GlModal, GlSprintf, } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { s__, sprintf } from '~/locale'; import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql'; import lockState from '../graphql/mutations/lock_state.mutation.graphql'; import removeState from '../graphql/mutations/remove_state.mutation.graphql'; @@ -52,6 +52,7 @@ export default { ), modalRemove: s__('Terraform|Remove'), remove: s__('Terraform|Remove state file and versions'), + removeSuccessful: s__('Terraform|%{name} successfully removed'), unlock: s__('Terraform|Unlock'), }, computed: { @@ -121,10 +122,13 @@ export default { loadingRemove: true, }); - this.stateActionMutation(removeState); + this.stateActionMutation( + removeState, + sprintf(this.$options.i18n.removeSuccessful, { name: this.state.name }), + ); } }, - stateActionMutation(mutation) { + stateActionMutation(mutation, successMessage = null) { let errorMessages = []; this.$apollo @@ -143,6 +147,10 @@ export default { data?.terraformStateLock?.errors || data?.terraformStateUnlock?.errors || []; + + if (errorMessages.length === 0 && successMessage) { + this.$toast.show(successMessage); + } }) .catch(() => { errorMessages = [this.$options.i18n.errorUpdate]; diff --git a/app/assets/javascripts/terraform/index.js b/app/assets/javascripts/terraform/index.js index f0a924d8a58..2288bab2fa8 100644 --- a/app/assets/javascripts/terraform/index.js +++ b/app/assets/javascripts/terraform/index.js @@ -1,10 +1,12 @@ import { defaultDataIdFromObject } from 'apollo-cache-inmemory'; +import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import TerraformList from './components/terraform_list.vue'; import resolvers from './graphql/resolvers'; +Vue.use(GlToast); Vue.use(VueApollo); export default () => { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 99aa8395f3d..d15794c71b1 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -159,7 +159,7 @@ export default { <div class="rebase-state-find-class-convention media media-body space-children"> <span v-if="rebaseInProgress || isMakingRequest" - class="gl-font-weight-bold gl-ml-0!" + class="gl-font-weight-bold" data-testid="rebase-message" >{{ __('Rebase in progress') }}</span > diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue deleted file mode 100644 index 861661d3519..00000000000 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ /dev/null @@ -1,88 +0,0 @@ -<script> -import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; -import { s__ } from '../../locale'; - -const ICON_ON = 'status_success_borderless'; -const ICON_OFF = 'status_failed_borderless'; -const LABEL_ON = s__('ToggleButton|Toggle Status: ON'); -const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF'); - -export default { - components: { - GlIcon, - GlLoadingIcon, - }, - - model: { - prop: 'value', - event: 'change', - }, - - props: { - name: { - type: String, - required: false, - default: null, - }, - value: { - type: Boolean, - required: false, - default: null, - }, - disabledInput: { - type: Boolean, - required: false, - default: false, - }, - isLoading: { - type: Boolean, - required: false, - default: false, - }, - }, - - computed: { - toggleIcon() { - return this.value ? ICON_ON : ICON_OFF; - }, - ariaLabel() { - return this.value ? LABEL_ON : LABEL_OFF; - }, - }, - - methods: { - toggleFeature() { - if (!this.disabledInput) this.$emit('change', !this.value); - }, - }, -}; -</script> - -<template> - <label class="gl-mt-2"> - <input v-if="name" :name="name" :value="value" type="hidden" /> - <button - type="button" - role="switch" - class="project-feature-toggle" - :aria-label="ariaLabel" - :aria-checked="value" - :class="{ - 'is-checked': value, - 'gl-blue-500': value, - 'is-disabled': disabledInput, - 'is-loading': isLoading, - }" - @click.prevent="toggleFeature" - > - <gl-loading-icon class="loading-icon" /> - <span class="toggle-icon"> - <gl-icon - :size="18" - :name="toggleIcon" - :class="value ? 'gl-text-blue-500' : 'gl-text-gray-400'" - /> - </span> - </button> - </label> -</template> diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 17450da6e68..bedd6891d02 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -177,7 +177,7 @@ class LabelsFinder < UnionFinder end if group? - @projects = if params[:include_subgroups] + @projects = if params[:include_descendant_groups] @projects.in_namespace(group.self_and_descendants.select(:id)) else @projects.in_namespace(group.id) diff --git a/app/graphql/resolvers/group_labels_resolver.rb b/app/graphql/resolvers/group_labels_resolver.rb new file mode 100644 index 00000000000..5c2f950bbc0 --- /dev/null +++ b/app/graphql/resolvers/group_labels_resolver.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Resolvers + class GroupLabelsResolver < LabelsResolver + type Types::LabelType.connection_type, null: true + + argument :include_descendant_groups, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Include labels from descendant groups.', + default_value: false + + argument :only_group_labels, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Include only group level labels.', + default_value: false + end +end diff --git a/app/graphql/resolvers/labels_resolver.rb b/app/graphql/resolvers/labels_resolver.rb new file mode 100644 index 00000000000..1b523b8a240 --- /dev/null +++ b/app/graphql/resolvers/labels_resolver.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Resolvers + class LabelsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + authorize :read_label + + type Types::LabelType.connection_type, null: true + + argument :search_term, GraphQL::STRING_TYPE, + required: false, + description: 'A search term to find labels with.' + + argument :include_ancestor_groups, GraphQL::BOOLEAN_TYPE, + required: false, + description: 'Include labels from ancestor groups.', + default_value: false + + def resolve(**args) + return Label.none if parent.nil? + + authorize!(parent) + + # LabelsFinder uses `search` param, so we transform `search_term` into `search` + args[:search] = args.delete(:search_term) + LabelsFinder.new(current_user, parent_param.merge(args)).execute + end + + private + + def parent + object.respond_to?(:sync) ? object.sync : object + end + + def parent_param + key = case parent + when Group then :group + when Project then :project + else raise "Unexpected parent type: #{parent.class}" + end + + { "#{key}": parent } + end + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 42391ec1d98..0aaeb8d20df 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -107,17 +107,8 @@ module Types field :labels, Types::LabelType.connection_type, null: true, - description: 'Labels available on this group.' do - argument :search_term, GraphQL::STRING_TYPE, - required: false, - description: 'A search term to find labels with.' - end - - def labels(search_term: nil) - LabelsFinder - .new(current_user, group: group, search: search_term) - .execute - end + description: 'Labels available on this group.', + resolver: Resolvers::GroupLabelsResolver def avatar_url object.avatar_url(only_path: false) diff --git a/app/graphql/types/packages/package_type_enum.rb b/app/graphql/types/packages/package_type_enum.rb index 9713c9d49b1..e2b5cf3163e 100644 --- a/app/graphql/types/packages/package_type_enum.rb +++ b/app/graphql/types/packages/package_type_enum.rb @@ -5,7 +5,7 @@ module Types class PackageTypeEnum < BaseEnum PACKAGE_TYPE_NAMES = { pypi: 'PyPI', - npm: 'NPM' + npm: 'npm' }.freeze ::Packages::Package.package_types.keys.each do |package_type| diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 9ef451cbe3c..7205c615271 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -337,17 +337,8 @@ module Types field :labels, Types::LabelType.connection_type, null: true, - description: 'Labels available on this project.' do - argument :search_term, GraphQL::STRING_TYPE, - required: false, - description: 'A search term to find labels with.' - end - - def labels(search_term: nil) - LabelsFinder - .new(current_user, project: project, search: search_term) - .execute - end + description: 'Labels available on this project.', + resolver: Resolvers::LabelsResolver def avatar_url object.avatar_url(only_path: false) diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb index e1cf579eefc..f8314d8b429 100644 --- a/app/models/concerns/enums/ci/pipeline.rb +++ b/app/models/concerns/enums/ci/pipeline.rb @@ -74,7 +74,8 @@ module Enums remote_source: 4, external_project_source: 5, bridge_source: 6, - parameter_source: 7 + parameter_source: 7, + compliance_source: 8 } end end diff --git a/changelogs/unreleased/292713-remove-loading-tf.yml b/changelogs/unreleased/292713-remove-loading-tf.yml new file mode 100644 index 00000000000..e5f11a2cad6 --- /dev/null +++ b/changelogs/unreleased/292713-remove-loading-tf.yml @@ -0,0 +1,5 @@ +--- +title: Display loading when removing Terraform state +merge_request: 53897 +author: +type: changed diff --git a/changelogs/unreleased/add-labels-resolver.yml b/changelogs/unreleased/add-labels-resolver.yml new file mode 100644 index 00000000000..1594c35de19 --- /dev/null +++ b/changelogs/unreleased/add-labels-resolver.yml @@ -0,0 +1,6 @@ +--- +title: Adds only_group_labels and include_ancestor_labels and include_descendant_groups + arguments to the project and group labels resolvers respectively +merge_request: 53639 +author: +type: fixed diff --git a/changelogs/unreleased/count-implicit-auto-devops-inclusions-separately.yml b/changelogs/unreleased/count-implicit-auto-devops-inclusions-separately.yml new file mode 100644 index 00000000000..cbf81e90e8e --- /dev/null +++ b/changelogs/unreleased/count-implicit-auto-devops-inclusions-separately.yml @@ -0,0 +1,5 @@ +--- +title: Track YAML-less Auto DevOps inclusions separately +merge_request: 53383 +author: +type: changed diff --git a/changelogs/unreleased/vs-fix-alignment-rebase-in-progress.yml b/changelogs/unreleased/vs-fix-alignment-rebase-in-progress.yml new file mode 100644 index 00000000000..ed2989dfcb0 --- /dev/null +++ b/changelogs/unreleased/vs-fix-alignment-rebase-in-progress.yml @@ -0,0 +1,5 @@ +--- +title: Fix alignment of 'Rebase in progress' label +merge_request: 54189 +author: +type: fixed diff --git a/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml b/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml index fccebf552a9..50e1e6715df 100644 --- a/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml +++ b/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300400 milestone: '13.9' type: development group: group::continuous integration -default_enabled: false +default_enabled: true diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 6ffe440e7df..c863864f75b 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -3973,7 +3973,7 @@ type ComplianceFramework { """ Full path of the compliance pipeline configuration stored in a project - repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`. + repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`. """ pipelineConfigurationFullPath: String } @@ -4031,7 +4031,7 @@ input ComplianceFrameworkInput { """ Full path of the compliance pipeline configuration stored in a project - repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`. + repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`. """ pipelineConfigurationFullPath: String } @@ -11620,11 +11620,26 @@ type Group { first: Int """ + Include labels from ancestor groups. + """ + includeAncestorGroups: Boolean = false + + """ + Include labels from descendant groups. + """ + includeDescendantGroups: Boolean = false + + """ Returns the last _n_ elements from the list. """ last: Int """ + Include only group level labels. + """ + onlyGroupLabels: Boolean = false + + """ A search term to find labels with. """ searchTerm: String @@ -18179,7 +18194,7 @@ enum PackageTypeEnum { MAVEN """ - Packages from the NPM package manager + Packages from the npm package manager """ NPM @@ -18383,7 +18398,7 @@ type Pipeline { """ Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, - BRIDGE_SOURCE, PARAMETER_SOURCE) + BRIDGE_SOURCE, PARAMETER_SOURCE, COMPLIANCE_SOURCE) """ configSource: PipelineConfigSourceEnum @@ -18654,6 +18669,7 @@ type PipelineCancelPayload { enum PipelineConfigSourceEnum { AUTO_DEVOPS_SOURCE BRIDGE_SOURCE + COMPLIANCE_SOURCE EXTERNAL_PROJECT_SOURCE PARAMETER_SOURCE REMOTE_SOURCE @@ -19834,6 +19850,11 @@ type Project { first: Int """ + Include labels from ancestor groups. + """ + includeAncestorGroups: Boolean = false + + """ Returns the last _n_ elements from the list. """ last: Int diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 379bc2349a8..9fcc113b288 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -10821,7 +10821,7 @@ }, { "name": "pipelineConfigurationFullPath", - "description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.", + "description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`.", "args": [ ], @@ -10991,7 +10991,7 @@ }, { "name": "pipelineConfigurationFullPath", - "description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.", + "description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`.", "type": { "kind": "SCALAR", "name": "String", @@ -31573,6 +31573,46 @@ "description": "Labels available on this group.", "args": [ { + "name": "searchTerm", + "description": "A search term to find labels with.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "includeAncestorGroups", + "description": "Include labels from ancestor groups.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + }, + { + "name": "includeDescendantGroups", + "description": "Include labels from descendant groups.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + }, + { + "name": "onlyGroupLabels", + "description": "Include only group level labels.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + }, + { "name": "after", "description": "Returns the elements in the list that come after the specified cursor.", "type": { @@ -31611,16 +31651,6 @@ "ofType": null }, "defaultValue": null - }, - { - "name": "searchTerm", - "description": "A search term to find labels with.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null } ], "type": { @@ -53370,7 +53400,7 @@ }, { "name": "NPM", - "description": "Packages from the NPM package manager", + "description": "Packages from the npm package manager", "isDeprecated": false, "deprecationReason": null }, @@ -53950,7 +53980,7 @@ }, { "name": "configSource", - "description": "Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE)", + "description": "Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE, COMPLIANCE_SOURCE)", "args": [ ], @@ -54847,6 +54877,12 @@ "description": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "COMPLIANCE_SOURCE", + "description": null, + "isDeprecated": false, + "deprecationReason": null } ], "possibleTypes": null @@ -57712,6 +57748,26 @@ "description": "Labels available on this project.", "args": [ { + "name": "searchTerm", + "description": "A search term to find labels with.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "includeAncestorGroups", + "description": "Include labels from ancestor groups.", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false" + }, + { "name": "after", "description": "Returns the elements in the list that come after the specified cursor.", "type": { @@ -57750,16 +57806,6 @@ "ofType": null }, "defaultValue": null - }, - { - "name": "searchTerm", - "description": "A search term to find labels with.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null } ], "type": { diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 472cb202ea2..c1fe265a821 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -630,7 +630,7 @@ Represents a ComplianceFramework associated with a Project. | `description` | String! | Description of the compliance framework. | | `id` | ID! | Compliance framework ID. | | `name` | String! | Name of the compliance framework. | -| `pipelineConfigurationFullPath` | String | Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`. | +| `pipelineConfigurationFullPath` | String | Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hippa`. | ### ComposerMetadata @@ -2785,7 +2785,7 @@ Information about pagination in a connection.. | `beforeSha` | String | Base SHA of the source branch. | | `cancelable` | Boolean! | Specifies if a pipeline can be canceled. | | `committedAt` | Time | Timestamp of the pipeline's commit. | -| `configSource` | PipelineConfigSourceEnum | Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE) | +| `configSource` | PipelineConfigSourceEnum | Configuration source of the pipeline (UNKNOWN_SOURCE, REPOSITORY_SOURCE, AUTO_DEVOPS_SOURCE, WEBIDE_SOURCE, REMOTE_SOURCE, EXTERNAL_PROJECT_SOURCE, BRIDGE_SOURCE, PARAMETER_SOURCE, COMPLIANCE_SOURCE) | | `coverage` | Float | Coverage percentage. | | `createdAt` | Time! | Timestamp of the pipeline's creation. | | `detailedStatus` | DetailedStatus! | Detailed status of the pipeline. | @@ -5203,7 +5203,7 @@ Rotation length unit of an on-call rotation. | `GENERIC` | Packages from the Generic package manager | | `GOLANG` | Packages from the Golang package manager | | `MAVEN` | Packages from the Maven package manager | -| `NPM` | Packages from the NPM package manager | +| `NPM` | Packages from the npm package manager | | `NUGET` | Packages from the Nuget package manager | | `PYPI` | Packages from the PyPI package manager | | `RUBYGEMS` | Packages from the Rubygems package manager | @@ -5214,6 +5214,7 @@ Rotation length unit of an on-call rotation. | ----- | ----------- | | `AUTO_DEVOPS_SOURCE` | | | `BRIDGE_SOURCE` | | +| `COMPLIANCE_SOURCE` | | | `EXTERNAL_PROJECT_SOURCE` | | | `PARAMETER_SOURCE` | | | `REMOTE_SOURCE` | | diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md index b43fb9a77df..83f85951388 100644 --- a/doc/user/application_security/sast/analyzers.md +++ b/doc/user/application_security/sast/analyzers.md @@ -154,24 +154,24 @@ The [Security Scanner Integration](../../../development/integrations/secure.md) ## Analyzers Data -| Property / Tool | Apex | Bandit | Brakeman | ESLint security | SpotBugs | Flawfinder | Gosec | Kubesec Scanner | MobSF | NodeJsScan | PHP CS Security Audit | Security code Scan (.NET) | Semgrep | Sobelow | -| --------------------------------------- | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :---------------------: | :-------------------------: | :-------------------------: | :----------------: | -| Severity | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ⚠| ✗ | -| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Description | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | -| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| End line | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | -| Start column | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | -| End column | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | -| External ID (for example, CVE) | ✗ | ✗ | ⚠| ✗ | ⚠| ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠| ✗ | -| URLs | ✓ | ✗ | ✓ | ✗ | ⚠| ✗ | ⚠| ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | -| Internal doc/explanation | ✓ | ⚠| ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | -| Solution | ✓ | ✗ | ✗ | ✗ | ⚠| ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠| ✗ | -| Affected item (for example, class or package) | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | -| Confidence | ✗ | ✓ | ✓ | ✗ | ✓ | x | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ⚠| ✓ | -| Source code extract | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | -| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | +| Property / Tool | Apex | Bandit | Brakeman | ESLint security | SpotBugs | Flawfinder | Gosec | Kubesec Scanner | MobSF | NodeJsScan | PHP CS Security Audit | Security code Scan (.NET) | Semgrep | Sobelow | +|--------------------------------|------|--------|----------|-----------------|----------|------------|-------|-----------------|-------|------------|-----------------------|---------------------------|---------|---------| +| Affected item (for example, class or package) | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | +| Confidence | ✗ | ✓ | ✓ | ✗ | ✓ | x | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ⚠| ✓ | +| Description | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | +| End column | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | +| End line | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | +| External ID (for example, CVE) | ✗ | ✗ | ⚠| ✗ | ⚠| ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠| ✗ | +| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Internal doc/explanation | ✓ | ⚠| ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | +| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | +| Severity | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ⚠| ✗ | +| Solution | ✓ | ✗ | ✗ | ✗ | ⚠| ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠| ✗ | +| Source code extract | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | +| Start column | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | +| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| URLs | ✓ | ✗ | ✓ | ✗ | ⚠| ✗ | ⚠| ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | - ✓ => we have that data - ⚠=> we have that data but it's partially reliable, or we need to extract it from unstructured content diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 2c9a32607e1..74c608fcc38 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -657,15 +657,16 @@ license_scanning: The License Compliance job should now use local copies of the License Compliance analyzers to scan your code and generate security reports, without requiring internet access. -Additional configuration may be needed for connecting to -[private Bower registries](#using-private-bower-registries), -[private Bundler registries](#using-private-bundler-registries), -[private Conan registries](#using-private-bower-registries), -[private Go registries](#using-private-go-registries), -[private Maven repositories](#using-private-maven-repositories), -[private npm registries](#using-private-npm-registries), -[private Python repositories](#using-private-python-repositories), -and [private Yarn registries](#using-private-yarn-registries). +Additional configuration may be needed for connecting to private registries for: + +- [Bower](#using-private-bower-registries), +- [Bundler](#using-private-bundler-registries), +- [Conan](#using-private-bower-registries), +- [Go](#using-private-go-registries), +- [Maven repositories](#using-private-maven-repositories), +- [npm](#using-private-npm-registries), +- [Python repositories](#using-private-python-repositories), +- [Yarn](#using-private-yarn-registries). ### SPDX license list name matching diff --git a/lib/gitlab/ci/pipeline/chain/config/content.rb b/lib/gitlab/ci/pipeline/chain/config/content.rb index 5314fd471c3..a7680f6e593 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content.rb @@ -34,16 +34,22 @@ module Gitlab private def find_config - SOURCES.each do |source| + sources.each do |source| config = source.new(@pipeline, @command) return config if config.exists? end nil end + + def sources + SOURCES + end end end end end end end + +Gitlab::Ci::Pipeline::Chain::Config::Content.prepend_if_ee('EE::Gitlab::Ci::Pipeline::Chain::Config::Content') diff --git a/lib/gitlab/ci/pipeline/chain/template_usage.rb b/lib/gitlab/ci/pipeline/chain/template_usage.rb index c1a7b4ed453..2fcf1740b5f 100644 --- a/lib/gitlab/ci/pipeline/chain/template_usage.rb +++ b/lib/gitlab/ci/pipeline/chain/template_usage.rb @@ -19,7 +19,7 @@ module Gitlab def track_event(template) Gitlab::UsageDataCounters::CiTemplateUniqueCounter - .track_unique_project_event(project_id: pipeline.project_id, template: template) + .track_unique_project_event(project_id: pipeline.project_id, template: template, config_source: pipeline.config_source) end def included_templates diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index 10822f943b6..5403017d710 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -7,6 +7,9 @@ module Gitlab class UrlBlocker BlockedUrlError = Class.new(StandardError) + GETADDRINFO_TIMEOUT_SECONDS = 15 + private_constant :GETADDRINFO_TIMEOUT_SECONDS + class << self # Validates the given url according to the constraints specified by arguments. # @@ -110,7 +113,7 @@ module Gitlab end def get_address_info(uri, dns_rebind_protection) - Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM).map do |addr| + Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM, timeout: GETADDRINFO_TIMEOUT_SECONDS).map do |addr| addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr end rescue SocketError diff --git a/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb b/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb index 933e358509b..772a4623280 100644 --- a/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb +++ b/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb @@ -4,6 +4,7 @@ module Gitlab::UsageDataCounters class CiTemplateUniqueCounter REDIS_SLOT = 'ci_templates'.freeze + # NOTE: Events originating from implicit Auto DevOps pipelines get prefixed with `implicit_` TEMPLATE_TO_EVENT = { '5-Minute-Production-App.gitlab-ci.yml' => '5_min_production_app', 'Auto-DevOps.gitlab-ci.yml' => 'auto_devops', @@ -18,19 +19,21 @@ module Gitlab::UsageDataCounters }.freeze class << self - def track_unique_project_event(project_id:, template:) + def track_unique_project_event(project_id:, template:, config_source:) return if Feature.disabled?(:usage_data_track_ci_templates_unique_projects, default_enabled: :yaml) - if event = unique_project_event(template) + if event = unique_project_event(template, config_source) Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event, values: project_id) end end private - def unique_project_event(template) + def unique_project_event(template, config_source) if name = TEMPLATE_TO_EVENT[template] - "p_#{REDIS_SLOT}_#{name}" + prefix = 'implicit_' if config_source.to_s == 'auto_devops_source' + + "p_#{REDIS_SLOT}_#{prefix}#{name}" end end end diff --git a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml new file mode 100644 index 00000000000..9c19c9e8b8c --- /dev/null +++ b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml @@ -0,0 +1,91 @@ +# Implicit Auto DevOps pipeline events +- name: p_ci_templates_implicit_auto_devops + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_implicit_auto_devops_build + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_implicit_auto_devops_deploy + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_implicit_security_sast + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_implicit_security_secret_detection + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +# Explicit include:template pipeline events +- name: p_ci_templates_5_min_production_app + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_auto_devops + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_aws_cf_deploy_ec2 + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_aws_deploy_ecs + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_auto_devops_build + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_auto_devops_deploy + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_auto_devops_deploy_latest + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_security_sast + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_security_secret_detection + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects + +- name: p_ci_templates_terraform_base_latest + category: ci_templates + redis_slot: ci_templates + aggregation: weekly + feature_flag: usage_data_track_ci_templates_unique_projects diff --git a/lib/gitlab/usage_data_counters/known_events/common.yml b/lib/gitlab/usage_data_counters/known_events/common.yml index a877e0dc042..4fe4aedc8c9 100644 --- a/lib/gitlab/usage_data_counters/known_events/common.yml +++ b/lib/gitlab/usage_data_counters/known_events/common.yml @@ -466,57 +466,6 @@ redis_slot: terraform aggregation: weekly feature_flag: usage_data_p_terraform_state_api_unique_users -# CI templates -- name: p_ci_templates_5_min_production_app - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_auto_devops - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_aws_cf_deploy_ec2 - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_aws_deploy_ecs - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_auto_devops_build - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_auto_devops_deploy - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_auto_devops_deploy_latest - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_security_sast - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_security_secret_detection - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects -- name: p_ci_templates_terraform_base_latest - category: ci_templates - redis_slot: ci_templates - aggregation: weekly - feature_flag: usage_data_track_ci_templates_unique_projects # Pipeline Authoring - name: o_pipeline_authoring_unique_users_committing_ciconfigfile category: pipeline_authoring diff --git a/locale/gitlab.pot b/locale/gitlab.pot index aaea0271112..39cfe546066 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -28728,6 +28728,9 @@ msgstr "" msgid "Terraform" msgstr "" +msgid "Terraform|%{name} successfully removed" +msgstr "" + msgid "Terraform|%{number} Terraform report failed to generate" msgid_plural "Terraform|%{number} Terraform reports failed to generate" msgstr[0] "" @@ -28804,6 +28807,9 @@ msgstr "" msgid "Terraform|Remove state file and versions" msgstr "" +msgid "Terraform|Removing" +msgstr "" + msgid "Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete" msgstr "" @@ -30857,12 +30863,6 @@ msgstr "" msgid "Toggle thread" msgstr "" -msgid "ToggleButton|Toggle Status: OFF" -msgstr "" - -msgid "ToggleButton|Toggle Status: ON" -msgstr "" - msgid "Toggled :%{name}: emoji award." msgstr "" diff --git a/spec/features/projects/terraform_spec.rb b/spec/features/projects/terraform_spec.rb index dfa4dad8490..55b906c2bc5 100644 --- a/spec/features/projects/terraform_spec.rb +++ b/spec/features/projects/terraform_spec.rb @@ -68,7 +68,7 @@ RSpec.describe 'Terraform', :js do fill_in "terraform-state-remove-input-#{additional_state.name}", with: additional_state.name click_button 'Remove' - expect(page).not_to have_content(additional_state.name) + expect(page).to have_content("#{additional_state.name} successfully removed") expect { additional_state.reload }.to raise_error ActiveRecord::RecordNotFound end end diff --git a/spec/frontend/issuable_list/components/issuable_item_spec.js b/spec/frontend/issuable_list/components/issuable_item_spec.js index d6e6816086e..987acf559e3 100644 --- a/spec/frontend/issuable_list/components/issuable_item_spec.js +++ b/spec/frontend/issuable_list/components/issuable_item_spec.js @@ -18,6 +18,8 @@ const createComponent = ({ issuableSymbol = '#', issuable = mockIssuable, slots slots, }); +const MOCK_GITLAB_URL = 'http://0.0.0.0:3000'; + describe('IssuableItem', () => { // The mock data is dependent that this is after our default date useFakeDate(2020, 11, 11); @@ -28,7 +30,7 @@ describe('IssuableItem', () => { let wrapper; beforeEach(() => { - gon.gitlab_url = 'http://0.0.0.0:3000'; + gon.gitlab_url = MOCK_GITLAB_URL; wrapper = createComponent(); }); @@ -73,11 +75,11 @@ describe('IssuableItem', () => { describe('isIssuableUrlExternal', () => { it.each` - issuableWebUrl | urlType | returnValue - ${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false} - ${'http://0.0.0.0:3000/gitlab-org/gitlab-test/-/issues/1'} | ${'absolute and internal'} | ${false} - ${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true} - ${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true} + issuableWebUrl | urlType | returnValue + ${'/gitlab-org/gitlab-test/-/issues/2'} | ${'relative'} | ${false} + ${`${MOCK_GITLAB_URL}/gitlab-org/gitlab-test/-/issues/1`} | ${'absolute and internal'} | ${false} + ${'http://jira.atlassian.net/browse/IG-1'} | ${'external'} | ${true} + ${'https://github.com/gitlabhq/gitlabhq/issues/1'} | ${'external'} | ${true} `( 'returns $returnValue when `issuable.webUrl` is $urlType', async ({ issuableWebUrl, returnValue }) => { @@ -217,14 +219,32 @@ describe('IssuableItem', () => { }); describe('template', () => { - it('renders issuable title', () => { - const titleEl = wrapper.find('[data-testid="issuable-title"]'); + it.each` + gitlabWebUrl | webUrl | expectedHref | expectedTarget + ${undefined} | ${`${MOCK_GITLAB_URL}/issue`} | ${`${MOCK_GITLAB_URL}/issue`} | ${undefined} + ${undefined} | ${'https://jira.com/issue'} | ${'https://jira.com/issue'} | ${'_blank'} + ${'/gitlab-org/issue'} | ${'https://jira.com/issue'} | ${'/gitlab-org/issue'} | ${undefined} + `( + 'renders issuable title correctly when `gitlabWebUrl` is `$gitlabWebUrl` and webUrl is `$webUrl`', + async ({ webUrl, gitlabWebUrl, expectedHref, expectedTarget }) => { + wrapper.setProps({ + issuable: { + ...mockIssuable, + webUrl, + gitlabWebUrl, + }, + }); - expect(titleEl.exists()).toBe(true); - expect(titleEl.find(GlLink).attributes('href')).toBe(mockIssuable.webUrl); - expect(titleEl.find(GlLink).attributes('target')).not.toBeDefined(); - expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title); - }); + await wrapper.vm.$nextTick(); + + const titleEl = wrapper.find('[data-testid="issuable-title"]'); + + expect(titleEl.exists()).toBe(true); + expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref); + expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget); + expect(titleEl.find(GlLink).text()).toBe(mockIssuable.title); + }, + ); it('renders checkbox when `showCheckbox` prop is true', async () => { wrapper.setProps({ diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js index 7449ddd3d2a..61f6e9f0f7b 100644 --- a/spec/frontend/terraform/components/states_table_actions_spec.js +++ b/spec/frontend/terraform/components/states_table_actions_spec.js @@ -14,6 +14,7 @@ localVue.use(VueApollo); describe('StatesTableActions', () => { let lockResponse; let removeResponse; + let toast; let unlockResponse; let updateStateResponse; let wrapper; @@ -59,10 +60,13 @@ describe('StatesTableActions', () => { const createComponent = (propsData = defaultProps) => { const apolloProvider = createMockApolloProvider(); + toast = jest.fn(); + wrapper = shallowMount(StateActions, { apolloProvider, localVue, propsData, + mocks: { $toast: { show: toast } }, stubs: { GlDropdown, GlModal, GlSprintf }, }); @@ -83,6 +87,7 @@ describe('StatesTableActions', () => { afterEach(() => { lockResponse = null; removeResponse = null; + toast = null; unlockResponse = null; updateStateResponse = null; wrapper.destroy(); @@ -243,7 +248,6 @@ describe('StatesTableActions', () => { describe('when clicking the remove button', () => { beforeEach(() => { findRemoveBtn().vm.$emit('click'); - return waitForPromises(); }); @@ -254,21 +258,70 @@ describe('StatesTableActions', () => { }); describe('when submitting the remove modal', () => { - it('does not call the remove mutation when state name is missing', async () => { - findRemoveModal().vm.$emit('ok'); - await wrapper.vm.$nextTick(); + describe('when state name is missing', () => { + beforeEach(() => { + findRemoveModal().vm.$emit('ok'); + return waitForPromises(); + }); - expect(removeResponse).not.toHaveBeenCalledWith(); + it('does not call the remove mutation', () => { + expect(removeResponse).not.toHaveBeenCalledWith(); + }); }); - it('calls the remove mutation when state name is present', async () => { - await wrapper.setData({ removeConfirmText: defaultProps.state.name }); + describe('when state name is present', () => { + beforeEach(async () => { + await wrapper.setData({ removeConfirmText: defaultProps.state.name }); + + findRemoveModal().vm.$emit('ok'); - findRemoveModal().vm.$emit('ok'); - await wrapper.vm.$nextTick(); + await waitForPromises(); + }); + + it('calls the remove mutation', () => { + expect(removeResponse).toHaveBeenCalledWith({ stateID: defaultProps.state.id }); + }); + + it('calls the toast action', () => { + expect(toast).toHaveBeenCalledWith(`${defaultProps.state.name} successfully removed`); + }); - expect(removeResponse).toHaveBeenCalledWith({ - stateID: defaultProps.state.id, + it('calls mutations to set loading and errors', () => { + // loading update + expect(updateStateResponse).toHaveBeenNthCalledWith( + 1, + {}, + { + terraformState: { + ...defaultProps.state, + _showDetails: false, + errorMessages: [], + loadingLock: false, + loadingRemove: true, + }, + }, + // Apollo fields + expect.any(Object), + expect.any(Object), + ); + + // final update + expect(updateStateResponse).toHaveBeenNthCalledWith( + 2, + {}, + { + terraformState: { + ...defaultProps.state, + _showDetails: false, + errorMessages: [], + loadingLock: false, + loadingRemove: false, + }, + }, + // Apollo fields + expect.any(Object), + expect.any(Object), + ); }); }); }); diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js index a9aeba202e0..100e577f514 100644 --- a/spec/frontend/terraform/components/states_table_spec.js +++ b/spec/frontend/terraform/components/states_table_spec.js @@ -1,4 +1,4 @@ -import { GlIcon, GlTooltip } from '@gitlab/ui'; +import { GlIcon, GlLoadingIcon, GlTooltip } from '@gitlab/ui'; import { mount } from '@vue/test-utils'; import { useFakeDate } from 'helpers/fake_date'; import StatesTable from '~/terraform/components/states_table.vue'; @@ -92,6 +92,17 @@ describe('StatesTable', () => { }, }, }, + { + _showDetails: false, + errorMessages: [], + name: 'state-5', + loadingLock: false, + loadingRemove: true, + lockedAt: null, + lockedByUser: null, + updatedAt: '2020-10-10T00:00:00Z', + latestVersion: null, + }, ], }; @@ -112,14 +123,15 @@ describe('StatesTable', () => { }); it.each` - name | toolTipText | locked | lineNumber - ${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${0} - ${'state-2'} | ${'Locking state'} | ${false} | ${1} - ${'state-3'} | ${'Unlocking state'} | ${false} | ${2} - ${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${3} + name | toolTipText | locked | loading | lineNumber + ${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0} + ${'state-2'} | ${'Locking state'} | ${false} | ${true} | ${1} + ${'state-3'} | ${'Unlocking state'} | ${false} | ${true} | ${2} + ${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${false} | ${3} + ${'state-5'} | ${'Removing'} | ${false} | ${true} | ${4} `( 'displays the name and locked information "$name" for line "$lineNumber"', - ({ name, toolTipText, locked, lineNumber }) => { + ({ name, toolTipText, locked, loading, lineNumber }) => { const states = wrapper.findAll('[data-testid="terraform-states-table-name"]'); const state = states.at(lineNumber); @@ -127,6 +139,7 @@ describe('StatesTable', () => { expect(state.text()).toContain(name); expect(state.find(GlIcon).exists()).toBe(locked); + expect(state.find(GlLoadingIcon).exists()).toBe(loading); expect(toolTip.exists()).toBe(locked); if (locked) { diff --git a/spec/frontend/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js deleted file mode 100644 index 632e648aadc..00000000000 --- a/spec/frontend/vue_shared/components/toggle_button_spec.js +++ /dev/null @@ -1,96 +0,0 @@ -import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import ToggleButton from '~/vue_shared/components/toggle_button.vue'; - -describe('Toggle Button component', () => { - let wrapper; - - function createComponent(propsData = {}) { - wrapper = shallowMount(ToggleButton, { - propsData, - }); - } - - const findInput = () => wrapper.find('input'); - const findButton = () => wrapper.find('button'); - const findToggleIcon = () => wrapper.find(GlIcon); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('renders input with provided name', () => { - createComponent({ - name: 'foo', - }); - - expect(findInput().attributes('name')).toBe('foo'); - }); - - describe.each` - value | iconName - ${true} | ${'status_success_borderless'} - ${false} | ${'status_failed_borderless'} - `('when `value` prop is `$value`', ({ value, iconName }) => { - beforeEach(() => { - createComponent({ - value, - name: 'foo', - }); - }); - - it('renders input with correct value attribute', () => { - expect(findInput().attributes('value')).toBe(`${value}`); - }); - - it('renders correct icon', () => { - const icon = findToggleIcon(); - expect(icon.isVisible()).toBe(true); - expect(icon.props('name')).toBe(iconName); - expect(findButton().classes('is-checked')).toBe(value); - }); - - describe('when clicked', () => { - it('emits `change` event with correct event', async () => { - findButton().trigger('click'); - await wrapper.vm.$nextTick(); - - expect(wrapper.emitted('change')).toStrictEqual([[!value]]); - }); - }); - }); - - describe('when `disabledInput` prop is `true`', () => { - beforeEach(() => { - createComponent({ - value: true, - disabledInput: true, - }); - }); - - it('renders disabled button', () => { - expect(findButton().classes()).toContain('is-disabled'); - }); - - it('does not emit change event when clicked', async () => { - findButton().trigger('click'); - await wrapper.vm.$nextTick(); - - expect(wrapper.emitted('change')).toBeFalsy(); - }); - }); - - describe('when `isLoading` prop is `true`', () => { - beforeEach(() => { - createComponent({ - value: true, - isLoading: true, - }); - }); - - it('renders loading class', () => { - expect(findButton().classes()).toContain('is-loading'); - }); - }); -}); diff --git a/spec/graphql/resolvers/group_labels_resolver_spec.rb b/spec/graphql/resolvers/group_labels_resolver_spec.rb new file mode 100644 index 00000000000..ed94f12502a --- /dev/null +++ b/spec/graphql/resolvers/group_labels_resolver_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::GroupLabelsResolver do + include GraphqlHelpers + + using RSpec::Parameterized::TableSyntax + + let_it_be(:current_user) { create(:user) } + let_it_be(:group, reload: true) { create(:group, :private) } + let_it_be(:subgroup, reload: true) { create(:group, :private, parent: group) } + let_it_be(:sub_subgroup, reload: true) { create(:group, :private, parent: subgroup) } + let_it_be(:project, reload: true) { create(:project, :private, group: sub_subgroup) } + let_it_be(:label1) { create(:label, project: project, name: 'project feature') } + let_it_be(:label2) { create(:label, project: project, name: 'new project feature') } + let_it_be(:group_label1) { create(:group_label, group: group, name: 'group feature') } + let_it_be(:group_label2) { create(:group_label, group: group, name: 'new group feature') } + let_it_be(:subgroup_label1) { create(:group_label, group: subgroup, name: 'subgroup feature') } + let_it_be(:subgroup_label2) { create(:group_label, group: subgroup, name: 'new subgroup feature') } + let_it_be(:sub_subgroup_label1) { create(:group_label, group: sub_subgroup, name: 'sub_subgroup feature') } + let_it_be(:sub_subgroup_label2) { create(:group_label, group: sub_subgroup, name: 'new sub_subgroup feature') } + + specify do + expect(described_class).to have_nullable_graphql_type(Types::LabelType.connection_type) + end + + describe '#resolve' do + context 'with unauthorized user' do + it 'raises error' do + expect { resolve_labels(subgroup) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'with authorized user' do + it 'does not raise error' do + group.add_guest(current_user) + + expect { resolve_labels(subgroup) }.not_to raise_error + end + end + + context 'without parent' do + it 'returns no labels' do + expect(resolve_labels(nil)).to eq(Label.none) + end + end + + context 'at group level' do + before_all do + group.add_developer(current_user) + end + + # because :include_ancestor_groups, :include_descendant_groups, :only_group_labels default to false + # the `nil` value would be equivalent to passing in `false` so just check for `nil` option + where(:include_ancestor_groups, :include_descendant_groups, :only_group_labels, :search_term, :test) do + nil | nil | nil | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2) } + nil | nil | true | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2) } + nil | true | nil | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2, label1, label2) } + nil | true | true | nil | -> { expect(subject).to contain_exactly(subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) } + true | nil | nil | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2) } + true | nil | true | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2) } + true | true | nil | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2, label1, label2) } + true | true | true | nil | -> { expect(subject).to contain_exactly(group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) } + + nil | nil | nil | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2) } + nil | nil | true | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2) } + nil | true | nil | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2, sub_subgroup_label2, label2) } + nil | true | true | 'new' | -> { expect(subject).to contain_exactly(subgroup_label2, sub_subgroup_label2) } + true | nil | nil | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2) } + true | nil | true | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2) } + true | true | nil | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2, sub_subgroup_label2, label2) } + true | true | true | 'new' | -> { expect(subject).to contain_exactly(group_label2, subgroup_label2, sub_subgroup_label2) } + end + + with_them do + let(:params) do + { + include_ancestor_groups: include_ancestor_groups, + include_descendant_groups: include_descendant_groups, + only_group_labels: only_group_labels, + search_term: search_term + } + end + + subject { resolve_labels(subgroup, params) } + + it { self.instance_exec(&test) } + end + end + end + + def resolve_labels(parent, args = {}, context = { current_user: current_user }) + resolve(described_class, obj: parent, args: args, ctx: context) + end +end diff --git a/spec/graphql/resolvers/labels_resolver_spec.rb b/spec/graphql/resolvers/labels_resolver_spec.rb new file mode 100644 index 00000000000..3d027a6c8d5 --- /dev/null +++ b/spec/graphql/resolvers/labels_resolver_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::LabelsResolver do + include GraphqlHelpers + + using RSpec::Parameterized::TableSyntax + + let_it_be(:current_user) { create(:user) } + let_it_be(:group, reload: true) { create(:group, :private) } + let_it_be(:subgroup, reload: true) { create(:group, :private, parent: group) } + let_it_be(:sub_subgroup, reload: true) { create(:group, :private, parent: subgroup) } + let_it_be(:project, reload: true) { create(:project, :private, group: subgroup) } + let_it_be(:label1) { create(:label, project: project, name: 'project feature') } + let_it_be(:label2) { create(:label, project: project, name: 'new project feature') } + let_it_be(:group_label1) { create(:group_label, group: group, name: 'group feature') } + let_it_be(:group_label2) { create(:group_label, group: group, name: 'new group feature') } + let_it_be(:subgroup_label1) { create(:group_label, group: subgroup, name: 'subgroup feature') } + let_it_be(:subgroup_label2) { create(:group_label, group: subgroup, name: 'new subgroup feature') } + let_it_be(:sub_subgroup_label1) { create(:group_label, group: sub_subgroup, name: 'sub_subgroup feature') } + let_it_be(:sub_subgroup_label2) { create(:group_label, group: sub_subgroup, name: 'new sub_subgroup feature') } + + specify do + expect(described_class).to have_nullable_graphql_type(Types::LabelType.connection_type) + end + + describe '#resolve' do + context 'with unauthorized user' do + it 'returns no labels' do + expect { resolve_labels(project) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'with authorized user' do + it 'returns no labels' do + group.add_guest(current_user) + + expect { resolve_labels(project) }.not_to raise_error + end + end + + context 'without parent' do + it 'returns no labels' do + expect(resolve_labels(nil)).to eq(Label.none) + end + end + + context 'at project level' do + before_all do + group.add_developer(current_user) + end + + # because :include_ancestor_groups, :include_descendant_groups, :only_group_labels default to false + # the `nil` value would be equivalent to passing in `false` so just check for `nil` option + where(:include_ancestor_groups, :include_descendant_groups, :only_group_labels, :search_term, :test) do + nil | nil | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2) } + nil | nil | true | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2) } + nil | true | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) } + nil | true | true | nil | -> { expect(subject).to contain_exactly(label1, label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) } + true | nil | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2) } + true | nil | true | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2) } + true | true | nil | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) } + true | true | true | nil | -> { expect(subject).to contain_exactly(label1, label2, group_label1, group_label2, subgroup_label1, subgroup_label2, sub_subgroup_label1, sub_subgroup_label2) } + + nil | nil | nil | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2) } + nil | nil | true | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2) } + nil | true | nil | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2, sub_subgroup_label2) } + nil | true | true | 'new' | -> { expect(subject).to contain_exactly(label2, subgroup_label2, sub_subgroup_label2) } + true | nil | nil | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2) } + true | nil | true | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2) } + true | true | nil | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2, sub_subgroup_label2) } + true | true | true | 'new' | -> { expect(subject).to contain_exactly(label2, group_label2, subgroup_label2, sub_subgroup_label2) } + end + + with_them do + let(:params) do + { + include_ancestor_groups: include_ancestor_groups, + include_descendant_groups: include_descendant_groups, + only_group_labels: only_group_labels, + search_term: search_term + } + end + + subject { resolve_labels(project, params) } + + it { self.instance_exec(&test) } + end + end + end + + def resolve_labels(parent, args = {}, context = { current_user: current_user }) + resolve(described_class, obj: parent, args: args, ctx: context) + end +end diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb index de19e8b602a..bba702ba3e9 100644 --- a/spec/graphql/types/group_type_spec.rb +++ b/spec/graphql/types/group_type_spec.rb @@ -38,5 +38,7 @@ RSpec.describe GitlabSchema.types['Group'] do it { is_expected.to have_graphql_resolver(Resolvers::GroupMembersResolver) } end - it_behaves_like 'a GraphQL type with labels' + it_behaves_like 'a GraphQL type with labels' do + let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups, :includeDescendantGroups, :onlyGroupLabels] } + end end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 95c835773e1..9579ef8b99b 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -332,7 +332,9 @@ RSpec.describe GitlabSchema.types['Project'] do it { is_expected.to have_graphql_resolver(Resolvers::Terraform::StatesResolver) } end - it_behaves_like 'a GraphQL type with labels' + it_behaves_like 'a GraphQL type with labels' do + let(:labels_resolver_arguments) { [:search_term, :includeAncestorGroups] } + end describe 'jira_imports' do subject { resolve_field(:jira_imports, project) } diff --git a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb index 3616461d94f..cd868a57bbc 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/template_usage_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::TemplateUsage do %w(Template-1 Template-2).each do |expected_template| expect(Gitlab::UsageDataCounters::CiTemplateUniqueCounter).to( receive(:track_unique_project_event) - .with(project_id: project.id, template: expected_template) + .with(project_id: project.id, template: expected_template, config_source: pipeline.config_source) ) end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index fa01d4e48df..20a8f2f6a41 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -160,6 +160,47 @@ RSpec.describe Gitlab::UrlBlocker, :stub_invalid_dns_only do end end end + + context 'when resolving runs into a timeout' do + let(:import_url) { 'http://example.com' } + + subject { described_class.validate!(import_url, dns_rebind_protection: dns_rebind_protection) } + + before do + skip 'timeout is not available' unless timeout_available? + + stub_env('RSPEC_ALLOW_INVALID_URLS', 'false') + stub_const("#{described_class}::GETADDRINFO_TIMEOUT_SECONDS", 0) + end + + context 'with dns rebinding enabled' do + let(:dns_rebind_protection) { true } + + it 'raises an error due to DNS timeout' do + expect { subject }.to raise_error(described_class::BlockedUrlError) + end + end + + context 'with dns rebinding disabled' do + let(:dns_rebind_protection) { false } + + it_behaves_like 'validates URI and hostname' do + let(:expected_uri) { import_url } + let(:expected_hostname) { nil } + end + end + + # Detect whether the timeout option is available. + # + # See https://bugs.ruby-lang.org/issues/15553 + def timeout_available? + Addrinfo.getaddrinfo('localhost', nil, timeout: 0) + + false + rescue SocketError + true + end + end end describe '#blocked_url?' do diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb index 4b07f9143b5..b1d5d106082 100644 --- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb @@ -3,28 +3,88 @@ require 'spec_helper' RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do - let(:project_id) { 1 } - describe '.track_unique_project_event' do - described_class::TEMPLATE_TO_EVENT.keys.each do |template| - context "when given template #{template}" do - it_behaves_like 'tracking unique hll events' do - subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template) } + using RSpec::Parameterized::TableSyntax + + where(:template, :config_source, :expected_event) do + # Implicit Auto DevOps usage + 'Auto-DevOps.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops' + 'Jobs/Build.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_build' + 'Jobs/Deploy.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_deploy' + 'Security/SAST.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_sast' + 'Security/Secret-Detection.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_secret_detection' + # Explicit include:template usage + '5-Minute-Production-App.gitlab-ci.yml' | :repository_source | 'p_ci_templates_5_min_production_app' + 'Auto-DevOps.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops' + 'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_cf_deploy_ec2' + 'AWS/Deploy-ECS.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_deploy_ecs' + 'Jobs/Build.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_build' + 'Jobs/Deploy.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy' + 'Jobs/Deploy.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy_latest' + 'Security/SAST.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_sast' + 'Security/Secret-Detection.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_secret_detection' + 'Terraform/Base.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_terraform_base_latest' + end + + with_them do + it_behaves_like 'tracking unique hll events' do + subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) } + + let(:project_id) { 1 } + let(:target_id) { expected_event } + let(:expected_type) { instance_of(Integer) } + end + end + + context 'known_events coverage tests' do + let(:project_id) { 1 } + let(:config_source) { :repository_source } - let(:target_id) { "p_ci_templates_#{described_class::TEMPLATE_TO_EVENT[template]}" } - let(:expected_type) { instance_of(Integer) } + # These tests help guard against missing "explicit" events in known_events/ci_templates.yml + context 'explicit include:template events' do + described_class::TEMPLATE_TO_EVENT.keys.each do |template| + it "does not raise error for #{template}" do + expect do + described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) + end.not_to raise_error + end + end + end + + # This test is to help guard against missing "implicit" events in known_events/ci_templates.yml + it 'does not raise error for any template in an implicit Auto DevOps pipeline' do + project = create(:project, :auto_devops) + pipeline = double(project: project) + command = double + result = Gitlab::Ci::YamlProcessor.new( + Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops.new(pipeline, command).content, + project: project, + user: double, + sha: double + ).execute + + config_source = :auto_devops_source + + result.included_templates.each do |template| + expect do + described_class.track_unique_project_event(project_id: project.id, template: template, config_source: config_source) + end.not_to raise_error end end end - it 'does not track templates outside of TEMPLATE_TO_EVENT' do - expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to( - receive(:track_event) - ) + context 'templates outside of TEMPLATE_TO_EVENT' do + let(:project_id) { 1 } + let(:config_source) { :repository_source } + Dir.glob(File.join('lib', 'gitlab', 'ci', 'templates', '**'), base: Rails.root) do |template| next if described_class::TEMPLATE_TO_EVENT.key?(template) - described_class.track_unique_project_event(project_id: 1, template: template) + it "does not track #{template}" do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to(receive(:track_event)) + + described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) + end end end end diff --git a/spec/support/helpers/dns_helpers.rb b/spec/support/helpers/dns_helpers.rb index 1795b0a9ac3..52c708e77a5 100644 --- a/spec/support/helpers/dns_helpers.rb +++ b/spec/support/helpers/dns_helpers.rb @@ -12,19 +12,38 @@ module DnsHelpers end def stub_all_dns! - allow(Addrinfo).to receive(:getaddrinfo).with(anything, anything, nil, :STREAM).and_return([]) - allow(Addrinfo).to receive(:getaddrinfo).with(anything, anything, nil, :STREAM, anything, anything).and_return([]) + allow(Addrinfo).to receive(:getaddrinfo).and_return([]) end def stub_invalid_dns! - allow(Addrinfo).to receive(:getaddrinfo).with(/\Afoobar\.\w|(\d{1,3}\.){4,}\d{1,3}\z/i, anything, nil, :STREAM) do - raise SocketError.new("getaddrinfo: Name or service not known") - end + invalid_addresses = %r{ + \A + (?: + foobar\.\w | + (?:\d{1,3}\.){4,}\d{1,3} + ) + \z + }ix + + allow(Addrinfo).to receive(:getaddrinfo) + .with(invalid_addresses, any_args) + .and_raise(SocketError, 'getaddrinfo: Name or service not known') end def permit_local_dns! - local_addresses = /\A(127|10)\.0\.0\.\d{1,3}|(192\.168|172\.16)\.\d{1,3}\.\d{1,3}|0\.0\.0\.0|localhost\z/i - allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM).and_call_original - allow(Addrinfo).to receive(:getaddrinfo).with(local_addresses, anything, nil, :STREAM, anything, anything, any_args).and_call_original + local_addresses = %r{ + \A + (?: + (?:127|10)\.0\.0\.\d{1,3} | + (?:192\.168|172\.16)\.\d{1,3}\.\d{1,3} | + 0\.0\.0\.0 | + localhost + ) + \z + }ix + + allow(Addrinfo).to receive(:getaddrinfo) + .with(local_addresses, any_args) + .and_call_original end end diff --git a/spec/support/helpers/stub_requests.rb b/spec/support/helpers/stub_requests.rb index 473f07dd413..81872b1c908 100644 --- a/spec/support/helpers/stub_requests.rb +++ b/spec/support/helpers/stub_requests.rb @@ -18,14 +18,13 @@ module StubRequests end def stub_dns(url, ip_address:, port: 80) - url = parse_url(url) + url = URI(url) socket = Socket.sockaddr_in(port, ip_address) addr = Addrinfo.new(socket) - # See Gitlab::UrlBlocker allow(Addrinfo).to receive(:getaddrinfo) - .with(url.hostname, url.port, nil, :STREAM) - .and_return([addr]) + .with(url.hostname, url.port, any_args) + .and_return([addr]) end def stub_all_dns(url, ip_address:) @@ -34,22 +33,14 @@ module StubRequests socket = Socket.sockaddr_in(port, ip_address) addr = Addrinfo.new(socket) - # See Gitlab::UrlBlocker - allow(Addrinfo).to receive(:getaddrinfo).and_call_original allow(Addrinfo).to receive(:getaddrinfo) - .with(url.hostname, anything, nil, :STREAM) + .with(url.hostname, any_args) .and_return([addr]) end def stubbed_hostname(url, hostname: IP_ADDRESS_STUB) - url = parse_url(url) + url = URI(url) url.hostname = hostname url.to_s end - - private - - def parse_url(url) - url.is_a?(URI) ? url : URI(url) - end end diff --git a/spec/support/shared_examples/graphql/label_fields.rb b/spec/support/shared_examples/graphql/label_fields.rb index 0d09ab391f1..4159e4e03ab 100644 --- a/spec/support/shared_examples/graphql/label_fields.rb +++ b/spec/support/shared_examples/graphql/label_fields.rb @@ -18,7 +18,7 @@ RSpec.shared_examples 'a GraphQL type with labels' do subject { described_class.fields['labels'] } it { is_expected.to have_graphql_type(Types::LabelType.connection_type) } - it { is_expected.to have_graphql_arguments(:search_term) } + it { is_expected.to have_graphql_arguments(labels_resolver_arguments) } end end |