diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-27 18:09:52 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-27 18:09:52 +0000 |
commit | 28f1931ae84034333abf651ecde369683697ddaf (patch) | |
tree | 7701b2caa8e0e7edfc2d4dca38855c1c03ed8de3 | |
parent | 4c39dd11dcbdab4fdd9424a62320a1fc773c2918 (diff) | |
download | gitlab-ce-28f1931ae84034333abf651ecde369683697ddaf.tar.gz |
Add latest changes from gitlab-org/gitlab@master
65 files changed, 1010 insertions, 313 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/accessors.js b/app/assets/javascripts/pipelines/components/graph/accessors.js new file mode 100644 index 00000000000..31fbab00ee8 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/accessors.js @@ -0,0 +1,10 @@ +import { REST, GRAPHQL } from './constants'; + +export const accessors = { + [REST]: { + groupId: 'id', + }, + [GRAPHQL]: { + groupId: 'name', + }, +}; diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index a580ee11627..e35817a4dbb 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -87,7 +87,7 @@ export default { :title="tooltipText" :class="cssClass" :disabled="isDisabled" - class="js-ci-action ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center" + class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center" @click.stop="onClickAction" > <gl-loading-icon v-if="isLoading" class="js-action-icon-loading" /> diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js index ba1922b6dae..6f0deccfef6 100644 --- a/app/assets/javascripts/pipelines/components/graph/constants.js +++ b/app/assets/javascripts/pipelines/components/graph/constants.js @@ -1,3 +1,6 @@ export const DOWNSTREAM = 'downstream'; export const MAIN = 'main'; export const UPSTREAM = 'upstream'; + +export const REST = 'rest'; +export const GRAPHQL = 'graphql'; diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index b440f300d27..7704f2ba0fd 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,7 +1,5 @@ <script> -import { escape, capitalize } from 'lodash'; import StageColumnComponent from './stage_column_component.vue'; -import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin'; import { MAIN } from './constants'; export default { @@ -9,7 +7,6 @@ export default { components: { StageColumnComponent, }, - mixins: [GraphBundleMixin], props: { isLinkedPipeline: { type: Boolean, @@ -31,96 +28,21 @@ export default { return this.pipeline.stages; }, }, - methods: { - capitalizeStageName(name) { - const escapedName = escape(name); - return capitalize(escapedName); - }, - isFirstColumn(index) { - return index === 0; - }, - stageConnectorClass(index, stage) { - let className; - - // If it's the first stage column and only has one job - if (this.isFirstColumn(index) && stage.groups.length === 1) { - className = 'no-margin'; - } else if (index > 0) { - // If it is not the first column - className = 'left-margin'; - } - - return className; - }, - refreshPipelineGraph() { - this.$emit('refreshPipelineGraph'); - }, - /** - * CSS class is applied: - * - if pipeline graph contains only one stage column component - * - * @param {number} index - * @returns {boolean} - */ - shouldAddRightMargin(index) { - return !(index === this.graph.length - 1); - }, - handleClickedDownstream(pipeline, clickedIndex, downstreamNode) { - /** - * Calculates the margin top of the clicked downstream pipeline by - * subtracting the clicked downstream pipelines offsetTop by it's parent's - * offsetTop and then subtracting 15 - */ - this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15); - - /** - * If the expanded trigger is defined and the id is different than the - * pipeline we clicked, then it means we clicked on a sibling downstream link - * and we want to reset the pipeline store. Triggering the reset without - * this condition would mean not allowing downstreams of downstreams to expand - */ - if (this.expandedDownstream?.id !== pipeline.id) { - this.$emit('onResetDownstream', this.pipeline, pipeline); - } - - this.$emit('onClickDownstreamPipeline', pipeline); - }, - calculateMarginTop(downstreamNode, pixelDiff) { - return `${downstreamNode.offsetTop - downstreamNode.offsetParent.offsetTop - pixelDiff}px`; - }, - hasOnlyOneJob(stage) { - return stage.groups.length === 1; - }, - hasUpstreamColumn(index) { - return index === 0 && this.hasUpstream; - }, - }, }; </script> <template> - <div class="build-content middle-block js-pipeline-graph"> + <div class="js-pipeline-graph"> <div - class="pipeline-visualization pipeline-graph" - :class="{ 'pipeline-tab-content': !isLinkedPipeline }" + class="gl-pipeline-min-h gl-display-flex gl-position-relative gl-overflow-auto gl-bg-gray-10 gl-white-space-nowrap" + :class="{ 'gl-py-5': !isLinkedPipeline }" > - <div> - <ul class="stage-column-list align-top"> - <stage-column-component - v-for="(stage, index) in graph" - :key="stage.name" - :class="{ - 'has-only-one-job': hasOnlyOneJob(stage), - 'gl-mr-26': shouldAddRightMargin(index), - }" - :title="capitalizeStageName(stage.name)" - :groups="stage.groups" - :stage-connector-class="stageConnectorClass(index, stage)" - :is-first-column="isFirstColumn(index)" - :action="stage.status.action" - @refreshPipelineGraph="refreshPipelineGraph" - /> - </ul> - </div> + <stage-column-component + v-for="stage in graph" + :key="stage.name" + :title="stage.name" + :groups="stage.groups" + :action="stage.status.action" + /> </div> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue index c1a939af6d3..2ef8940cf65 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_legacy.vue @@ -1,7 +1,7 @@ <script> import { escape, capitalize } from 'lodash'; import { GlLoadingIcon } from '@gitlab/ui'; -import StageColumnComponent from './stage_column_component.vue'; +import StageColumnComponentLegacy from './stage_column_component_legacy.vue'; import GraphWidthMixin from '../../mixins/graph_width_mixin'; import LinkedPipelinesColumn from './linked_pipelines_column.vue'; import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin'; @@ -10,7 +10,7 @@ import { UPSTREAM, DOWNSTREAM, MAIN } from './constants'; export default { name: 'PipelineGraphLegacy', components: { - StageColumnComponent, + StageColumnComponentLegacy, GlLoadingIcon, LinkedPipelinesColumn, }, @@ -220,7 +220,7 @@ export default { }" class="stage-column-list align-top" > - <stage-column-component + <stage-column-component-legacy v-for="(stage, index) in graph" :key="stage.name" :class="{ diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index 49591a80752..38d48517bd6 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -44,17 +44,19 @@ export default { type="button" data-toggle="dropdown" data-display="static" - class="dropdown-menu-toggle build-content" + class="dropdown-menu-toggle build-content gl-build-content" > - <ci-icon :status="group.status" /> + <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> + <span class="gl-display-flex gl-align-items-center"> + <ci-icon :status="group.status" :size="24" /> - <span - class="gl-text-truncate mw-70p gl-pl-2 gl-display-inline-block gl-vertical-align-bottom" - > - {{ group.name }} - </span> + <span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block"> + {{ group.name }} + </span> + </span> - <span class="dropdown-counter-badge"> {{ group.size }} </span> + <span class="gl-font-weight-100 gl-font-size-lg gl-pr-2"> {{ group.size }} </span> + </div> </button> <ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"> diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index 4ed0aae0d1e..ef443d93c62 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_item.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue @@ -129,19 +129,23 @@ export default { }; </script> <template> - <div class="ci-job-component" data-qa-selector="job_item_container"> + <div + class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between" + data-qa-selector="job_item_container" + > <gl-link v-if="status.has_details" v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }" :href="status.details_path" :title="tooltipText" :class="jobClasses" - class="js-pipeline-graph-job-link qa-job-link menu-item" + class="js-pipeline-graph-job-link qa-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none + gl-focus-text-decoration-none" data-testid="job-with-link" @click.stop="hideTooltips" @mouseout="hideTooltips" > - <job-name-component :name="job.name" :status="job.status" /> + <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> </gl-link> <div @@ -153,7 +157,7 @@ export default { data-testid="job-without-link" @mouseout="hideTooltips" > - <job-name-component :name="job.name" :status="job.status" /> + <job-name-component :name="job.name" :status="job.status" :icon-size="24" /> </div> <action-component diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue index 1b71949784a..23a38fc053e 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue @@ -16,18 +16,22 @@ export default { type: String, required: true, }, - status: { type: Object, required: true, }, + iconSize: { + type: Number, + required: false, + default: 16, + }, }, }; </script> <template> - <span class="ci-job-name-component mw-100"> - <ci-icon :status="status" /> - <span class="gl-text-truncate mw-70p gl-pl-2 gl-display-inline-block gl-vertical-align-bottom"> + <span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center"> + <ci-icon :size="iconSize" :status="status" /> + <span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block"> {{ name }} </span> </span> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 258b6bf6b6d..5e2d79758e1 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,17 +1,19 @@ <script> -import { isEmpty, escape } from 'lodash'; -import stageColumnMixin from '../../mixins/stage_column_mixin'; +import { capitalize, escape, isEmpty } from 'lodash'; +import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue'; import JobItem from './job_item.vue'; import JobGroupDropdown from './job_group_dropdown.vue'; import ActionComponent from './action_component.vue'; +import { GRAPHQL } from './constants'; +import { accessors } from './accessors'; export default { components: { - JobItem, - JobGroupDropdown, ActionComponent, + JobGroupDropdown, + JobItem, + MainGraphWrapper, }, - mixins: [stageColumnMixin], props: { title: { type: String, @@ -21,16 +23,6 @@ export default { type: Array, required: true, }, - isFirstColumn: { - type: Boolean, - required: false, - default: false, - }, - stageConnectorClass: { - type: String, - required: false, - default: '', - }, action: { type: Object, required: false, @@ -47,62 +39,67 @@ export default { default: () => ({}), }, }, + accessors, + titleClasses: [ + 'gl-font-weight-bold', + 'gl-pipeline-job-width', + 'gl-text-truncate', + 'gl-line-height-36', + 'gl-pl-3', + ], computed: { + formattedTitle() { + return capitalize(escape(this.title)); + }, hasAction() { return !isEmpty(this.action); }, }, methods: { + getAccessor(property) { + return accessors[GRAPHQL][property]; + }, groupId(group) { return `ci-badge-${escape(group.name)}`; }, - pipelineActionRequestComplete() { - this.$emit('refreshPipelineGraph'); - }, }, }; </script> <template> - <li :class="stageConnectorClass" class="stage-column"> - <div class="stage-name position-relative" data-testid="stage-column-title"> - {{ title }} - <action-component - v-if="hasAction" - :action-icon="action.icon" - :tooltip-text="action.title" - :link="action.path" - class="js-stage-action stage-action rounded" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - </div> - - <div class="builds-container"> - <ul> - <li - v-for="(group, index) in groups" - :id="groupId(group)" - :key="group.id" - :class="buildConnnectorClass(index)" - class="build" - > - <div class="curve"></div> - - <job-item - v-if="group.size === 1" - :job="group.jobs[0]" - :job-hovered="jobHovered" - :pipeline-expanded="pipelineExpanded" - css-class-job-name="build-content" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - - <job-group-dropdown - v-if="group.size > 1" - :group="group" - @pipelineActionRequestComplete="pipelineActionRequestComplete" - /> - </li> - </ul> - </div> - </li> + <main-graph-wrapper> + <template #stages> + <div + data-testid="stage-column-title" + class="gl-display-flex gl-justify-content-space-between gl-relative" + :class="$options.titleClasses" + > + <div>{{ formattedTitle }}</div> + <action-component + v-if="hasAction" + :action-icon="action.icon" + :tooltip-text="action.title" + :link="action.path" + class="js-stage-action stage-action rounded" + /> + </div> + </template> + <template #jobs> + <div + v-for="group in groups" + :id="groupId(group)" + :key="group[getAccessor('groupId')]" + data-testid="stage-column-group" + class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width" + > + <job-item + v-if="group.size === 1" + :job="group.jobs[0]" + :job-hovered="jobHovered" + :pipeline-expanded="pipelineExpanded" + css-class-job-name="gl-build-content" + /> + <job-group-dropdown v-else :group="group" /> + </div> + </template> + </main-graph-wrapper> </template> diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue new file mode 100644 index 00000000000..258b6bf6b6d --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component_legacy.vue @@ -0,0 +1,108 @@ +<script> +import { isEmpty, escape } from 'lodash'; +import stageColumnMixin from '../../mixins/stage_column_mixin'; +import JobItem from './job_item.vue'; +import JobGroupDropdown from './job_group_dropdown.vue'; +import ActionComponent from './action_component.vue'; + +export default { + components: { + JobItem, + JobGroupDropdown, + ActionComponent, + }, + mixins: [stageColumnMixin], + props: { + title: { + type: String, + required: true, + }, + groups: { + type: Array, + required: true, + }, + isFirstColumn: { + type: Boolean, + required: false, + default: false, + }, + stageConnectorClass: { + type: String, + required: false, + default: '', + }, + action: { + type: Object, + required: false, + default: () => ({}), + }, + jobHovered: { + type: String, + required: false, + default: '', + }, + pipelineExpanded: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + hasAction() { + return !isEmpty(this.action); + }, + }, + methods: { + groupId(group) { + return `ci-badge-${escape(group.name)}`; + }, + pipelineActionRequestComplete() { + this.$emit('refreshPipelineGraph'); + }, + }, +}; +</script> +<template> + <li :class="stageConnectorClass" class="stage-column"> + <div class="stage-name position-relative" data-testid="stage-column-title"> + {{ title }} + <action-component + v-if="hasAction" + :action-icon="action.icon" + :tooltip-text="action.title" + :link="action.path" + class="js-stage-action stage-action rounded" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </div> + + <div class="builds-container"> + <ul> + <li + v-for="(group, index) in groups" + :id="groupId(group)" + :key="group.id" + :class="buildConnnectorClass(index)" + class="build" + > + <div class="curve"></div> + + <job-item + v-if="group.size === 1" + :job="group.jobs[0]" + :job-hovered="jobHovered" + :pipeline-expanded="pipelineExpanded" + css-class-job-name="build-content" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + + <job-group-dropdown + v-if="group.size > 1" + :group="group" + @pipelineActionRequestComplete="pipelineActionRequestComplete" + /> + </li> + </ul> + </div> + </li> +</template> diff --git a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue new file mode 100644 index 00000000000..205ee0fb414 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue @@ -0,0 +1,32 @@ +<script> +export default { + props: { + stageClasses: { + type: String, + required: false, + default: '', + }, + jobClasses: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> +<template> + <div> + <div + class="gl-display-flex gl-align-items-center gl-w-full gl-px-8 gl-py-4 gl-mb-5" + :class="stageClasses" + > + <slot name="stages"> </slot> + </div> + <div + class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8" + :class="jobClasses" + > + <slot name="jobs"> </slot> + </div> + </div> +</template> diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss index 1de66aa73da..f50b2d816c3 100644 --- a/app/assets/stylesheets/page_bundles/pipeline.scss +++ b/app/assets/stylesheets/page_bundles/pipeline.scss @@ -129,6 +129,51 @@ overflow: auto; } +// Move to Gitlab UI +.gl-font-weight-100 { + font-weight: 100; +} + +.gl-active-text-decoration-none:active, +.gl-focus-text-decoration-none:focus { + text-decoration: none; +} + +// These are single-value classes to use with utility-class style CSS +// but to still access this variable. Do not add other styles. +.gl-pipeline-min-h { + min-height: $dropdown-max-height-lg; +} + +.gl-pipeline-job-width { + width: 186px; +} + +.gl-pipeline-title-width { + width: 176px; +} + +.gl-build-content { + @include build-content(); +} + +.gl-ci-action-icon-container { + position: absolute; + right: 5px; + top: 50% !important; + transform: translateY(-50%); + + // Action Icons in big pipeline-graph nodes + &.ci-action-icon-wrapper { + height: 30px; + width: 30px; + border-radius: 100%; + display: block; + padding: 0; + line-height: 0; + } +} + // Pipeline graph, used at // app/assets/javascripts/pipelines/components/graph/graph_component.vue .pipeline-graph { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 429181c2ad4..f8b96772ed7 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -69,7 +69,7 @@ } .btn { - margin: 10px 0 0; + margin-top: 10px; } } } diff --git a/app/models/concerns/enums/ci/pipeline.rb b/app/models/concerns/enums/ci/pipeline.rb index bb8df37f649..28eb4c11746 100644 --- a/app/models/concerns/enums/ci/pipeline.rb +++ b/app/models/concerns/enums/ci/pipeline.rb @@ -9,7 +9,8 @@ module Enums { unknown_failure: 0, config_error: 1, - external_validation_failure: 2 + external_validation_failure: 2, + deployments_limit_exceeded: 23 } end diff --git a/app/models/packages/package_file.rb b/app/models/packages/package_file.rb index d68f75140ac..e8d1dd1e8c4 100644 --- a/app/models/packages/package_file.rb +++ b/app/models/packages/package_file.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Packages::PackageFile < ApplicationRecord include UpdateProjectStatistics + include FileStoreMounter delegate :project, :project_id, to: :package delegate :conan_file_type, to: :conan_file_metadatum @@ -35,20 +36,12 @@ class Packages::PackageFile < ApplicationRecord .where(packages_conan_file_metadata: { conan_package_reference: conan_package_reference }) end - mount_uploader :file, Packages::PackageFileUploader - - after_save :update_file_metadata, if: :saved_change_to_file? + mount_file_store_uploader Packages::PackageFileUploader update_project_statistics project_statistics_name: :packages_size before_save :update_size_from_file - def update_file_metadata - # The file.object_store is set during `uploader.store!` - # which happens after object is inserted/updated - self.update_column(:file_store, file.object_store) - end - def download_path Gitlab::Routing.url_helpers.download_project_package_file_path(project, self) end diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb index da610f13899..f3bb63b31c3 100644 --- a/app/presenters/ci/pipeline_presenter.rb +++ b/app/presenters/ci/pipeline_presenter.rb @@ -10,7 +10,8 @@ module Ci def self.failure_reasons { unknown_failure: 'Unknown pipeline failure!', config_error: 'CI/CD YAML configuration error!', - external_validation_failure: 'External pipeline validation failed!' } + external_validation_failure: 'External pipeline validation failed!', + deployments_limit_exceeded: 'Pipeline deployments limit exceeded!' } end presents :pipeline diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index e3bab2de44e..32670d81906 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -18,6 +18,7 @@ module Ci Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules, Gitlab::Ci::Pipeline::Chain::Seed, Gitlab::Ci::Pipeline::Chain::Limit::Size, + Gitlab::Ci::Pipeline::Chain::Limit::Deployments, Gitlab::Ci::Pipeline::Chain::Validate::External, Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::StopDryRun, diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 70c1a939878..58b364f52d4 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -25,7 +25,7 @@ .detail-page-header-actions.js-issuable-actions .clearfix.issue-btn-group.dropdown - %button.gl-button.btn.btn-default.float-left{ type: "button", data: { toggle: "dropdown" } } + %button.gl-button.btn.btn-default.float-left.gl-display-md-none{ type: "button", data: { toggle: "dropdown" } } Options = sprite_icon('chevron-down', css_class: 'gl-text-gray-500') .dropdown-menu.dropdown-menu-right diff --git a/changelogs/unreleased/254979-double-border-split-button.yml b/changelogs/unreleased/254979-double-border-split-button.yml new file mode 100644 index 00000000000..afa69697166 --- /dev/null +++ b/changelogs/unreleased/254979-double-border-split-button.yml @@ -0,0 +1,5 @@ +--- +title: Fixed double-border style on WebIDE button +merge_request: 48605 +author: +type: fixed diff --git a/changelogs/unreleased/add-limits-for-deployments-per-pipeline.yml b/changelogs/unreleased/add-limits-for-deployments-per-pipeline.yml new file mode 100644 index 00000000000..64e123e0d00 --- /dev/null +++ b/changelogs/unreleased/add-limits-for-deployments-per-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Limit maximum deployments per pipeline to 500 +merge_request: 46931 +author: +type: added diff --git a/changelogs/unreleased/ak-add-index-on-builds.yml b/changelogs/unreleased/ak-add-index-on-builds.yml new file mode 100644 index 00000000000..498801cf8c0 --- /dev/null +++ b/changelogs/unreleased/ak-add-index-on-builds.yml @@ -0,0 +1,5 @@ +--- +title: Adds id desc to index_ci_builds_on_runner_id_and_id_desc +merge_request: 48241 +author: +type: fixed diff --git a/db/migrate/20201030223933_add_ci_pipeline_deployments_to_plan_limits.rb b/db/migrate/20201030223933_add_ci_pipeline_deployments_to_plan_limits.rb new file mode 100644 index 00000000000..60f0ff9d6ed --- /dev/null +++ b/db/migrate/20201030223933_add_ci_pipeline_deployments_to_plan_limits.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddCiPipelineDeploymentsToPlanLimits < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + add_column :plan_limits, :ci_pipeline_deployments, :integer, default: 500, null: false + end +end diff --git a/db/post_migrate/20201120140210_add_runner_id_and_id_desc_index_to_ci_builds.rb b/db/post_migrate/20201120140210_add_runner_id_and_id_desc_index_to_ci_builds.rb new file mode 100644 index 00000000000..5eda0e25dbe --- /dev/null +++ b/db/post_migrate/20201120140210_add_runner_id_and_id_desc_index_to_ci_builds.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddRunnerIdAndIdDescIndexToCiBuilds < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + NEW_INDEX = 'index_ci_builds_on_runner_id_and_id_desc' + OLD_INDEX = 'index_ci_builds_on_runner_id' + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_builds, %i[runner_id id], name: NEW_INDEX, order: { id: :desc } + remove_concurrent_index_by_name :ci_builds, OLD_INDEX + end + + def down + add_concurrent_index :ci_builds, %i[runner_id], name: OLD_INDEX + remove_concurrent_index_by_name :ci_builds, NEW_INDEX + end +end diff --git a/db/schema_migrations/20201030223933 b/db/schema_migrations/20201030223933 new file mode 100644 index 00000000000..2fb5f394989 --- /dev/null +++ b/db/schema_migrations/20201030223933 @@ -0,0 +1 @@ +a3aa783f2648a95e3ff8b503ef15b8153759c74ac85b30bf94e39710824e57b0
\ No newline at end of file diff --git a/db/schema_migrations/20201120140210 b/db/schema_migrations/20201120140210 new file mode 100644 index 00000000000..5a281f95f5d --- /dev/null +++ b/db/schema_migrations/20201120140210 @@ -0,0 +1 @@ +6b88d79aa8d373fa1d9aa2698a9d20c09aff14ef16af4c123abd4e7c98e41311
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a895fd6414e..d75d3d6f5c9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14797,7 +14797,8 @@ CREATE TABLE plan_limits ( golang_max_file_size bigint DEFAULT 104857600 NOT NULL, debian_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL, project_feature_flags integer DEFAULT 200 NOT NULL, - ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL + ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL, + ci_pipeline_deployments integer DEFAULT 500 NOT NULL ); CREATE SEQUENCE plan_limits_id_seq @@ -20482,7 +20483,7 @@ CREATE INDEX index_ci_builds_on_protected ON ci_builds USING btree (protected); CREATE INDEX index_ci_builds_on_queued_at ON ci_builds USING btree (queued_at); -CREATE INDEX index_ci_builds_on_runner_id ON ci_builds USING btree (runner_id); +CREATE INDEX index_ci_builds_on_runner_id_and_id_desc ON ci_builds USING btree (runner_id, id DESC); CREATE INDEX index_ci_builds_on_stage_id ON ci_builds USING btree (stage_id); diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index e7cd54c0e83..f202a912696 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -250,6 +250,29 @@ Plan.default.actual_limits.update!(ci_active_jobs: 500) Set the limit to `0` to disable it. +### Maximum number of deployment jobs in a pipeline + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46931) in GitLab 13.7. + +You can limit the maximum number of deployment jobs in a pipeline. A deployment is +any job with an [`environment`](../ci/environments/index.md) specified. The number +of deployments in a pipeline is checked at pipeline creation. Pipelines that have +too many deployments fail with a `deployments_limit_exceeded` error. + +The default limit is 500 for all [self-managed and GitLab.com plans](https://about.gitlab.com/pricing/). + +To change the limit on a self-managed installation, change the `default` plan limit with the following +[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session) command: + +```ruby +# If limits don't exist for the default plan, you can create one with: +# Plan.default.create_limits! + +Plan.default.actual_limits.update!(ci_pipeline_deployments: 500) +``` + +Set the limit to `0` to disable it. + ### Number of CI/CD subscriptions to a project > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9045) in GitLab 12.9. diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 3548a11febe..b692ca1d288 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/README.md b/doc/integration/README.md index 396090eb420..3843c451e6b 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments comments: false --- diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md index 2307fdc5ac3..c04ff31b10a 100644 --- a/doc/integration/akismet.md +++ b/doc/integration/akismet.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md index d479e9ead3f..7e531682faf 100644 --- a/doc/integration/auth0.md +++ b/doc/integration/auth0.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/azure.md b/doc/integration/azure.md index b5b809b6a0a..f22a94a01c7 100644 --- a/doc/integration/azure.md +++ b/doc/integration/azure.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index bf8c9a4fc20..9c4a8e27747 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/cas.md b/doc/integration/cas.md index 8bb25f4f6af..5a198e85f5c 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 7c6048d7683..215a2a8c21d 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md index d16bfb118db..b86958726a7 100644 --- a/doc/integration/facebook.md +++ b/doc/integration/facebook.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/github.md b/doc/integration/github.md index 7634afa65ea..61bcb8a25b3 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 0b249e05dca..37c91aedb15 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md index 449d0906601..1158d6c9bc8 100644 --- a/doc/integration/gmail_action_buttons_for_gitlab.md +++ b/doc/integration/gmail_action_buttons_for_gitlab.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/google.md b/doc/integration/google.md index 5c174862f4e..cd00c854fea 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md index 1d33eeb8b25..70a7f417c75 100644 --- a/doc/integration/jenkins.md +++ b/doc/integration/jenkins.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jenkins_deprecated.md b/doc/integration/jenkins_deprecated.md index 07cd1b9fcb9..f023dbaef41 100644 --- a/doc/integration/jenkins_deprecated.md +++ b/doc/integration/jenkins_deprecated.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/oauth2_generic.md b/doc/integration/oauth2_generic.md index ce4a18a84a1..6cef4b0294c 100644 --- a/doc/integration/oauth2_generic.md +++ b/doc/integration/oauth2_generic.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md index 2347a507846..6e26aa947da 100644 --- a/doc/integration/oauth_provider.md +++ b/doc/integration/oauth_provider.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 33d9974873e..c3f78015590 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md index 742a93865ee..0287bd4bcbd 100644 --- a/doc/integration/openid_connect_provider.md +++ b/doc/integration/openid_connect_provider.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md index 606c791fab3..61b60b7962d 100644 --- a/doc/integration/recaptcha.md +++ b/doc/integration/recaptcha.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md index 76ba1225223..ae1fc1ed5a2 100644 --- a/doc/integration/salesforce.md +++ b/doc/integration/salesforce.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md index b070e9c5ffc..7a4b8d982e7 100644 --- a/doc/integration/shibboleth.md +++ b/doc/integration/shibboleth.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index 5f58a5a8e88..6820ff8a0aa 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md index 49db107fa4e..7545afcf06f 100644 --- a/doc/integration/trello_power_up.md +++ b/doc/integration/trello_power_up.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index 945e057faf5..8404352d0e9 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/ewm.md b/doc/user/project/integrations/ewm.md index 5401b1bfd43..58507ded499 100644 --- a/doc/user/project/integrations/ewm.md +++ b/doc/user/project/integrations/ewm.md @@ -1,6 +1,6 @@ --- -stage: none -group: unassigned +stage: Create +group: Ecosystem info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/lib/gitlab/ci/limit.rb b/lib/gitlab/ci/limit.rb new file mode 100644 index 00000000000..c22a3c503d5 --- /dev/null +++ b/lib/gitlab/ci/limit.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + ## + # Abstract base class for CI/CD Quotas + # + class Limit + LimitExceededError = Class.new(StandardError) + + def initialize(_context, _resource) + end + + def enabled? + raise NotImplementedError + end + + def exceeded? + raise NotImplementedError + end + + def message + raise NotImplementedError + end + + def log_error!(extra_context = {}) + error = LimitExceededError.new(message) + # TODO: change this to Gitlab::ErrorTracking.log_exception(error, extra_context) + # https://gitlab.com/gitlab-org/gitlab/issues/32906 + ::Gitlab::ErrorTracking.track_exception(error, extra_context) + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/limit/deployments.rb b/lib/gitlab/ci/pipeline/chain/limit/deployments.rb new file mode 100644 index 00000000000..d684eedcaac --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/limit/deployments.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Limit + class Deployments < Chain::Base + extend ::Gitlab::Utils::Override + include ::Gitlab::Ci::Pipeline::Chain::Helpers + + attr_reader :limit + private :limit + + def initialize(*) + super + + @limit = ::Gitlab::Ci::Pipeline::Quota::Deployments + .new(project.namespace, pipeline, command) + end + + override :perform! + def perform! + return unless limit.exceeded? + + limit.log_error!(project_id: project.id, plan: project.actual_plan_name) + error(limit.message, drop_reason: :deployments_limit_exceeded) + end + + override :break? + def break? + limit.exceeded? + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/quota/deployments.rb b/lib/gitlab/ci/pipeline/quota/deployments.rb new file mode 100644 index 00000000000..59d695c61c1 --- /dev/null +++ b/lib/gitlab/ci/pipeline/quota/deployments.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Quota + class Deployments < ::Gitlab::Ci::Limit + include ::Gitlab::Utils::StrongMemoize + include ActionView::Helpers::TextHelper + + def initialize(namespace, pipeline, command) + @namespace = namespace + @pipeline = pipeline + @command = command + end + + def enabled? + limit > 0 + end + + def exceeded? + return false unless enabled? + + pipeline_deployment_count > limit + end + + def message + return unless exceeded? + + "Pipeline has too many deployments! Requested #{pipeline_deployment_count}, but the limit is #{limit}." + end + + private + + def pipeline_deployment_count + strong_memoize(:pipeline_deployment_count) do + @command.stage_seeds.sum do |stage_seed| + stage_seed.seeds.count do |build_seed| + build_seed.attributes[:environment].present? + end + end + end + end + + def limit + strong_memoize(:limit) do + @namespace.actual_limits.ci_pipeline_deployments + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml index 3b87d53f165..895e6e8ea6d 100644 --- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml @@ -2,6 +2,8 @@ test: variables: POSTGRES_VERSION: 9.6.16 POSTGRES_DB: test + POSTGRES_USER: user + POSTGRES_PASSWORD: testing-password services: - "postgres:${POSTGRES_VERSION}" stage: test diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index 95bc90f9dad..3ebafb5c5e4 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -48,23 +48,15 @@ module Gitlab @finished_at ? (@finished_at - @started_at) : 0.0 end - def thread_cpu_duration - System.thread_cpu_duration(@thread_cputime_start) - end - def run Thread.current[THREAD_KEY] = self @started_at = System.monotonic_time - @thread_cputime_start = System.thread_cpu_time yield ensure @finished_at = System.monotonic_time - observe(:gitlab_transaction_cputime_seconds, thread_cpu_duration) do - buckets SMALL_BUCKETS - end observe(:gitlab_transaction_duration_seconds, duration) do buckets SMALL_BUCKETS end diff --git a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js index bdfe546f0e4..3313f71e102 100644 --- a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js +++ b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js @@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils'; import { setHTMLFixture } from 'helpers/fixtures'; import PipelineStore from '~/pipelines/stores/pipeline_store'; import GraphComponentLegacy from '~/pipelines/components/graph/graph_component_legacy.vue'; -import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; +import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue'; import linkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; import graphJSON from './mock_data_legacy'; import linkedPipelineJSON from './linked_pipelines_mock_data'; @@ -16,7 +16,7 @@ describe('graph component', () => { const findExpandPipelineBtn = () => wrapper.find('[data-testid="expandPipelineButton"]'); const findAllExpandPipelineBtns = () => wrapper.findAll('[data-testid="expandPipelineButton"]'); - const findStageColumns = () => wrapper.findAll(StageColumnComponent); + const findStageColumns = () => wrapper.findAll(StageColumnComponentLegacy); const findStageColumnAt = i => findStageColumns().at(i); beforeEach(() => { diff --git a/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js b/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js new file mode 100644 index 00000000000..463e4c12c7d --- /dev/null +++ b/spec/frontend/pipelines/graph/stage_column_component_legacy_spec.js @@ -0,0 +1,135 @@ +import { shallowMount } from '@vue/test-utils'; +import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue'; + +describe('stage column component', () => { + const mockJob = { + id: 4250, + name: 'test', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4250', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4250/retry', + method: 'post', + }, + }, + }; + + let wrapper; + + beforeEach(() => { + const mockGroups = []; + for (let i = 0; i < 3; i += 1) { + const mockedJob = { ...mockJob }; + mockedJob.id += i; + mockGroups.push(mockedJob); + } + + wrapper = shallowMount(StageColumnComponentLegacy, { + propsData: { + title: 'foo', + groups: mockGroups, + hasTriggeredBy: false, + }, + }); + }); + + it('should render provided title', () => { + expect( + wrapper + .find('.stage-name') + .text() + .trim(), + ).toBe('foo'); + }); + + it('should render the provided groups', () => { + expect(wrapper.findAll('.builds-container > ul > li').length).toBe( + wrapper.props('groups').length, + ); + }); + + describe('jobId', () => { + it('escapes job name', () => { + wrapper = shallowMount(StageColumnComponentLegacy, { + propsData: { + groups: [ + { + id: 4259, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + label: 'success', + tooltip: '<img src=x onerror=alert(document.domain)>', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + }, + }); + + expect(wrapper.find('.builds-container li').attributes('id')).toBe( + 'ci-badge-<img src=x onerror=alert(document.domain)>', + ); + }); + }); + + describe('with action', () => { + it('renders action button', () => { + wrapper = shallowMount(StageColumnComponentLegacy, { + propsData: { + groups: [ + { + id: 4259, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + label: 'success', + tooltip: '<img src=x onerror=alert(document.domain)>', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + action: { + icon: 'play', + title: 'Play all', + path: 'action', + }, + }, + }); + + expect(wrapper.find('.js-stage-action').exists()).toBe(true); + }); + }); + + describe('without action', () => { + it('does not render action button', () => { + wrapper = shallowMount(StageColumnComponentLegacy, { + propsData: { + groups: [ + { + id: 4259, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + label: 'success', + tooltip: '<img src=x onerror=alert(document.domain)>', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + }, + }); + + expect(wrapper.find('.js-stage-action').exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js index d32534326c5..abf13823cb9 100644 --- a/spec/frontend/pipelines/graph/stage_column_component_spec.js +++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js @@ -1,64 +1,77 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; +import ActionComponent from '~/pipelines/components/graph/action_component.vue'; +import StageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; -import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; - -describe('stage column component', () => { - const mockJob = { - id: 4250, - name: 'test', - status: { - icon: 'status_success', - text: 'passed', - label: 'passed', - group: 'success', - details_path: '/root/ci-mock/builds/4250', - action: { - icon: 'retry', - title: 'Retry', - path: '/root/ci-mock/builds/4250/retry', - method: 'post', - }, +const mockJob = { + id: 4250, + name: 'test', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4250', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4250/retry', + method: 'post', }, - }; + }, +}; + +const mockGroups = Array(4) + .fill(0) + .map((item, idx) => { + return { ...mockJob, id: idx, name: `fish-${idx}` }; + }); + +const defaultProps = { + title: 'Fish', + groups: mockGroups, +}; +describe('stage column component', () => { let wrapper; - beforeEach(() => { - const mockGroups = []; - for (let i = 0; i < 3; i += 1) { - const mockedJob = { ...mockJob }; - mockedJob.id += i; - mockGroups.push(mockedJob); - } + const findStageColumnTitle = () => wrapper.find('[data-testid="stage-column-title"]'); + const findStageColumnGroup = () => wrapper.find('[data-testid="stage-column-group"]'); + const findAllStageColumnGroups = () => wrapper.findAll('[data-testid="stage-column-group"]'); + const findActionComponent = () => wrapper.find(ActionComponent); - wrapper = shallowMount(stageColumnComponent, { + const createComponent = ({ method = shallowMount, props = {} } = {}) => { + wrapper = method(StageColumnComponent, { propsData: { - title: 'foo', - groups: mockGroups, - hasTriggeredBy: false, + ...defaultProps, + ...props, }, }); - }); + }; - it('should render provided title', () => { - expect( - wrapper - .find('.stage-name') - .text() - .trim(), - ).toBe('foo'); + afterEach(() => { + wrapper.destroy(); + wrapper = null; }); - it('should render the provided groups', () => { - expect(wrapper.findAll('.builds-container > ul > li').length).toBe( - wrapper.props('groups').length, - ); + describe('when mounted', () => { + beforeEach(() => { + createComponent({ method: mount }); + }); + + it('should render provided title', () => { + expect(findStageColumnTitle().text()).toBe(defaultProps.title); + }); + + it('should render the provided groups', () => { + expect(findAllStageColumnGroups().length).toBe(mockGroups.length); + }); }); - describe('jobId', () => { - it('escapes job name', () => { - wrapper = shallowMount(stageColumnComponent, { - propsData: { + describe('job', () => { + beforeEach(() => { + createComponent({ + method: mount, + props: { groups: [ { id: 4259, @@ -70,21 +83,29 @@ describe('stage column component', () => { }, }, ], - title: 'test', - hasTriggeredBy: false, + title: 'test <img src=x onerror=alert(document.domain)>', }, }); + }); + + it('capitalizes and escapes name', () => { + expect(findStageColumnTitle().text()).toBe( + 'Test <img src=x onerror=alert(document.domain)>', + ); + }); - expect(wrapper.find('.builds-container li').attributes('id')).toBe( + it('escapes id', () => { + expect(findStageColumnGroup().attributes('id')).toBe( 'ci-badge-<img src=x onerror=alert(document.domain)>', ); }); }); describe('with action', () => { - it('renders action button', () => { - wrapper = shallowMount(stageColumnComponent, { - propsData: { + beforeEach(() => { + createComponent({ + method: mount, + props: { groups: [ { id: 4259, @@ -105,15 +126,18 @@ describe('stage column component', () => { }, }, }); + }); - expect(wrapper.find('.js-stage-action').exists()).toBe(true); + it('renders action button', () => { + expect(findActionComponent().exists()).toBe(true); }); }); describe('without action', () => { - it('does not render action button', () => { - wrapper = shallowMount(stageColumnComponent, { - propsData: { + beforeEach(() => { + createComponent({ + method: mount, + props: { groups: [ { id: 4259, @@ -129,8 +153,10 @@ describe('stage column component', () => { hasTriggeredBy: false, }, }); + }); - expect(wrapper.find('.js-stage-action').exists()).toBe(false); + it('does not render action button', () => { + expect(findActionComponent().exists()).toBe(false); }); }); }); diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb new file mode 100644 index 00000000000..225c5c2b7b6 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project, reload: true) { create(:project, namespace: namespace) } + let_it_be(:plan_limits, reload: true) { create(:plan_limits, :default_plan) } + + let(:stage_seeds) do + [ + double(:test, seeds: [ + double(:test, attributes: {}) + ]), + double(:staging, seeds: [ + double(:staging, attributes: { environment: 'staging' }) + ]), + double(:production, seeds: [ + double(:production, attributes: { environment: 'production' }) + ]) + ] + end + + let(:save_incompleted) { false } + + let(:command) do + double(:command, + project: project, + stage_seeds: stage_seeds, + save_incompleted: save_incompleted + ) + end + + let(:pipeline) { build(:ci_pipeline, project: project) } + let(:step) { described_class.new(pipeline, command) } + + subject(:perform) { step.perform! } + + context 'when pipeline deployments limit is exceeded' do + before do + plan_limits.update!(ci_pipeline_deployments: 1) + end + + context 'when saving incompleted pipelines' do + let(:save_incompleted) { true } + + it 'drops the pipeline' do + perform + + expect(pipeline).to be_persisted + expect(pipeline.reload).to be_failed + end + + it 'breaks the chain' do + perform + + expect(step.break?).to be true + end + + it 'sets a valid failure reason' do + perform + + expect(pipeline.deployments_limit_exceeded?).to be true + end + end + + context 'when not saving incomplete pipelines' do + let(:save_incompleted) { false } + + it 'does not persist the pipeline' do + perform + + expect(pipeline).not_to be_persisted + end + + it 'breaks the chain' do + perform + + expect(step.break?).to be true + end + + it 'adds an informative error to the pipeline' do + perform + + expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.']) + end + end + + it 'logs the error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(Gitlab::Ci::Limit::LimitExceededError), + project_id: project.id, plan: namespace.actual_plan_name + ) + + perform + end + end + + context 'when pipeline deployments limit is not exceeded' do + before do + plan_limits.update!(ci_pipeline_deployments: 100) + end + + it 'does not break the chain' do + perform + + expect(step.break?).to be false + end + + it 'does not invalidate the pipeline' do + perform + + expect(pipeline.errors).to be_empty + end + + it 'does not log any error' do + expect(Gitlab::ErrorTracking).not_to receive(:track_exception) + + perform + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb new file mode 100644 index 00000000000..5af27435d6b --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:default_plan, reload: true) { create(:default_plan) } + let_it_be(:project, reload: true) { create(:project, :repository, namespace: namespace) } + let_it_be(:plan_limits) { create(:plan_limits, plan: default_plan) } + + let(:pipeline) { build_stubbed(:ci_pipeline, project: project) } + + let(:stage_seeds) do + [ + double(:test, seeds: [ + double(:test, attributes: {}) + ]), + double(:staging, seeds: [ + double(:staging, attributes: { environment: 'staging' }) + ]), + double(:production, seeds: [ + double(:production, attributes: { environment: 'production' }) + ]) + ] + end + + let(:command) do + double(:command, + project: project, + stage_seeds: stage_seeds, + save_incompleted: true + ) + end + + let(:ci_pipeline_deployments_limit) { 0 } + + before do + plan_limits.update!(ci_pipeline_deployments: ci_pipeline_deployments_limit) + end + + subject(:quota) { described_class.new(namespace, pipeline, command) } + + shared_context 'limit exceeded' do + let(:ci_pipeline_deployments_limit) { 1 } + end + + shared_context 'limit not exceeded' do + let(:ci_pipeline_deployments_limit) { 2 } + end + + describe '#enabled?' do + context 'when limit is enabled in plan' do + let(:ci_pipeline_deployments_limit) { 10 } + + it 'is enabled' do + expect(quota).to be_enabled + end + end + + context 'when limit is not enabled' do + let(:ci_pipeline_deployments_limit) { 0 } + + it 'is not enabled' do + expect(quota).not_to be_enabled + end + end + + context 'when limit does not exist' do + before do + allow(namespace).to receive(:actual_plan) { create(:default_plan) } + end + + it 'is enabled by default' do + expect(quota).to be_enabled + end + end + end + + describe '#exceeded?' do + context 'when limit is exceeded' do + include_context 'limit exceeded' + + it 'is exceeded' do + expect(quota).to be_exceeded + end + end + + context 'when limit is not exceeded' do + include_context 'limit not exceeded' + + it 'is not exceeded' do + expect(quota).not_to be_exceeded + end + end + end + + describe '#message' do + context 'when limit is exceeded' do + include_context 'limit exceeded' + + it 'returns info about pipeline deployment limit exceeded' do + expect(quota.message) + .to eq "Pipeline has too many deployments! Requested 2, but the limit is 1." + end + end + end +end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 88293f11149..d4e5a1a94f2 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -20,14 +20,6 @@ RSpec.describe Gitlab::Metrics::Transaction do end end - describe '#thread_cpu_duration' do - it 'returns the duration of a transaction in seconds' do - transaction.run { } - - expect(transaction.thread_cpu_duration).to be > 0 - end - end - describe '#run' do it 'yields the supplied block' do expect { |b| transaction.run(&b) }.to yield_control diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb index ef09fb037e9..82ac159b9cc 100644 --- a/spec/models/packages/package_file_spec.rb +++ b/spec/models/packages/package_file_spec.rb @@ -61,14 +61,14 @@ RSpec.describe Packages::PackageFile, type: :model do end end - describe '#update_file_metadata callback' do + describe '#update_file_store callback' do let_it_be(:package_file) { build(:package_file, :nuget, size: nil) } subject { package_file.save! } it 'updates metadata columns' do expect(package_file) - .to receive(:update_file_metadata) + .to receive(:update_file_store) .and_call_original # This expectation uses a stub because we can no longer test a change from |