diff options
author | Mike Greiling <mike@pixelcog.com> | 2018-11-07 05:21:25 +0000 |
---|---|---|
committer | Mike Greiling <mike@pixelcog.com> | 2018-11-07 05:21:25 +0000 |
commit | 839a654ef0da5f13729074e24231344fe64a2cdc (patch) | |
tree | 6dff019a9ea6164af7c9721ca2cb2ba30b886002 | |
parent | 0a4eeafb43236359c55e221bdc8cab52acef9a0f (diff) | |
parent | cf8fe12b7b3a24082db47f71c80b01e62e391f32 (diff) | |
download | gitlab-ce-gl-ui-loading-icon.tar.gz |
Merge branch 'master' into 'gl-ui-loading-icon'gl-ui-loading-icon
# Conflicts:
# app/assets/javascripts/jobs/components/job_app.vue
261 files changed, 4776 insertions, 2825 deletions
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js index 5bea47f23c5..d8d0fa1fac4 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -31,7 +31,7 @@ class DirtySubmitForm { updateDirtyInput(event) { const input = event.target; - if (!input.dataset.dirtySubmitOriginalValue) return; + if (!input.dataset.isDirtySubmitInput) return; this.updateDirtyInputs(input); this.toggleSubmission(); @@ -65,6 +65,7 @@ class DirtySubmitForm { } static initInput(element) { + element.dataset.isDirtySubmitInput = true; element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element); } diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index a9534ac597e..aff483876f8 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -3,9 +3,11 @@ import _ from 'underscore'; import { mapGetters, mapState, mapActions } from 'vuex'; import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; +import { polyfillSticky } from '~/lib/utils/sticky'; import bp from '~/breakpoints'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import Callout from '~/vue_shared/components/callout.vue'; +import Icon from '~/vue_shared/components/icon.vue'; import createStore from '../store'; import EmptyState from './empty_state.vue'; import EnvironmentsBlock from './environments_block.vue'; @@ -24,6 +26,7 @@ export default { EmptyState, EnvironmentsBlock, ErasedBlock, + Icon, Log, LogTopBar, StuckBlock, @@ -97,6 +100,14 @@ export default { if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { this.fetchStages(); } + + if (newVal.archived) { + this.$nextTick(() => { + if (this.$refs.sticky) { + polyfillSticky(this.$refs.sticky); + } + }); + } }, }, created() { @@ -114,16 +125,13 @@ export default { window.addEventListener('resize', this.onResize); window.addEventListener('scroll', this.updateScroll); }, - mounted() { this.updateSidebar(); }, - destroyed() { window.removeEventListener('resize', this.onResize); window.removeEventListener('scroll', this.updateScroll); }, - methods: { ...mapActions([ 'setJobEndpoint', @@ -218,14 +226,28 @@ export default { :erased-at="job.erased_at" /> + <div + v-if="job.archived" + ref="sticky" + class="js-archived-job prepend-top-default archived-sticky sticky-top" + > + <icon + name="lock" + class="align-text-bottom" + /> + + {{ __('This job is archived. Only the complete pipeline can be retried.') }} + </div> <!--job log --> <div v-if="hasTrace" - class="build-trace-container prepend-top-default"> + class="build-trace-container" + > <log-top-bar :class="{ 'sidebar-expanded': isSidebarOpen, - 'sidebar-collapsed': !isSidebarOpen + 'sidebar-collapsed': !isSidebarOpen, + 'has-archived-block': job.archived }" :erase-path="job.erase_path" :size="traceSize" diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index eeefa33264f..8b506b124ec 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -69,7 +69,7 @@ export default { }; </script> <template> - <div class="top-bar affix"> + <div class="top-bar"> <!-- truncate information --> <div class="js-truncated-info truncated-info d-none d-sm-block float-left"> <template v-if="isTraceSizeVisible"> diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index 3b425ee2fed..f4243522ef8 100644 --- a/app/assets/javascripts/reports/components/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,18 +1,31 @@ <script> -import IssuesBlock from '~/reports/components/report_issues.vue'; -import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants'; +import ReportItem from '~/reports/components/report_item.vue'; +import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; +import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; + +const wrapIssueWithState = (status, isNew = false) => issue => ({ + status: issue.status || status, + isNew, + issue, +}); /** * Renders block of issues */ - export default { components: { - IssuesBlock, + SmartVirtualList, + ReportItem, }, - success: STATUS_SUCCESS, - failed: STATUS_FAILED, - neutral: STATUS_NEUTRAL, + // Typical height of a report item in px + typicalReportItemHeight: 32, + /* + The maximum amount of shown issues. This is calculated by + ( max-height of report-block-list / typicalReportItemHeight ) + some safety margin + We will use VirtualList if we have more items than this number. + For entries lower than this number, the virtual scroll list calculates the total height of the element wrongly. + */ + maxShownReportItems: 20, props: { newIssues: { type: Array, @@ -40,42 +53,34 @@ export default { default: '', }, }, + computed: { + issuesWithState() { + return [ + ...this.newIssues.map(wrapIssueWithState(STATUS_FAILED, true)), + ...this.unresolvedIssues.map(wrapIssueWithState(STATUS_FAILED)), + ...this.neutralIssues.map(wrapIssueWithState(STATUS_NEUTRAL)), + ...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)), + ]; + }, + }, }; </script> <template> - <div class="report-block-container"> - - <issues-block - v-if="newIssues.length" - :component="component" - :issues="newIssues" - class="js-mr-code-new-issues" - status="failed" - is-new - /> - - <issues-block - v-if="unresolvedIssues.length" - :component="component" - :issues="unresolvedIssues" - :status="$options.failed" - class="js-mr-code-new-issues" - /> - - <issues-block - v-if="neutralIssues.length" - :component="component" - :issues="neutralIssues" - :status="$options.neutral" - class="js-mr-code-non-issues" - /> - - <issues-block - v-if="resolvedIssues.length" + <smart-virtual-list + :length="issuesWithState.length" + :remain="$options.maxShownReportItems" + :size="$options.typicalReportItemHeight" + class="report-block-container" + wtag="ul" + wclass="report-block-list" + > + <report-item + v-for="(wrapped, index) in issuesWithState" + :key="index" + :issue="wrapped.issue" + :status="wrapped.status" :component="component" - :issues="resolvedIssues" - :status="$options.success" - class="js-mr-code-resolved-issues" + :is-new="wrapped.isNew" /> - </div> + </smart-virtual-list> </template> diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_item.vue index a2a03945ae3..01e6d357a21 100644 --- a/app/assets/javascripts/reports/components/report_issues.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -3,14 +3,14 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue'; import { components, componentNames } from '~/reports/components/issue_body'; export default { - name: 'ReportIssues', + name: 'ReportItem', components: { IssueStatusIcon, ...components, }, props: { - issues: { - type: Array, + issue: { + type: Object, required: true, }, component: { @@ -33,27 +33,21 @@ export default { }; </script> <template> - <div> - <ul class="report-block-list"> - <li - v-for="(issue, index) in issues" - :key="index" - :class="{ 'is-dismissed': issue.isDismissed }" - class="report-block-list-issue" - > - <issue-status-icon - :status="issue.status || status" - class="append-right-5" - /> + <li + :class="{ 'is-dismissed': issue.isDismissed }" + class="report-block-list-issue" + > + <issue-status-icon + :status="status" + class="append-right-5" + /> - <component - :is="component" - v-if="component" - :issue="issue" - :status="issue.status || status" - :is-new="isNew" - /> - </li> - </ul> - </div> + <component + :is="component" + v-if="component" + :issue="issue" + :status="status" + :is-new="isNew" + /> + </li> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 57c52a2016a..2a8380f5f2b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -65,6 +65,14 @@ export default { deployedText() { return this.$options.deployedTextMap[this.deployment.status]; }, + isDeployInProgress() { + return this.deployment.status === 'running'; + }, + deployInProgressTooltip() { + return this.isDeployInProgress + ? __('Stopping this environment is currently not possible as a deployment is in progress') + : ''; + }, shouldRenderDropdown() { return ( this.enableCiEnvironmentsStatusChanges && @@ -183,15 +191,23 @@ export default { css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin" /> </template> - <loading-button + <span v-if="deployment.stop_url" - :loading="isStopping" - container-class="btn btn-default btn-sm inline prepend-left-4" - title="Stop environment" - @click="stopEnvironment" + v-tooltip + :title="deployInProgressTooltip" + class="d-inline-block" + tabindex="0" > - <icon name="stop" /> - </loading-button> + <loading-button + :loading="isStopping" + :disabled="isDeployInProgress" + :title="__('Stop environment')" + container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4" + @click="stopEnvironment" + > + <icon name="stop" /> + </loading-button> + </span> </div> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 8bcabc10225..53608838f2f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -71,6 +71,7 @@ export default { linkStart: `<a href="${this.troubleshootingDocsPath}">`, linkEnd: '</a>', }, + false, ); }, }, diff --git a/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue new file mode 100644 index 00000000000..63034a45f77 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue @@ -0,0 +1,42 @@ +<script> +import VirtualList from 'vue-virtual-scroll-list'; + +export default { + name: 'SmartVirtualList', + components: { VirtualList }, + props: { + size: { type: Number, required: true }, + length: { type: Number, required: true }, + remain: { type: Number, required: true }, + rtag: { type: String, default: 'div' }, + wtag: { type: String, default: 'div' }, + wclass: { type: String, default: null }, + }, +}; +</script> +<template> + <virtual-list + v-if="length > remain" + v-bind="$attrs" + :size="remain" + :remain="remain" + :rtag="rtag" + :wtag="wtag" + :wclass="wclass" + class="js-virtual-list" + > + <slot></slot> + </virtual-list> + <component + :is="rtag" + v-else + class="js-plain-element" + > + <component + :is="wtag" + :class="wclass" + > + <slot></slot> + </component> + </component> +</template> diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 1e93bf2b751..a20920e2503 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -39,7 +39,7 @@ svg { fill: currentColor; - $svg-sizes: 8 10 12 16 18 24 32 48 72; + $svg-sizes: 8 10 12 14 16 18 24 32 48 72; @each $svg-size in $svg-sizes { &.s#{$svg-size} { @include svg-size(#{$svg-size}px); diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 19eee4e4aba..bfcac3f1c3f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -269,6 +269,7 @@ $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; $project-title-row-height: 24px; +$gl-line-height: 16px; /* * Common component specific colors diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 1449723de52..81cb519883b 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -55,9 +55,29 @@ @include build-trace(); } + .archived-sticky { + top: $header-height; + border-radius: 2px 2px 0 0; + color: $orange-600; + background-color: $orange-100; + border: 1px solid $border-gray-normal; + border-bottom: 0; + padding: 3px 12px; + margin: auto; + align-items: center; + + .with-performance-bar & { + top: $header-height + $performance-bar-height; + } + } + .top-bar { @include build-trace-top-bar(35px); + &.has-archived-block { + top: $header-height + $performance-bar-height + 28px; + } + &.affix { top: $header-height; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index a91d44805ee..618f23d81b1 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,41 +4,29 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding-top 0 $gl-padding-top 40px; + padding: $gl-padding 0 $gl-padding 56px; border-bottom: 1px solid $white-normal; - color: $gl-text-color; + color: $gl-text-color-secondary; position: relative; - - &.event-inline { - .system-note-image { - top: 20px; - } - - .user-avatar { - top: 14px; - } - - .event-title, - .event-item-timestamp { - line-height: 40px; - } - } - - a { - color: $gl-text-color; - } + line-height: $gl-line-height; .system-note-image { position: absolute; left: 0; - top: 14px; svg { - width: 20px; - height: 20px; fill: $gl-text-color-secondary; } + } + + .system-note-image-inline { + svg { + fill: $gl-text-color-secondary; + } + } + .system-note-image, + .system-note-image-inline { &.opened-icon, &.created-icon { svg { @@ -53,16 +41,35 @@ &.accepted-icon svg { fill: $blue-300; } + + &.commented-on-icon svg { + fill: $blue-600; + } + } + + .event-user-info { + margin-bottom: $gl-padding-8; + + .author_name { + a { + color: $gl-text-color; + font-weight: $gl-font-weight-bold; + } + } } .event-title { - @include str-truncated(calc(100% - 174px)); - font-weight: $gl-font-weight-bold; - color: $gl-text-color; + .event-type { + &::first-letter { + text-transform: capitalize; + } + } } .event-body { + margin-top: $gl-padding-8; margin-right: 174px; + color: $gl-text-color; .event-note { word-wrap: break-word; @@ -92,7 +99,7 @@ } .note-image-attach { - margin-top: 4px; + margin-top: $gl-padding-4; margin-left: 0; max-width: 200px; float: none; @@ -107,7 +114,6 @@ color: $gl-gray-500; float: left; font-size: $gl-font-size; - line-height: 16px; margin-right: 5px; } } @@ -127,7 +133,9 @@ } } - &:last-child { border: 0; } + &:last-child { + border: 0; + } .event_commits { li { @@ -154,7 +162,6 @@ .event-item-timestamp { float: right; - line-height: 22px; } } @@ -177,10 +184,8 @@ .event-item { padding-left: 0; - &.event-inline { - .event-title { - line-height: 20px; - } + .event-user-info { + margin-bottom: $gl-padding-4; } .event-title { @@ -194,7 +199,8 @@ } .event-body { - margin: 0; + margin-top: $gl-padding-4; + margin-right: 0; padding-left: 0; } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index f084adaf5d3..1d691d1d8b8 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -240,6 +240,12 @@ left: 0; } + .activities-block { + .event-item { + padding-left: 40px; + } + } + @include media-breakpoint-down(xs) { .cover-block { padding-top: 20px; @@ -267,6 +273,12 @@ margin-right: 0; } } + + .activities-block { + .event-item { + padding-left: 0; + } + } } } diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 7f874687212..0dd7500623d 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -100,18 +100,12 @@ module Boards .merge(board_id: params[:board_id], list_id: params[:list_id], request: request) end + def serializer + IssueSerializer.new(current_user: current_user) + end + def serialize_as_json(resource) - resource.as_json( - only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight], - labels: true, - issue_endpoints: true, - include_full_project_path: board.group_board?, - include: { - project: { only: [:id, :path] }, - assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, - milestone: { only: [:id, :title] } - } - ) + serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?) end def whitelist_query_limiting diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb index c6c3598a976..0a9d3d86245 100644 --- a/app/controllers/concerns/members_presentation.rb +++ b/app/controllers/concerns/members_presentation.rb @@ -12,12 +12,7 @@ module MembersPresentation ).fabricate! end - # rubocop: disable CodeReuse/ActiveRecord def preload_associations(members) - ActiveRecord::Associations::Preloader.new.preload(members, :user) - ActiveRecord::Associations::Preloader.new.preload(members, :source) - ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status) - ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations) + MembersPreloader.new(members).preload_all end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index c94946a04e7..2adfc04deb8 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -163,14 +163,10 @@ module EventsHelper def event_note_title_html(event) if event.note_target - text = raw("#{event.note_target_type} ") + - if event.commit_note? - content_tag(:span, event.note_target_reference, class: 'commit-sha') - else - event.note_target_reference - end - - link_to(text, event_note_target_url(event), title: event.target_title, class: 'has-tooltip') + capture do + concat content_tag(:span, event.note_target_type, class: "event-target-type append-right-4") + concat link_to(event.note_target_reference, event_note_target_url(event), title: event.target_title, class: 'has-tooltip event-target-link append-right-4') + end else content_tag(:strong, '(deleted)') end @@ -183,17 +179,9 @@ module EventsHelper "--broken encoding" end - def event_row_class(event) - if event.body? - "event-block" - else - "event-inline" - end - end - - def icon_for_event(note) + def icon_for_event(note, size: 24) icon_name = ICON_NAMES_BY_EVENT_TYPE[note] - sprite_icon(icon_name) if icon_name + sprite_icon(icon_name, size: size) if icon_name end def icon_for_profile_event(event) @@ -203,8 +191,24 @@ module EventsHelper end else content_tag :div, class: 'system-note-image user-avatar' do - author_avatar(event, size: 32) + author_avatar(event, size: 40) + end + end + end + + def inline_event_icon(event) + unless current_path?('users#show') + content_tag :span, class: "system-note-image-inline d-none d-sm-flex append-right-4 #{event.action_name.parameterize}-icon align-self-center" do + icon_for_event(event.action_name, size: 14) end end end + + def event_user_info(event) + content_tag(:div, class: "event-user-info") do + concat content_tag(:span, link_to_author(event), class: "author_name") + concat " ".html_safe + concat content_tag(:span, event.author.to_reference, class: "username") + end + end end diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index bae01d476df..4aba48061ba 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -3,7 +3,6 @@ module UserCalloutsHelper GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze - CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && @@ -14,10 +13,6 @@ module UserCalloutsHelper !user_dismissed?(GCP_SIGNUP_OFFER) end - def show_cluster_security_warning? - !user_dismissed?(CLUSTER_SECURITY_WARNING) - end - private def user_dismissed?(feature_name) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d7eab57763e..360c9924a7d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -9,19 +9,18 @@ module Ci include Presentable include Importable include Gitlab::Utils::StrongMemoize + include Deployable belongs_to :project, inverse_of: :builds belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' - has_many :deployments, as: :deployable - RUNNER_FEATURES = { upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? } }.freeze - has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' + has_one :deployment, as: :deployable, class_name: 'Deployment' has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id @@ -195,6 +194,8 @@ module Ci end after_transition pending: :running do |build| + build.deployment&.run + build.run_after_commit do BuildHooksWorker.perform_async(id) end @@ -207,14 +208,18 @@ module Ci end after_transition any => [:success] do |build| + build.deployment&.succeed + build.run_after_commit do - BuildSuccessWorker.perform_async(id) PagesWorker.perform_async(:deploy, id) if build.pages_generator? end end before_transition any => [:failed] do |build| next unless build.project + + build.deployment&.drop + next if build.retries_max.zero? if build.retries_count < build.retries_max @@ -233,6 +238,10 @@ module Ci after_transition running: any do |build| Ci::BuildRunnerSession.where(build: build).delete_all end + + after_transition any => [:skipped, :canceled] do |build| + build.deployment&.cancel + end end def ensure_metadata @@ -342,8 +351,12 @@ module Ci self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options end + def has_deployment? + !!self.deployment + end + def outdated_deployment? - success? && !last_deployment.try(:last?) + success? && !deployment.try(:last?) end def depends_on_builds @@ -358,6 +371,10 @@ module Ci user == current_user end + def on_stop + options&.dig(:environment, :on_stop) + end + # A slugified version of the build ref, suitable for inclusion in URLs and # domain names. Rules: # @@ -725,7 +742,7 @@ module Ci if success? return successful_deployment_status - elsif complete? && !success? + elsif failed? return :failed end @@ -742,13 +759,11 @@ module Ci end def successful_deployment_status - if success? && last_deployment&.last? - return :last - elsif success? && last_deployment.present? - return :out_of_date + if deployment&.last? + :last + else + :out_of_date end - - :creating end def each_report(report_types) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 34a889057ab..11c88200c37 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -15,7 +15,7 @@ module Ci metadata: nil, trace: nil, junit: 'junit.xml', - codequality: 'codequality.json', + codequality: 'gl-code-quality-report.json', sast: 'gl-sast-report.json', dependency_scanning: 'gl-dependency-scanning-report.json', container_scanning: 'gl-container-scanning-report.json', diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index aeee7f0a5d2..56010e899a4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -181,22 +181,31 @@ module Ci # # ref - The name (or names) of the branch(es)/tag(s) to limit the list of # pipelines to. - def self.newest_first(ref = nil) + # limit - This limits a backlog search, default to 100. + def self.newest_first(ref: nil, limit: 100) relation = order(id: :desc) + relation = relation.where(ref: ref) if ref + + if limit + ids = relation.limit(limit).select(:id) + # MySQL does not support limit in subquery + ids = ids.pluck(:id) if Gitlab::Database.mysql? + relation = relation.where(id: ids) + end - ref ? relation.where(ref: ref) : relation + relation end def self.latest_status(ref = nil) - newest_first(ref).pluck(:status).first + newest_first(ref: ref).pluck(:status).first end def self.latest_successful_for(ref) - newest_first(ref).success.take + newest_first(ref: ref).success.take end def self.latest_successful_for_refs(refs) - relation = newest_first(refs).success + relation = newest_first(ref: refs).success relation.each_with_object({}) do |pipeline, hash| hash[pipeline.ref] ||= pipeline @@ -238,6 +247,10 @@ module Ci end end + def self.latest_successful_ids_per_project + success.group(:project_id).select('max(id) as id') + end + def self.truncate_sha(sha) sha[0...8] end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 2bd373e0950..e80d35d0f3c 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -3,6 +3,7 @@ module Clusters class Cluster < ActiveRecord::Base include Presentable + include Gitlab::Utils::StrongMemoize self.table_name = 'clusters' @@ -24,9 +25,6 @@ module Clusters has_many :cluster_groups, class_name: 'Clusters::Group' has_many :groups, through: :cluster_groups, class_name: '::Group' - has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group' - has_one :group, through: :cluster_group, class_name: '::Group' - # we force autosave to happen when we save `Cluster` model has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true @@ -119,12 +117,19 @@ module Clusters end def first_project - return @first_project if defined?(@first_project) - - @first_project = projects.first + strong_memoize(:first_project) do + projects.first + end end alias_method :project, :first_project + def first_group + strong_memoize(:first_group) do + groups.first + end + end + alias_method :group, :first_group + def kubeclient platform_kubernetes.kubeclient if kubernetes? end diff --git a/app/models/concerns/deployable.rb b/app/models/concerns/deployable.rb new file mode 100644 index 00000000000..f4f1989f0a9 --- /dev/null +++ b/app/models/concerns/deployable.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Deployable + extend ActiveSupport::Concern + + included do + after_create :create_deployment + + def create_deployment + return unless starts_environment? && !has_deployment? + + environment = project.environments.find_or_create_by( + name: expanded_environment_name + ) + + environment.deployments.create!( + project_id: environment.project_id, + environment: environment, + ref: ref, + tag: tag, + sha: sha, + user: user, + deployable: self, + on_stop: on_stop).tap do |_| + self.reload # Reload relationships + end + end + end +end diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 0b2eedf3631..e3524305346 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base include Expirable include TokenAuthenticatable include PolicyActor + include Gitlab::Utils::StrongMemoize add_authentication_token_field :token AVAILABLE_SCOPES = %i(read_repository read_registry).freeze @@ -49,7 +50,9 @@ class DeployToken < ActiveRecord::Base # to a single project, later we're going to extend # that to be for multiple projects and namespaces. def project - projects.first + strong_memoize(:project) do + projects.first + end end def expires_at diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 37efbb04fce..54a900a3b85 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -3,6 +3,7 @@ class Deployment < ActiveRecord::Base include AtomicInternalId include IidRoutes + include AfterCommitQueue belongs_to :project, required: true belongs_to :environment, required: true @@ -16,11 +17,44 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_create :create_ref - after_create :invalidate_cache - scope :for_environment, -> (environment) { where(environment_id: environment) } + state_machine :status, initial: :created do + event :run do + transition created: :running + end + + event :succeed do + transition any - [:success] => :success + end + + event :drop do + transition any - [:failed] => :failed + end + + event :cancel do + transition any - [:canceled] => :canceled + end + + before_transition any => [:success, :failed, :canceled] do |deployment| + deployment.finished_at = Time.now + end + + after_transition any => :success do |deployment| + deployment.run_after_commit do + Deployments::SuccessWorker.perform_async(id) + end + end + end + + enum status: { + created: 0, + running: 1, + success: 2, + failed: 3, + canceled: 4 + } + def self.last_for_environment(environment) ids = self .for_environment(environment) @@ -69,15 +103,15 @@ class Deployment < ActiveRecord::Base end def update_merge_request_metrics! - return unless environment.update_merge_request_metrics? + return unless environment.update_merge_request_metrics? && success? merge_requests = project.merge_requests .joins(:metrics) .where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }) - .where("merge_request_metrics.merged_at <= ?", self.created_at) + .where("merge_request_metrics.merged_at <= ?", finished_at) if previous_deployment - merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at) + merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.finished_at) end # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table @@ -91,7 +125,7 @@ class Deployment < ActiveRecord::Base MergeRequest::Metrics .where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil) - .update_all(first_deployed_to_production_at: self.created_at) + .update_all(first_deployed_to_production_at: finished_at) end def previous_deployment @@ -109,8 +143,18 @@ class Deployment < ActiveRecord::Base @stop_action ||= manual_actions.find_by(name: on_stop) end + def finished_at + read_attribute(:finished_at) || legacy_finished_at + end + + def deployed_at + return unless success? + + finished_at + end + def formatted_deployment_time - created_at.to_time.in_time_zone.to_s(:medium) + deployed_at&.to_time&.in_time_zone&.to_s(:medium) end def has_metrics? @@ -118,21 +162,17 @@ class Deployment < ActiveRecord::Base end def metrics - return {} unless has_metrics? + return {} unless has_metrics? && success? metrics = prometheus_adapter.query(:deployment, self) - metrics&.merge(deployment_time: created_at.to_i) || {} + metrics&.merge(deployment_time: finished_at.to_i) || {} end def additional_metrics - return {} unless has_metrics? + return {} unless has_metrics? && success? metrics = prometheus_adapter.query(:additional_metrics_deployment, self) - metrics&.merge(deployment_time: created_at.to_i) || {} - end - - def status - 'success' + metrics&.merge(deployment_time: finished_at.to_i) || {} end private @@ -144,4 +184,8 @@ class Deployment < ActiveRecord::Base def ref_path File.join(environment.ref_path, 'deployments', iid.to_s) end + + def legacy_finished_at + self.created_at if success? && !read_attribute(:finished_at) + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 1c31c01eb9f..7d104bb0c25 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -8,9 +8,9 @@ class Environment < ActiveRecord::Base belongs_to :project, required: true - has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' + has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' before_validation :nullify_external_url before_validation :generate_slug, if: ->(env) { env.slug.blank? } diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index a84871f7253..7efc8da09ad 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -8,8 +8,8 @@ class EnvironmentStatus delegate :id, to: :environment delegate :name, to: :environment delegate :project, to: :environment + delegate :status, to: :deployment, allow_nil: true delegate :deployed_at, to: :deployment, allow_nil: true - delegate :status, to: :deployment def self.for_merge_request(mr, user) build_environments_status(mr, user, mr.head_pipeline) @@ -33,10 +33,6 @@ class EnvironmentStatus end end - def deployed_at - deployment&.created_at - end - def changes return [] if project.route_map_for(sha).nil? diff --git a/app/models/issue.rb b/app/models/issue.rb index 0de5e434b02..abdb3448d4e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -231,20 +231,6 @@ class Issue < ActiveRecord::Base def as_json(options = {}) super(options).tap do |json| - if options.key?(:issue_endpoints) && project - url_helper = Gitlab::Routing.url_helpers - - issue_reference = options[:include_full_project_path] ? to_reference(full: true) : to_reference - - json.merge!( - reference_path: issue_reference, - real_path: url_helper.project_issue_path(project, self), - issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'), - toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self), - assignable_labels_endpoint: url_helper.project_labels_path(project, format: :json, include_ancestor_groups: true) - ) - end - if options.key?(:labels) json[:labels] = labels.as_json( project: project, diff --git a/app/models/label.rb b/app/models/label.rb index 43b49445765..165e4a8f3e5 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -41,8 +41,8 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } scope :with_title, ->(title) { where(title: title) } scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) } - scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) } scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) } + scope :on_board, ->(board_id) { with_lists_and_board.where(boards: { id: board_id }) } scope :order_name_asc, -> { reorder(title: :asc) } scope :order_name_desc, -> { reorder(title: :desc) } scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) } diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb new file mode 100644 index 00000000000..33855191ca8 --- /dev/null +++ b/app/models/members_preloader.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class MembersPreloader + attr_reader :members + + def initialize(members) + @members = members + end + + def preload_all + ActiveRecord::Associations::Preloader.new.preload(members, :user) + ActiveRecord::Associations::Preloader.new.preload(members, :source) + ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status) + ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations) + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 74d48d0a9af..4a6627d3ca1 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -232,6 +232,12 @@ class Namespace < ActiveRecord::Base Project.inside_path(full_path) end + # Includes pipelines from this namespace and pipelines from all subgroups + # that belongs to this namespace + def all_pipelines + Ci::Pipeline.where(project: all_projects) + end + def has_parent? parent.present? end diff --git a/app/models/project.rb b/app/models/project.rb index 872bea46e7c..d5a4ae79c47 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -95,8 +95,7 @@ class Project < ActiveRecord::Base unless: :ci_cd_settings, if: proc { ProjectCiCdSetting.available? } - after_create :set_last_activity_at - after_create :set_last_repository_updated_at + after_create :set_timestamps_for_create after_update :update_forks_visibility_level before_destroy :remove_private_deploy_keys @@ -255,7 +254,7 @@ class Project < ActiveRecord::Base has_many :variables, class_name: 'Ci::Variable' has_many :triggers, class_name: 'Ci::Trigger' has_many :environments - has_many :deployments + has_many :deployments, -> { success } has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' has_many :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens @@ -2103,13 +2102,8 @@ class Project < ActiveRecord::Base gitlab_shell.exists?(repository_storage, "#{disk_path}.git") end - # set last_activity_at to the same as created_at - def set_last_activity_at - update_column(:last_activity_at, self.created_at) - end - - def set_last_repository_updated_at - update_column(:last_repository_updated_at, self.created_at) + def set_timestamps_for_create + update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at) end def cross_namespace_reference?(from) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 7cff0e30e8d..a399982e5ec 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -12,9 +12,9 @@ class IssueTrackerService < Service # overridden patterns. See ReferenceRegexes::EXTERNAL_PATTERN def self.reference_pattern(only_long: false) if only_long - /(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)/ + /(\b[A-Z][A-Z0-9_]*-)(?<issue>\d+)/ else - /(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)/ + /(\b[A-Z][A-Z0-9_]*-|#{Issue.reference_prefix})(?<issue>\d+)/ end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 37a1dd64052..ee5579329a8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -912,10 +912,6 @@ class Repository async_remove_remote(remote_name) if tmp_remote_name end - def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true) - gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune) - end - def async_remove_remote(remote_name) return unless remote_name diff --git a/app/serializers/README.md b/app/serializers/README.md index 0337f88db5f..bb94745b0b5 100644 --- a/app/serializers/README.md +++ b/app/serializers/README.md @@ -180,7 +180,7 @@ def index render json: MyResourceSerializer .new(current_user: @current_user) .represent_details(@project.resources) - nd + end end ``` @@ -196,7 +196,7 @@ def index .represent_details(@project.resources), count: @project.resources.count } - nd + end end ``` diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb new file mode 100644 index 00000000000..6a9e9638e70 --- /dev/null +++ b/app/serializers/issue_board_entity.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class IssueBoardEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :iid + expose :title + + expose :confidential + expose :due_date + expose :project_id + expose :relative_position + expose :weight, if: -> (*) { respond_to?(:weight) } + + expose :project do |issue| + API::Entities::Project.represent issue.project, only: [:id, :path] + end + + expose :milestone, expose_nil: false do |issue| + API::Entities::Project.represent issue.milestone, only: [:id, :title] + end + + expose :assignees do |issue| + API::Entities::UserBasic.represent issue.assignees, only: [:id, :name, :username, :avatar_url] + end + + expose :labels do |issue| + LabelEntity.represent issue.labels, project: issue.project, only: [:id, :title, :description, :color, :priority, :text_color] + end + + expose :reference_path, if: -> (issue) { issue.project } do |issue, options| + options[:include_full_project_path] ? issue.to_reference(full: true) : issue.to_reference + end + + expose :real_path, if: -> (issue) { issue.project } do |issue| + project_issue_path(issue.project, issue) + end + + expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue| + project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar') + end + + expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue| + toggle_subscription_project_issue_path(issue.project, issue) + end + + expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue| + project_labels_path(issue.project, format: :json, include_ancestor_groups: true) + end +end diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb index 37cf5e28396..d66f0a5acb7 100644 --- a/app/serializers/issue_serializer.rb +++ b/app/serializers/issue_serializer.rb @@ -4,15 +4,17 @@ class IssueSerializer < BaseSerializer # This overrided method takes care of which entity should be used # to serialize the `issue` based on `basic` key in `opts` param. # Hence, `entity` doesn't need to be declared on the class scope. - def represent(merge_request, opts = {}) + def represent(issue, opts = {}) entity = case opts[:serializer] when 'sidebar' IssueSidebarEntity + when 'board' + IssueBoardEntity else IssueEntity end - super(merge_request, opts, entity) + super(issue, opts, entity) end end diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb index 98743d62b50..5082245dda9 100644 --- a/app/serializers/label_entity.rb +++ b/app/serializers/label_entity.rb @@ -12,4 +12,8 @@ class LabelEntity < Grape::Entity expose :text_color expose :created_at expose :updated_at + + expose :priority, if: -> (*) { options.key?(:project) } do |label| + label.priority(options[:project]) + end end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 7dd87034410..43a26f4264e 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -70,10 +70,8 @@ module Boards label_ids = if moving_to_list.movable? moving_from_list.label_id - elsif board.group_board? - ::Label.on_group_boards(parent.id).pluck(:label_id) else - ::Label.on_project_boards(parent.id).pluck(:label_id) + ::Label.on_board(board.id).pluck(:label_id) end Array(label_ids).compact diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb deleted file mode 100644 index bb3f605da28..00000000000 --- a/app/services/create_deployment_service.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -class CreateDeploymentService - attr_reader :job - - delegate :expanded_environment_name, - :variables, - :project, - to: :job - - def initialize(job) - @job = job - end - - def execute - return unless executable? - - ActiveRecord::Base.transaction do - environment.external_url = expanded_environment_url if - expanded_environment_url - - environment.fire_state_event(action) - - break unless environment.save - break if environment.stopped? - - deploy.tap(&:update_merge_request_metrics!) - end - end - - private - - def executable? - project && job.environment.present? && environment - end - - def deploy - project.deployments.create( - environment: environment, - ref: job.ref, - tag: job.tag, - sha: job.sha, - user: job.user, - deployable: job, - on_stop: on_stop) - end - - def environment - @environment ||= job.persisted_environment - end - - def environment_options - @environment_options ||= job.options&.dig(:environment) || {} - end - - def expanded_environment_url - return @expanded_environment_url if defined?(@expanded_environment_url) - - @expanded_environment_url = - ExpandVariables.expand(environment_url, variables) if environment_url - end - - def environment_url - environment_options[:url] - end - - def on_stop - environment_options[:on_stop] - end - - def action - environment_options[:action] || 'start' - end -end diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb new file mode 100644 index 00000000000..aa7fcca1e2a --- /dev/null +++ b/app/services/update_deployment_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class UpdateDeploymentService + attr_reader :deployment + attr_reader :deployable + + delegate :environment, to: :deployment + delegate :variables, to: :deployable + + def initialize(deployment) + @deployment = deployment + @deployable = deployment.deployable + end + + def execute + deployment.create_ref + deployment.invalidate_cache + + ActiveRecord::Base.transaction do + environment.external_url = expanded_environment_url if + expanded_environment_url + + environment.fire_state_event(action) + + break unless environment.save + break if environment.stopped? + + deployment.tap(&:update_merge_request_metrics!) + end + end + + private + + def environment_options + @environment_options ||= deployable.options&.dig(:environment) || {} + end + + def expanded_environment_url + return @expanded_environment_url if defined?(@expanded_environment_url) + return unless environment_url + + @expanded_environment_url = + ExpandVariables.expand(environment_url, variables) + end + + def environment_url + environment_options[:url] + end + + def action + environment_options[:action] || 'start' + end +end diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml index 73cfea0ef92..160c5f009a7 100644 --- a/app/views/clusters/clusters/_banner.html.haml +++ b/app/views/clusters/clusters/_banner.html.haml @@ -7,9 +7,3 @@ .hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' } = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") - -- if show_cluster_security_warning? - .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning - %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } × - = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.") - = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index ad842036a62..8ed4666e79a 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -64,7 +64,7 @@ .form-group .form-check = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true - = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/clusters/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml index 6021b220285..ca55ccb8fdf 100644 --- a/app/views/clusters/clusters/gcp/_show.html.haml +++ b/app/views/clusters/clusters/gcp/_show.html.haml @@ -40,7 +40,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index 4e6232b69de..e4758938059 100644 --- a/app/views/clusters/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -28,7 +28,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/clusters/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml index a871fef0240..ad8c35e32e3 100644 --- a/app/views/clusters/clusters/user/_show.html.haml +++ b/app/views/clusters/clusters/user/_show.html.haml @@ -29,7 +29,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 78a1d1a0553..2fcb1d1fd2b 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,5 +1,5 @@ - if event.visible_to_user?(current_user) - .event-item{ class: event_row_class(event) } + .event-item .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 829a3da1558..96d6553a2ac 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,20 +1,19 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span{ class: event.action_name } += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) - if event.target - = event.action_name - %strong - = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title do - = event.target_type.titleize.downcase - = event.target.reference_link_text + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event.action_name + %span.event-target-type.append-right-4= event.target_type.titleize.downcase + = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do + = event.target.reference_link_text + - unless event.milestone? + %span.event-target-title.append-right-4= """.html_safe + event.target.title + """.html_safe - else - = event_action_name(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event_action_name(event) = render "events/event_scope", event: event - -- if event.target.respond_to?(:title) - .event-body - .event-note - = event.target.title diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 6ad7e157131..2f156603414 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,8 +1,10 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span{ class: event.action_name } += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } = event_action_name(event) - if event.project diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index cdacd998a69..fb0d2c3b8b0 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,9 +1,13 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - = event.action_name += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event.action_name = event_note_title_html(event) + %span.event-target-title.append-right-4= """.html_safe + event.target.title + """.html_safe = render "events/event_scope", event: event diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml index ccd2aacb4ea..d91f30c07cb 100644 --- a/app/views/events/event/_private.html.haml +++ b/app/views/events/event/_private.html.haml @@ -1,10 +1,11 @@ -.event-inline.event-item +.event-item .event-item-timestamp = time_ago_with_tooltip(event.created_at) - .system-note-image= sprite_icon('eye-slash', size: 16, css_class: 'icon') + .system-note-image= sprite_icon('eye-slash', size: 24, css_class: 'icon') - .event-title - - author_name = capture do - %span.author_name= link_to_author(event) - = s_('Profiles|%{author_name} made a private contribution').html_safe % { author_name: author_name } + = event_user_info(event) + + .event-title.d-flex.flex-wrap + = inline_event_icon(event) + = s_('Profiles|Made a private contribution') diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 5f0ee79cd9b..82693ec832e 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -2,13 +2,15 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span.pushed #{event.action_name} #{event.ref_type} - %strong += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4.pushed #{event.action_name} #{event.ref_type} + %span - commits_link = project_commits_path(project, event.ref_name) - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name) - = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name' + = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name append-right-4' = render "events/event_scope", event: event diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 2682d92fc56..b4b3f4a6b7e 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -14,6 +14,8 @@ = user_status(user) %span.cgray= user.to_reference + = render_if_exists 'shared/members/ee/sso_badge', member: member + - if user == current_user %span.badge.badge-success.prepend-left-5 It's you diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml index f8b3754840d..cf525f2bb2d 100644 --- a/app/views/users/_overview.html.haml +++ b/app/views/users/_overview.html.haml @@ -11,8 +11,8 @@ - if can?(current_user, :read_cross_project) .activities-block - .content-block - %h5.prepend-top-10 + .border-bottom.prepend-top-16 + %h5 = s_('UserProfile|Recent contributions') .overview-content-list{ data: { href: user_path } } .center.light.loading @@ -22,7 +22,7 @@ .col-md-12.col-lg-6 .projects-block - .content-block + .border-bottom.prepend-top-16 %h4 = s_('UserProfile|Personal projects') .overview-content-list{ data: { href: user_projects_path } } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index a66a6f4c777..953ab95735b 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -73,6 +73,8 @@ - pipeline_processing:update_head_pipeline_for_merge_request - pipeline_processing:ci_build_schedule +- deployment:deployments_success + - repository_check:repository_check_clear - repository_check:repository_check_batch - repository_check:repository_check_single_repository diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index c17608f7378..9a865fea621 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -10,13 +10,27 @@ class BuildSuccessWorker def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| create_deployment(build) if build.has_environment? + stop_environment(build) if build.stops_environment? end end # rubocop: enable CodeReuse/ActiveRecord private + ## + # Deprecated: + # As of 11.5, we started creating a deployment record when ci_builds record is created. + # Therefore we no longer need to create a deployment, after a build succeeded. + # We're leaving this code for the transition period, but we can remove this code in 11.6. def create_deployment(build) - CreateDeploymentService.new(build).execute + build.create_deployment.try do |deployment| + deployment.succeed + end + end + + ## + # TODO: This should be processed in DeploymentSuccessWorker once we started storing `action` value in `deployments` records + def stop_environment(build) + build.persisted_environment.fire_state_event(:stop) end end diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb new file mode 100644 index 00000000000..da517f3fb26 --- /dev/null +++ b/app/workers/deployments/success_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Deployments + class SuccessWorker + include ApplicationWorker + + queue_namespace :deployment + + def perform(deployment_id) + Deployment.find_by_id(deployment_id).try do |deployment| + break unless deployment.success? + + UpdateDeploymentService.new(deployment).execute + end + end + end +end diff --git a/changelogs/unreleased/18933-render-index-as-readme.yml b/changelogs/unreleased/18933-render-index-as-readme.yml new file mode 100644 index 00000000000..44acc2c719a --- /dev/null +++ b/changelogs/unreleased/18933-render-index-as-readme.yml @@ -0,0 +1,5 @@ +--- +title: Make index.* render like README.* when it's present in a repository +merge_request: 22639 +author: Jakub Jirutka +type: added diff --git a/changelogs/unreleased/22717-single-letter-identifier-external-issue-tracker.yml b/changelogs/unreleased/22717-single-letter-identifier-external-issue-tracker.yml new file mode 100644 index 00000000000..3f7a0d9204e --- /dev/null +++ b/changelogs/unreleased/22717-single-letter-identifier-external-issue-tracker.yml @@ -0,0 +1,5 @@ +---
+title: "Allowing issues with single letter identifiers to be linked to external issue tracker (f.ex T-123)"
+merge_request: 22717
+author: DÃdac RodrÃguez Arbonès
+type: changed
\ No newline at end of file diff --git a/changelogs/unreleased/25140-disable-stop-button.yml b/changelogs/unreleased/25140-disable-stop-button.yml new file mode 100644 index 00000000000..a6ef52c3155 --- /dev/null +++ b/changelogs/unreleased/25140-disable-stop-button.yml @@ -0,0 +1,5 @@ +--- +title: Disables stop environment button while the deploy is in progress +merge_request: +author: +type: other diff --git a/changelogs/unreleased/49403-redesign-activity-feed.yml b/changelogs/unreleased/49403-redesign-activity-feed.yml new file mode 100644 index 00000000000..cec53a3ef5a --- /dev/null +++ b/changelogs/unreleased/49403-redesign-activity-feed.yml @@ -0,0 +1,4 @@ +title: Redesign activity feed +merge_request: 22217 +author: +type: other diff --git a/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml b/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml new file mode 100644 index 00000000000..89a91e8deaf --- /dev/null +++ b/changelogs/unreleased/51716-add-kubernetes-namespace-background-migration.yml @@ -0,0 +1,5 @@ +--- +title: Add background migration to populate Kubernetes namespaces +merge_request: 22433 +author: +type: added diff --git a/changelogs/unreleased/53362-allow-concurrency-in-puma.yml b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml new file mode 100644 index 00000000000..5fbda0161c1 --- /dev/null +++ b/changelogs/unreleased/53362-allow-concurrency-in-puma.yml @@ -0,0 +1,5 @@ +--- +title: Allow Rails concurrency when running in Puma +merge_request: 22751 +author: +type: performance diff --git a/changelogs/unreleased/53533-fix-broken-link.yml b/changelogs/unreleased/53533-fix-broken-link.yml new file mode 100644 index 00000000000..6d55c75d82e --- /dev/null +++ b/changelogs/unreleased/53533-fix-broken-link.yml @@ -0,0 +1,5 @@ +--- +title: Render unescaped link for failed pipeline status +merge_request: 22807 +author: +type: fixed diff --git a/changelogs/unreleased/53535-sticky-archived.yml b/changelogs/unreleased/53535-sticky-archived.yml new file mode 100644 index 00000000000..8d452d84871 --- /dev/null +++ b/changelogs/unreleased/53535-sticky-archived.yml @@ -0,0 +1,5 @@ +--- +title: Renders warning info when job is archieved +merge_request: +author: +type: added diff --git a/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml b/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml new file mode 100644 index 00000000000..aaae8feb220 --- /dev/null +++ b/changelogs/unreleased/7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries-ee.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of rendering large reports +merge_request: 22835 +author: +type: performance diff --git a/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml b/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml new file mode 100644 index 00000000000..7ef857d38ed --- /dev/null +++ b/changelogs/unreleased/ccr-51052_keep_labels_on_issue.yml @@ -0,0 +1,5 @@ +--- +title: Fixed label removal from issue +merge_request: 22762 +author: +type: fixed diff --git a/changelogs/unreleased/drop-gcp-cluster-table.yml b/changelogs/unreleased/drop-gcp-cluster-table.yml deleted file mode 100644 index 15964ec2eaf..00000000000 --- a/changelogs/unreleased/drop-gcp-cluster-table.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Drop gcp_clusters table -merge_request: 22713 -author: -type: other diff --git a/changelogs/unreleased/rails5-mysql-milliseconds-deployment-spec.yml b/changelogs/unreleased/rails5-mysql-milliseconds-deployment-spec.yml new file mode 100644 index 00000000000..8c71ecebfdb --- /dev/null +++ b/changelogs/unreleased/rails5-mysql-milliseconds-deployment-spec.yml @@ -0,0 +1,5 @@ +--- +title: 'Rails5: fix mysql milliseconds issue in deployment model specs' +merge_request: 22850 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/rake-gitaly-check.yml b/changelogs/unreleased/rake-gitaly-check.yml new file mode 100644 index 00000000000..90fbd62d203 --- /dev/null +++ b/changelogs/unreleased/rake-gitaly-check.yml @@ -0,0 +1,5 @@ +--- +title: Add gitlab:gitaly:check task for Gitaly health check +merge_request: 22063 +author: +type: other diff --git a/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml b/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml new file mode 100644 index 00000000000..af9512b27e9 --- /dev/null +++ b/changelogs/unreleased/remove-experimental-label-from-cluster-views.yml @@ -0,0 +1,5 @@ +--- +title: Removes experimental labels from cluster views +merge_request: 22550 +author: +type: other diff --git a/changelogs/unreleased/stateful_deployments.yml b/changelogs/unreleased/stateful_deployments.yml new file mode 100644 index 00000000000..4caa5ad77b8 --- /dev/null +++ b/changelogs/unreleased/stateful_deployments.yml @@ -0,0 +1,5 @@ +--- +title: Add status to Deployment +merge_request: 22380 +author: +type: changed diff --git a/config/environments/development.rb b/config/environments/development.rb index 23790b84e3c..494ddd72556 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -45,4 +45,6 @@ Rails.application.configure do # Do not log asset requests config.assets.quiet = true + + config.allow_concurrency = defined?(::Puma) end diff --git a/config/environments/production.rb b/config/environments/production.rb index 9941987929c..71195164e7a 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -83,5 +83,5 @@ Rails.application.configure do config.eager_load = true - config.allow_concurrency = false + config.allow_concurrency = defined?(::Puma) end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index d37775e772d..09e21b2c6f2 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -207,6 +207,10 @@ production: &base # endpoint: 'http://127.0.0.1:9000' # default: nil # path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object' + ## Packages (maven repository so far) + packages: + enabled: false + ## GitLab Pages pages: enabled: false diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 0e723cdeb9c..53e1c8778b6 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -29,6 +29,7 @@ - [pipeline_creation, 4] - [pipeline_default, 3] - [pipeline_cache, 3] + - [deployment, 3] - [pipeline_hooks, 2] - [gitlab_shell, 2] - [email_receiver, 2] diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 285436f4324..7a86fe2eb7c 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -180,11 +180,8 @@ class Gitlab::Seeder::CycleAnalytics ref: "refs/heads/#{merge_request.source_branch}") pipeline = service.execute(:push, ignore_skip_ci: true, save_on_errors: false) - pipeline.run! - Timecop.travel rand(1..6).hours.from_now - pipeline.succeed! - - PipelineMetricsWorker.new.perform(pipeline.id) + pipeline.builds.map(&:run!) + pipeline.update_status end end @@ -204,7 +201,8 @@ class Gitlab::Seeder::CycleAnalytics job = merge_request.head_pipeline.builds.where.not(environment: nil).last - CreateDeploymentService.new(job).execute + job.success! + pipeline.update_status end end end diff --git a/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb new file mode 100644 index 00000000000..61d32fe16eb --- /dev/null +++ b/db/migrate/20180927073410_add_index_to_project_deploy_tokens_deploy_token_id.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexToProjectDeployTokensDeployTokenId < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + # MySQL already has index inserted + add_concurrent_index :project_deploy_tokens, :deploy_token_id if Gitlab::Database.postgresql? + end + + def down + remove_concurrent_index(:project_deploy_tokens, :deploy_token_id) if Gitlab::Database.postgresql? + end +end diff --git a/db/migrate/20181015155839_add_finished_at_to_deployments.rb b/db/migrate/20181015155839_add_finished_at_to_deployments.rb new file mode 100644 index 00000000000..1a061bb0f5f --- /dev/null +++ b/db/migrate/20181015155839_add_finished_at_to_deployments.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddFinishedAtToDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :deployments, :finished_at, :datetime_with_timezone + end + + def down + remove_column :deployments, :finished_at, :datetime_with_timezone + end +end diff --git a/db/migrate/20181016141739_add_status_to_deployments.rb b/db/migrate/20181016141739_add_status_to_deployments.rb new file mode 100644 index 00000000000..321172696b4 --- /dev/null +++ b/db/migrate/20181016141739_add_status_to_deployments.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class AddStatusToDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DEPLOYMENT_STATUS_SUCCESS = 2 # Equivalent to Deployment.state_machine.states['success'].value + + DOWNTIME = false + + disable_ddl_transaction! + + ## + # NOTE: + # Ideally, `status` column should not have default value because it should be leveraged by state machine (i.e. application level). + # However, we have to use the default value for avoiding `NOT NULL` violation during the transition period. + # The default value should be removed in the future release. + def up + add_column_with_default(:deployments, + :status, + :integer, + limit: 2, + default: DEPLOYMENT_STATUS_SUCCESS, + allow_null: false) + end + + def down + remove_column(:deployments, :status) + end +end diff --git a/db/migrate/20181022135539_add_index_on_status_to_deployments.rb b/db/migrate/20181022135539_add_index_on_status_to_deployments.rb new file mode 100644 index 00000000000..2eed20aa855 --- /dev/null +++ b/db/migrate/20181022135539_add_index_on_status_to_deployments.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexOnStatusToDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :deployments, [:project_id, :status] + add_concurrent_index :deployments, [:environment_id, :status] + end + + def down + remove_concurrent_index :deployments, [:project_id, :status] + remove_concurrent_index :deployments, [:environment_id, :status] + end +end diff --git a/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb b/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb new file mode 100644 index 00000000000..5896102af1c --- /dev/null +++ b/db/migrate/20181023144439_add_partial_index_for_legacy_successful_deployments.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddPartialIndexForLegacySuccessfulDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'partial_index_deployments_for_legacy_successful_deployments'.freeze + + disable_ddl_transaction! + + def up + add_concurrent_index(:deployments, :id, where: "finished_at IS NULL AND status = 2", name: INDEX_NAME) + end + + def down + remove_concurrent_index_by_name(:deployments, INDEX_NAME) + end +end diff --git a/db/migrate/20181031190559_drop_gcp_clusters_table.rb b/db/migrate/20181031190559_drop_gcp_clusters_table.rb deleted file mode 100644 index 808d474b4fc..00000000000 --- a/db/migrate/20181031190559_drop_gcp_clusters_table.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -class DropGcpClustersTable < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - def up - drop_table :gcp_clusters - end - - def down - create_table :gcp_clusters do |t| - # Order columns by best align scheme - t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } - t.references :user, foreign_key: { on_delete: :nullify } - t.references :service, foreign_key: { on_delete: :nullify } - t.integer :status - t.integer :gcp_cluster_size, null: false - - # Timestamps - t.datetime_with_timezone :created_at, null: false - t.datetime_with_timezone :updated_at, null: false - - # Enable/disable - t.boolean :enabled, default: true - - # General - t.text :status_reason - - # k8s integration specific - t.string :project_namespace - - # Cluster details - t.string :endpoint - t.text :ca_cert - t.text :encrypted_kubernetes_token - t.string :encrypted_kubernetes_token_iv - t.string :username - t.text :encrypted_password - t.string :encrypted_password_iv - - # GKE - t.string :gcp_project_id, null: false - t.string :gcp_cluster_zone, null: false - t.string :gcp_cluster_name, null: false - t.string :gcp_machine_type - t.string :gcp_operation_id - t.text :encrypted_gcp_token - t.string :encrypted_gcp_token_iv - end - end -end diff --git a/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb b/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb new file mode 100644 index 00000000000..f80a2aa6eac --- /dev/null +++ b/db/post_migrate/20181022173835_enqueue_populate_cluster_kubernetes_namespace.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class EnqueuePopulateClusterKubernetesNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + MIGRATION = 'PopulateClusterKubernetesNamespaceTable'.freeze + + disable_ddl_transaction! + + def up + BackgroundMigrationWorker.perform_async(MIGRATION) + end + + def down + Clusters::KubernetesNamespace.delete_all + end +end diff --git a/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb b/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb new file mode 100644 index 00000000000..32b271c472a --- /dev/null +++ b/db/post_migrate/20181030135124_fill_empty_finished_at_in_deployments.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class FillEmptyFinishedAtInDeployments < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DEPLOYMENT_STATUS_SUCCESS = 2 # Equivalent to Deployment.statuses[:success] + + class Deployments < ActiveRecord::Base + self.table_name = 'deployments' + + include EachBatch + end + + def up + FillEmptyFinishedAtInDeployments::Deployments + .where('finished_at IS NULL') + .where('status = ?', DEPLOYMENT_STATUS_SUCCESS) + .each_batch(of: 10_000) do |relation| + relation.update_all('finished_at=created_at') + end + end + + def down + # no-op + end +end diff --git a/db/schema.rb b/db/schema.rb index 5ad8fb7c5a4..fe30c5375f4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -825,13 +825,18 @@ ActiveRecord::Schema.define(version: 20181101144347) do t.datetime "created_at" t.datetime "updated_at" t.string "on_stop" + t.integer "status", limit: 2, default: 2, null: false + t.datetime_with_timezone "finished_at" end add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree add_index "deployments", ["deployable_type", "deployable_id"], name: "index_deployments_on_deployable_type_and_deployable_id", using: :btree add_index "deployments", ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree add_index "deployments", ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree + add_index "deployments", ["environment_id", "status"], name: "index_deployments_on_environment_id_and_status", using: :btree + add_index "deployments", ["id"], name: "partial_index_deployments_for_legacy_successful_deployments", where: "((finished_at IS NULL) AND (status = 2))", using: :btree add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree + add_index "deployments", ["project_id", "status"], name: "index_deployments_on_project_id_and_status", using: :btree create_table "emails", force: :cascade do |t| t.integer "user_id", null: false @@ -919,6 +924,35 @@ ActiveRecord::Schema.define(version: 20181101144347) do add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree + create_table "gcp_clusters", force: :cascade do |t| + t.integer "project_id", null: false + t.integer "user_id" + t.integer "service_id" + t.integer "status" + t.integer "gcp_cluster_size", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.boolean "enabled", default: true + t.text "status_reason" + t.string "project_namespace" + t.string "endpoint" + t.text "ca_cert" + t.text "encrypted_kubernetes_token" + t.string "encrypted_kubernetes_token_iv" + t.string "username" + t.text "encrypted_password" + t.string "encrypted_password_iv" + t.string "gcp_project_id", null: false + t.string "gcp_cluster_zone", null: false + t.string "gcp_cluster_name", null: false + t.string "gcp_machine_type" + t.string "gcp_operation_id" + t.text "encrypted_gcp_token" + t.string "encrypted_gcp_token_iv" + end + + add_index "gcp_clusters", ["project_id"], name: "index_gcp_clusters_on_project_id", unique: true, using: :btree + create_table "gpg_key_subkeys", force: :cascade do |t| t.integer "gpg_key_id", null: false t.binary "keyid" @@ -1589,6 +1623,7 @@ ActiveRecord::Schema.define(version: 20181101144347) do t.datetime_with_timezone "created_at", null: false end + add_index "project_deploy_tokens", ["deploy_token_id"], name: "index_project_deploy_tokens_on_deploy_token_id", using: :btree add_index "project_deploy_tokens", ["project_id", "deploy_token_id"], name: "index_project_deploy_tokens_on_project_id_and_deploy_token_id", unique: true, using: :btree create_table "project_features", force: :cascade do |t| @@ -2401,6 +2436,9 @@ ActiveRecord::Schema.define(version: 20181101144347) do add_foreign_key "fork_network_members", "projects", on_delete: :cascade add_foreign_key "fork_networks", "projects", column: "root_project_id", name: "fk_e7b436b2b5", on_delete: :nullify add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade + add_foreign_key "gcp_clusters", "projects", on_delete: :cascade + add_foreign_key "gcp_clusters", "services", on_delete: :nullify + add_foreign_key "gcp_clusters", "users", on_delete: :nullify add_foreign_key "gpg_key_subkeys", "gpg_keys", on_delete: :cascade add_foreign_key "gpg_keys", "users", on_delete: :cascade add_foreign_key "gpg_signatures", "gpg_key_subkeys", on_delete: :nullify diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index 29af07d12dc..0d863594fc7 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -53,6 +53,7 @@ Git: /usr/bin/git Runs the following rake tasks: - `gitlab:gitlab_shell:check` +- `gitlab:gitaly:check` - `gitlab:sidekiq:check` - `gitlab:app:check` @@ -252,7 +253,7 @@ clear it. To clear all exclusive leases: -DANGER: **DANGER**: +DANGER: **DANGER**: Don't run it while GitLab or Sidekiq is running ```bash diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 3fe79943fdc..96f3861f8d7 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -23,6 +23,9 @@ one of the [Merge request coaches][team]. Depending on the areas your merge request touches, it must be **approved** by one or more [maintainers](https://about.gitlab.com/handbook/engineering/#maintainer): +For approvals, we use the approval functionality found in the merge request +widget. Reviewers can add their approval by [approving additionally](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html#adding-or-removing-an-approval). + 1. If your merge request includes backend changes [^1], it must be **approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_backend)**. 1. If your merge request includes frontend changes [^1], it must be @@ -97,6 +100,9 @@ If a developer who happens to also be a maintainer was involved in a merge reque as a domain expert and/or reviewer, it is recommended that they are not also picked as the maintainer to ultimately approve and merge it. +Maintainers should check before merging if the merge request is approved by the +required approvers. + ## Best practices ### Everyone diff --git a/doc/install/README.md b/doc/install/README.md index 27df03c6ac6..92116305775 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -34,11 +34,11 @@ the hardware requirements. - [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md) - [Install GitLab on Google Kubernetes Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/): video tutorial on the full process of installing GitLab on Google Kubernetes Engine (GKE), pushing an application to GitLab, building the app with GitLab CI/CD, and deploying to production. -- [Install on AWS](https://about.gitlab.com/aws/) -- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) - - Quickly test any version of GitLab on DigitalOcean using Docker Machine. +- [Install on AWS](aws/index.md): Install GitLab on AWS using the community AMIs that GitLab provides. - [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): requirements, installation process, updates. - [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/): video demonstration on how to install GitLab on Kubernetes, build a project, create Review Apps, store Docker images in Container Registry, deploy to production on Kubernetes, and monitor with Prometheus. +- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) - + Quickly test any version of GitLab on DigitalOcean using Docker Machine. ## Database diff --git a/doc/install/aws/img/add_tags.png b/doc/install/aws/img/add_tags.png Binary files differnew file mode 100644 index 00000000000..3572cd5daa1 --- /dev/null +++ b/doc/install/aws/img/add_tags.png diff --git a/doc/install/aws/img/associate_subnet_gateway.png b/doc/install/aws/img/associate_subnet_gateway.png Binary files differnew file mode 100644 index 00000000000..1edca974fca --- /dev/null +++ b/doc/install/aws/img/associate_subnet_gateway.png diff --git a/doc/install/aws/img/associate_subnet_gateway_2.png b/doc/install/aws/img/associate_subnet_gateway_2.png Binary files differnew file mode 100644 index 00000000000..76e101d32a3 --- /dev/null +++ b/doc/install/aws/img/associate_subnet_gateway_2.png diff --git a/doc/install/aws/img/aws_diagram.png b/doc/install/aws/img/aws_diagram.png Binary files differnew file mode 100644 index 00000000000..bcd5c69bbeb --- /dev/null +++ b/doc/install/aws/img/aws_diagram.png diff --git a/doc/install/aws/img/choose_ami.png b/doc/install/aws/img/choose_ami.png Binary files differnew file mode 100644 index 00000000000..034ac92691d --- /dev/null +++ b/doc/install/aws/img/choose_ami.png diff --git a/doc/install/aws/img/create_gateway.png b/doc/install/aws/img/create_gateway.png Binary files differnew file mode 100644 index 00000000000..9408520e050 --- /dev/null +++ b/doc/install/aws/img/create_gateway.png diff --git a/doc/install/aws/img/create_route_table.png b/doc/install/aws/img/create_route_table.png Binary files differnew file mode 100644 index 00000000000..ea72c57257e --- /dev/null +++ b/doc/install/aws/img/create_route_table.png diff --git a/doc/install/aws/img/create_security_group.png b/doc/install/aws/img/create_security_group.png Binary files differnew file mode 100644 index 00000000000..9a0dfccfe37 --- /dev/null +++ b/doc/install/aws/img/create_security_group.png diff --git a/doc/install/aws/img/create_subnet.png b/doc/install/aws/img/create_subnet.png Binary files differnew file mode 100644 index 00000000000..26f5ab1b625 --- /dev/null +++ b/doc/install/aws/img/create_subnet.png diff --git a/doc/install/aws/img/create_vpc.png b/doc/install/aws/img/create_vpc.png Binary files differnew file mode 100644 index 00000000000..a678f7013fd --- /dev/null +++ b/doc/install/aws/img/create_vpc.png diff --git a/doc/install/aws/img/ec_az.png b/doc/install/aws/img/ec_az.png Binary files differnew file mode 100644 index 00000000000..22a8291c593 --- /dev/null +++ b/doc/install/aws/img/ec_az.png diff --git a/doc/install/aws/img/ec_subnet.png b/doc/install/aws/img/ec_subnet.png Binary files differnew file mode 100644 index 00000000000..c44fb4485e3 --- /dev/null +++ b/doc/install/aws/img/ec_subnet.png diff --git a/doc/install/aws/img/policies.png b/doc/install/aws/img/policies.png Binary files differnew file mode 100644 index 00000000000..e99497a52a2 --- /dev/null +++ b/doc/install/aws/img/policies.png diff --git a/doc/install/aws/img/rds_subnet_group.png b/doc/install/aws/img/rds_subnet_group.png Binary files differnew file mode 100644 index 00000000000..7c6157e38e0 --- /dev/null +++ b/doc/install/aws/img/rds_subnet_group.png diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md new file mode 100644 index 00000000000..53fe1a6b25b --- /dev/null +++ b/doc/install/aws/index.md @@ -0,0 +1,655 @@ +# Installing GitLab on Amazon Web Services (AWS) + +To install GitLab on AWS, you can use the Amazon Machine Images (AMIs) that GitLab +provides with [each release](https://about.gitlab.com/releases/). + +This page offers a walkthrough of a common HA (Highly Available) configuration +for GitLab on AWS. You should customize it to accommodate your needs. + +## Introduction + +GitLab on AWS can leverage many of the services that are already +configurable with GitLab High Availability (HA). These services offer a great deal of +flexibility and can be adapted to the needs of most companies, while enabling the +automation of both vertical and horizontal scaling. + +In this guide, we'll go through a basic HA setup where we'll start by +configuring our Virtual Private Cloud and subnets to later integrate +services such as RDS for our database server and ElastiCache as a Redis +cluster to finally manage them within an auto scaling group with custom +scaling policies. + +## Requirements + +In addition to having a basic familiarity with [AWS](https://docs.aws.amazon.com/) and [Amazon EC2](https://docs.aws.amazon.com/ec2/), you will need: + +- [An AWS account](https://console.aws.amazon.com/console/home) +- [To create or upload an SSH key](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html) + to connect to the instance via SSH +- A domain name for the GitLab instance + +## Architecture + +Below is a diagram of the recommended architecture. + +![AWS architecture diagram](img/aws_diagram.png) + +## AWS costs + +Here's a list of the AWS services we will use, with links to pricing information: + +- **EC2**: GitLab will deployed on shared hardware which means + [on-demand pricing](https://aws.amazon.com/ec2/pricing/on-demand) + will apply. If you want to run it on a dedicated or reserved instance, + consult the [EC2 pricing page](https://aws.amazon.com/ec2/pricing/) for more + information on the cost. +- **EBS**: We will also use an EBS volume to store the Git data. See the + [Amazon EBS pricing](https://aws.amazon.com/ebs/pricing/). +- **S3**: We will use S3 to store backups, artifacts, LFS objects, etc. See the + [Amazon S3 pricing](https://aws.amazon.com/s3/pricing/). +- **ALB**: An Application Load Balancer will be used to route requests to the + GitLab instance. See the [Amazon ELB pricing](https://aws.amazon.com/elasticloadbalancing/pricing/). +- **RDS**: An Amazon Relational Database Service using PostgreSQL will be used + to provide a High Availability database configuration. See the + [Amazon RDS pricing](https://aws.amazon.com/rds/postgresql/pricing/). +- **ElastiCache**: An in-memory cache environment will be used to provide a + High Availability Redis configuration. See the + [Amazon ElastiCache pricing](https://aws.amazon.com/elasticache/pricing/). + +## Creating an IAM EC2 instance role and profile +To minimize the permissions of the user, we'll create a new [IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) +role with limited access: + +1. Navigate to the IAM dashboard https://console.aws.amazon.com/iam/home and + click **Create role**. +1. Create a new role by selecting **AWS service > EC2**, then click + **Next: Permissions**. +1. Choose **AmazonEC2FullAccess** and **AmazonS3FullAccess**, then click **Next: Review**. +1. Give the role the name `GitLabAdmin` and click **Create role**. + +## Configuring the network + +We'll start by creating a VPC for our GitLab cloud infrastructure, then +we can create subnets to have public and private instances in at least +two [Availability Zones (AZs)](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html). Public subnets will require a Route Table keep and an associated +Internet Gateway. + +### Creating the Virtual Private Cloud (VPC) + +We'll now create a VPC, a virtual networking environment that you'll control: + +1. Navigate to https://console.aws.amazon.com/vpc/home. +1. Select **Your VPCs** from the left menu and then click **Create VPC**. + At the "Name tag" enter `gitlab-vpc` and at the "IPv4 CIDR block" enter + `10.0.0.0/16`. If you don't require dedicated hardware, you can leave + "Tenancy" as default. Click **Yes, Create** when ready. + + ![Create VPC](img/create_vpc.png) + +### Subnets + +Now, let's create some subnets in different Availability Zones. Make sure +that each subnet is associated the the VPC we just created and +that CIDR blocks don't overlap. This will also +allow us to enable multi AZ for redundancy. + +We will create private and public subnets to match load balancers and +RDS instances as well: + +1. Select **Subnets** from the left menu. +1. Click **Create subnet**. Give it a descriptive name tag based on the IP, + for example `gitlab-public-10.0.0.0`, select the VPC we created previously, + and at the IPv4 CIDR block let's give it a 24 subnet `10.0.0.0/24`: + + ![Create subnet](img/create_subnet.png) + +1. Follow the same steps to create all subnets: + + | Name tag | Type |Availability Zone | CIDR block | + | -------- | ---- | ---------------- | ---------- | + | gitlab-public-10.0.0.0 | public | us-west-2a | 10.0.0.0 | + | gitlab-private-10.0.1.0 | private | us-west-2a | 10.0.1.0 | + | gitlab-public-10.0.2.0 | public | us-west-2b | 10.0.2.0 | + | gitlab-private-10.0.3.0 | private | us-west-2b | 10.0.3.0 | + +### Route Table + +Up to now all our subnets are private. We need to create a Route Table +to associate an Internet Gateway. On the same VPC dashboard: + +1. Select **Route Tables** from the left menu. +1. Click **Create Route Table**. +1. At the "Name tag" enter `gitlab-public` and choose `gitlab-vpc` under "VPC". +1. Hit **Yes, Create**. + +### Internet Gateway + +Now, still on the same dashboard, go to Internet Gateways and +create a new one: + +1. Select **Internet Gateways** from the left menu. +1. Click **Create internet gateway**, give it the name `gitlab-gateway` and + click **Create**. +1. Select it from the table, and then under the **Actions** dropdown choose + "Attach to VPC". + + ![Create gateway](img/create_gateway.png) + +1. Choose `gitlab-vpc` from the list and hit **Attach**. + +### Configuring subnets + +We now need to add a new target which will be our Internet Gateway and have +it receive traffic from any destination. + +1. Select **Route Tables** from the left menu and select the `gitlab-public` + route to show the options at the bottom. +1. Select the **Routes** tab, hit **Edit > Add another route** and set `0.0.0.0/0` + as destination. In the target, select the `gitlab-gateway` we created previously. + Hit **Save** once done. + + ![Associate subnet with gateway](img/associate_subnet_gateway.png) + +Next, we must associate the **public** subnets to the route table: + +1. Select the **Subnet Associations** tab and hit **Edit**. +1. Check only the public subnet and hit **Save**. + + ![Associate subnet with gateway](img/associate_subnet_gateway_2.png) + +--- + +Now that we're done with the network, let's create a security group. + +## Creating a security group + +The security group is basically the firewall: + +1. Select **Security Groups** from the left menu. +1. Click **Create Security Group** and fill in the details. Give it a name, + add a description, and choose the VPC we created previously +1. Select the security group from the list and at the the bottom select the + Inbound Rules tab. You will need to open the SSH, HTTP, and HTTPS ports. Set + the source to `0.0.0.0/0`. + + ![Create security group](img/create_security_group.png) + + TIP: **Tip:** + Based on best practices, you should allow SSH traffic from only a known + host or CIDR block. In that case, change the SSH source to be custom and give + it the IP you want to SSH from. + +1. When done, click **Save**. + +## PostgreSQL with RDS + +For our database server we will use Amazon RDS which offers Multi AZ +for redundancy. Let's start by creating a subnet group and then we'll +create the actual RDS instance. + +### RDS Subnet Group + +1. Navigate to the RDS dashboard and select **Subnet Groups** from the left menu. +1. Give it a name (`gitlab-rds-group`), a description, and choose the VPC from + the VPC dropdown. +1. Click "Add all the subnets related to this VPC" and + remove the public ones, we only want the **private subnets**. + In the end, you should see `10.0.1.0/24` and `10.0.3.0/24` (as + we defined them in the [subnets section](#subnets)). + Click **Create** when ready. + + ![RDS Subnet Group](img/rds_subnet_group.png) + +### Creating the database + +Now, it's time to create the database: + +1. Select **Instances** from the left menu and click **Create database**. +1. Select PostgreSQL and click **Next**. +1. Since this is a production server, let's choose "Production". Click **Next**. +1. Let's see the instance specifications: + 1. Leave the license model as is (`postgresql-license`). + 1. For the version, select the latest of the 9.6 series (check the + [database requirements](../../install/requirements.md#postgresql-requirements)) + if there are any updates on this). + 1. For the size, let's select a `t2.medium` instance. + 1. Multi-AZ-deployment is recommended as redundancy, so choose "Create + replica in different zone". Read more at + [High Availability (Multi-AZ)](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html). + 1. A Provisioned IOPS (SSD) storage type is best suited for HA (though you can + choose a General Purpose (SSD) to reduce the costs). Read more about it at + [Storage for Amazon RDS](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html). + +1. The rest of the settings on this page request a DB isntance identifier, username + and a master password. We've chosen to use `gitlab-db-ha`, `gitlab` and a + very secure password respectively. Keep these in hand for later. +1. Click **Next** to proceed to the advanced settings. +1. Make sure to choose our gitlab VPC, our subnet group, set public accessibility to + **No**, and to leave it to create a new security group. The only additional + change which will be helpful is the database name for which we can use + `gitlabhq_production`. At the very bottom, there's an option to enable + auto updates to minor versions. You may want to turn it off. +1. When done, click **Create database**. + +### Installing the `pg_trgm` extension for PostgreSQL + +Once the database is created, connect to your new RDS instance to verify access +and to install a required extension. + +You can find the host or endpoint by selecting the instance you just created and +after the details drop down you'll find it labeled as 'Endpoint'. Do not to +include the colon and port number: + +```sh +sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production +``` + +At the psql prompt create the extension and then quit the session: + +```sh +psql (9.4.7) +Type "help" for help. + +gitlab=# CREATE EXTENSION pg_trgm; +gitlab=# \q +``` + +--- + +Now that the database is created, let's move on setting up Redis with ElasticCache. + +## Redis with ElastiCache + +ElastiCache is an in-memory hosted caching solution. Redis maintains its own +persistence and is used for certain types of the GitLab application. + +To set up Redis: + +1. Navigate to the ElastiCache dashboard from your AWS console. +1. Go to **Subnet Groups** in the left menu, and create a new subnet group. + Make sure to select our VPC and its [private subnets](#subnets). Click + **Create** when ready. + + ![ElastiCache subnet](img/ec_subnet.png) + +1. Select **Redis** on the left menu and click **Create** to create a new + Redis cluster. Depending on your load, you can choose whether to enable + cluster mode or not. Even without cluster mode on, you still get the + chance to deploy Redis in multi availability zones. In this guide, we chose + not to enable it. +1. In the settings section: + 1. Give the cluster a name (`gitlab-redis`) and a description. + 1. For the version, select the latest of `3.2` series (e.g., `3.2.10`). + 1. Select the node type and the number of replicas. +1. In the advanced settings section: + 1. Select the multi-AZ auto-failover option. + 1. Select the subnet group we created previously. + 1. Manually select the preferred availability zones, and under "Replica 2" + choose a different zone than the other two. + + ![Redis availability zones](img/ec_az.png) + +1. In the security settings, edit the security groups and choose the + `gitlab-security-group` we had previously created. +1. Leave the rest of the settings to their default values or edit to your liking. +1. When done, click **Create**. + +## RDS and Redis Security Group + +Let's navigate to our EC2 security groups and add a small change for our EC2 +instances to be able to connect to RDS. First, copy the security group name we +defined, namely `gitlab-security-group`, select the RDS security group and edit the +inbound rules. Choose the rule type to be PostgreSQL and paste the name under +source. + +Similar to the above, jump to the `gitlab-security-group` group +and add a custom TCP rule for port `6379` accessible within itself. + +## Load Balancer + +On the EC2 dashboard, look for Load Balancer on the left column: + +1. Click the **Create Load Balancer** button. + 1. Choose the Application Load Balancer. + 1. Give it a name (`gitlab-loadbalancer`) and set the scheme to "internet-facing". + 1. In the "Listeners" section, make sure it has HTTP and HTTPS. + 1. In the "Availability Zones" section, select the `gitlab-vpc` we have created + and associate the **public subnets**. +1. Click **Configure Security Settings** to go to the next section to + select the TLS certificate. When done, go to the next step. +1. In the "Security Groups" section, create a new one by giving it a name + (`gitlab-loadbalancer-sec-group`) and allow both HTTP ad HTTPS traffic + from anywhere (`0.0.0.0/0, ::/0`). +1. In the next step, configure the routing and select an existing target group + (`gitlab-public`). The Load Balancer Health will allow us to indicate where to + ping and what makes up a healthy or unhealthy instance. +1. Leave the "Register Targets" section as is, and finally review the settings + and create the ELB. + +After the Load Balancer is up and running, you can revisit your Security +Groups to refine the access only through the ELB and any other requirement +you might have. + +## Deploying GitLab inside an auto scaling group + +We'll use AWS's wizard to deploy GitLab and then SSH into the instance to +configure the PostgreSQL and Redis connections. + +The Auto Scaling Group option is available through the EC2 dashboard on the left +sidebar. + +1. Click **Create Auto Scaling group**. +1. Create a new launch configuration. + +### Choose the AMI + +Choose the AMI: + +1. Go to the Community AMIs and search for `GitLab EE <version>` + where `<version>` the latest version as seen on the + [releases page](https://about.gitlab.com/releases/). + + ![Choose AMI](img/choose_ami.png) + +### Choose an instance type + +You should choose an instance type based on your workload. Consult +[the hardware requirements](../requirements.md#hardware-requirements) to choose +one that fits your needs (at least `c4.xlarge`, which is enough to accommodate 100 users): + +1. Choose the your instance type. +1. Click **Next: Configure Instance Details**. + +### Configure details + +In this step we'll configure some details: + +1. Enter a name (`gitlab-autoscaling`). +1. Select the IAM role we created. +1. Optionally, enable CloudWatch and the EBS-optimized instance settings. +1. In the "Advanced Details" section, set the IP address type to + "Do not assign a public IP address to any instances." +1. Click **Next: Add Storage**. + +### Add storage + +The root volume is 8GB by default and should be enough given that we won't store +any data there. Let's create a new EBS volume that will host the Git data. Its +size depends on your needs and you can always migrate to a bigger volume later. +You will be able to [set up that volume](#setting-up-the-ebs-volume) +after the instance is created. + +### Configure security group + +As a last step, configure the security group: + +1. Select the existing load balancer security group we have [created](#load-balancer). +1. Select **Review**. + +### Review and launch + +Now is a good time to review all the previous settings. When ready, click +**Create launch configuration** and select the SSH key pair with which you will +connect to the instance. + +### Create Auto Scaling Group + +We are now able to start creating our Auto Scaling Group: + +1. Give it a group name. +1. Set the group size to 2 as we want to always start with two instances. +1. Assign it our network VPC and add the **private subnets**. +1. In the "Advanced Details" section, choose to receive traffic from ELBs + and select our ELB. +1. Choose the ELB health check. +1. Click **Next: Configure scaling policies**. + +This is the really great part of Auto Scaling; we get to choose when AWS +launches new instances and when it removes them. For this group we'll +scale between 2 and 4 instances where one instance will be added if CPU +utilization is greater than 60% and one instance is removed if it falls +to less than 45%. + +![Auto scaling group policies](img/policies.png) + +Finally, configure notifications and tags as you see fit, and create the +auto scaling group. + +You'll notice that after we save the configuration, AWS starts launching our two +instances in different AZs and without a public IP which is exactly what +we intended. + +## After deployment + +After a few minutes, the instances should be up and accessible via the internet. +Let's connect to the primary and configure some things before logging in. + +### Configuring GitLab to connect with postgres and Redis + +While connected to your server, let's connect to the RDS instance to verify +access and to install a required extension: + +```sh +sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production +``` + +Edit the `gitlab.rb` file at `/etc/gitlab/gitlab.rb` +find the `external_url 'http://gitlab.example.com'` option and change it +to the domain you will be using or the public IP address of the current +instance to test the configuration. + +For a more detailed description about configuring GitLab, see [Configuring GitLab for HA](../../administration/high_availability/gitlab.md) + +Now look for the GitLab database settings and uncomment as necessary. In +our current case we'll specify the database adapter, encoding, host, name, +username, and password: + +```ruby +# Disable the built-in Postgres +postgresql['enable'] = false + +# Fill in the connection details +gitlab_rails['db_adapter'] = "postgresql" +gitlab_rails['db_encoding'] = "unicode" +gitlab_rails['db_database'] = "gitlabhq_production" +gitlab_rails['db_username'] = "gitlab" +gitlab_rails['db_password'] = "mypassword" +gitlab_rails['db_host'] = "<rds-endpoint>" +``` + +Next, we need to configure the Redis section by adding the host and +uncommenting the port: + +```ruby +# Disable the built-in Redis +redis['enable'] = false + +# Fill in the connection details +gitlab_rails['redis_host'] = "<redis-endpoint>" +gitlab_rails['redis_port'] = 6379 +``` + +Finally, reconfigure GitLab for the change to take effect: + + +```sh +sudo gitlab-ctl reconfigure +``` + +You might also find it useful to run a check and a service status to make sure +everything has been setup correctly: + +```sh +sudo gitlab-rake gitlab:check +sudo gitlab-ctl status +``` + +If everything looks good, you should be able to reach GitLab in your browser. + +### Setting up the EBS volume + +The EBS volume will host the Git repositories data: + +1. First, format the `/dev/xvdb` volume and then mount it under the directory + where the data will be stored. For example, `/mnt/gitlab-data/`. +1. Tell GitLab to store its data in the new directory by editing + `/etc/gitlab/gitlab.rb` with your editor: + + ```ruby + git_data_dirs({ + "default" => { "path" => "/mnt/gitlab-data" } + }) + ``` + + where `/mnt/gitlab-data` the location where you will store the Git data. + +1. Save the file and reconfigure GitLab: + + ```sh + sudo gitlab-ctl reconfigure + ``` + +TIP: **Tip:** +If you wish to add more than one data volumes to store the Git repositories, +read the [repository storage paths docs](../../administration/repository_storage_paths.md). + +### Setting up Gitaly + +Gitaly is a service that provides high-level RPC access to Git repositories. +It should be enabled and configured in a separate EC2 instance on the +[private VPC](#subnets) we configured previously. + +Follow the [documentation to set up Gitaly](../../administration/gitaly/index.md). + +### Using Amazon S3 object storage + +GitLab stores many objects outside the Git repository, many of which can be +uploaded to S3. That way, you can offload the root disk volume of these objects +which would otherwise take much space. + +In particular, you can store in S3: + +- [The Git LFS objects](../../workflow/lfs/lfs_administration.md#s3-for-omnibus-installations) ((Omnibus GitLab installations)) +- [The Container Registry images](../../administration/container_registry.md#container-registry-storage-driver) (Omnibus GitLab installations) +- [The GitLab CI/CD job artifacts](../../administration/job_artifacts.md#using-object-storage) (Omnibus GitLab installations) + +### Setting up a domain name + +After you SSH into the instance, configure the domain name: + +1. Open `/etc/gitlab/gitlab.rb` with your preferred editor. +1. Edit the `external_url` value: + + ```ruby + external_url 'http://example.com' + ``` + +1. Reconfigure GitLab: + + ```sh + sudo gitlab-ctl reconfigure + ``` + +You should now be able to reach GitLab at the URL you defined. To use HTTPS +(recommended), see the [HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https). + +### Logging in for the first time + +If you followed the previous section, you should be now able to visit GitLab +in your browser. The very first time, you will be asked to set up a password +for the `root` user which has admin privileges on the GitLab instance. + +After you set it up, login with username `root` and the newly created password. + +## Health check and monitoring with Prometheus + +Apart from Amazon's Cloudwatch which you can enable on various services, +GitLab provides its own integrated monitoring solution based on Prometheus. +For more information on how to set it up, visit the +[GitLab Prometheus documentation](../../administration/monitoring/prometheus/index.md) + +GitLab also has various [health check endpoints](../..//user/admin_area/monitoring/health_check.md) +that you can ping and get reports. + +## GitLab Runners + +If you want to take advantage of [GitLab CI/CD](../../ci/README.md), you have to +set up at least one [GitLab Runner](https://docs.gitlab.com/runner/). + +Read more on configuring an +[autoscaling GitLab Runner on AWS](https://docs.gitlab.com/runner/configuration/runner_autoscale_aws/). + +## Backup and restore + +GitLab provides [a tool to backup](../../raketasks/backup_restore.md#creating-a-backup-of-the-gitlab-system) +and restore its Git data, database, attachments, LFS objects, etc. + +Some important things to know: + +- The backup/restore tool **does not** store some configuration files, like secrets; you'll + need to [configure this yourself](../../raketasks/backup_restore.md#storing-configuration-files). +- By default, the backup files are stored locally, but you can + [backup GitLab using S3](../../raketasks/backup_restore.md#using-amazon-s3). +- You can [exclude specific directories form the backup](../../raketasks/backup_restore.md#excluding-specific-directories-from-the-backup). + +### Backing up GitLab + +To back up GitLab: + +1. SSH into your instance. +1. Take a backup: + + ```sh + sudo gitlab-rake gitlab:backup:create + ``` + +### Restoring GitLab from a backup + +To restore GitLab, first review the [restore documentation](../../raketasks/backup_restore.md#restore), +and primarily the restore prerequisites. Then, follow the steps under the +[Omnibus installations section](../../raketasks/backup_restore.md#restore-for-omnibus-installations). + +## Updating GitLab + +GitLab releases a new version every month on the 22nd. Whenever a new version is +released, you can update your GitLab instance: + +1. SSH into your instance +1. Take a backup: + + ```sh + sudo gitlab-rake gitlab:backup:create + ``` + +1. Update the repositories and install GitLab: + + ```sh + sudo apt update + sudo apt install gitlab-ee + ``` + +After a few minutes, the new version should be up and running. + +## Conclusion + +In this guide, we went mostly through scaling and some redundancy options, +your mileage may vary. + +Keep in mind that all Highly Available solutions come with a trade-off between +cost/complexity and uptime. The more uptime you want, the more complex the solution. +And the more complex the solution, the more work is involved in setting up and +maintaining it. + +Have a read through these other resources and feel free to +[open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/new) +to request additional material: + +- [GitLab High Availability](https://docs.gitlab.com/ee/administration/high_availability/): + GitLab supports several different types of clustering and high-availability. +- [Geo replication](https://docs.gitlab.com/ee/administration/geo/replication/): + Geo is the solution for widely distributed development teams. +- [Omnibus GitLab](https://docs.gitlab.com/omnibus/) - Everything you need to know + about administering your GitLab instance. +- [Upload a license](https://docs.gitlab.com/ee/user/admin_area/license.html): + Activate all GitLab Enterprise Edition functionality with a license. +- [Pricing](https://about.gitlab.com/pricing): Pricing for the different tiers. diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index beff4b89424..6d822d3f7f2 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -53,6 +53,32 @@ To get started with the command line, please read through the Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository. +### Repository README and index files + +When a `README` or `index` file is present in a repository, its contents will be +automatically pre-rendered by GitLab without opening it. + +They can either be plain text or have an extension of a supported markup language: + +- Asciidoc: `README.adoc` or `index.adoc` +- Markdown: `README.md` or `index.md` +- reStructuredText: `README.rst` or `index.rst` +- Text: `README.txt` or `index.txt` + +Some things to note about precedence: + +1. When both a `README` and an `index` file are present, the `README` will always + take precedence. +1. When more than one file is present with different extensions, they are + ordered alphabetically, with the exception of a file without an extension + which will always be last in precedence. For example, `README.adoc` will take + precedence over `README.md`, and `README.rst` will take precedence over + `README`. + +NOTE: **Note:** +`index` files without an extension will not automatically pre-render. You'll +have to explicitly open them to see their contents. + ### Jupyter Notebook files > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2508) in GitLab 9.1 @@ -165,7 +191,7 @@ minutes. ![Repository Languages bar](img/repository_languages.png) -Not all files are detected, among others; documentation, +Not all files are detected, among others; documentation, vendored code, and most markup languages are excluded. This behaviour can be adjusted by overriding the default. For example, to enable `.proto` files to be detected, add the following to `.gitattributes` in the root of your repository. diff --git a/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb new file mode 100644 index 00000000000..35bfc381180 --- /dev/null +++ b/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true +# +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class PopulateClusterKubernetesNamespaceTable + include Gitlab::Database::MigrationHelpers + + BATCH_SIZE = 1_000 + + module Migratable + class KubernetesNamespace < ActiveRecord::Base + self.table_name = 'clusters_kubernetes_namespaces' + end + + class ClusterProject < ActiveRecord::Base + include EachBatch + + self.table_name = 'cluster_projects' + + belongs_to :project + + def self.with_no_kubernetes_namespace + where.not(id: Migratable::KubernetesNamespace.select(:cluster_project_id)) + end + + def namespace + slug = "#{project.path}-#{project.id}".downcase + slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') + end + + def service_account + "#{namespace}-service-account" + end + end + + class Project < ActiveRecord::Base + self.table_name = 'projects' + end + end + + def perform + cluster_projects_with_no_kubernetes_namespace.each_batch(of: BATCH_SIZE) do |cluster_projects_batch, index| + sql_values = sql_values_for(cluster_projects_batch) + + insert_into_cluster_kubernetes_namespace(sql_values) + end + end + + private + + def cluster_projects_with_no_kubernetes_namespace + Migratable::ClusterProject.with_no_kubernetes_namespace + end + + def sql_values_for(cluster_projects) + cluster_projects.map do |cluster_project| + values_for_cluster_project(cluster_project) + end + end + + def values_for_cluster_project(cluster_project) + { + cluster_project_id: cluster_project.id, + cluster_id: cluster_project.cluster_id, + project_id: cluster_project.project_id, + namespace: cluster_project.namespace, + service_account_name: cluster_project.service_account, + created_at: 'NOW()', + updated_at: 'NOW()' + } + end + + def insert_into_cluster_kubernetes_namespace(rows) + Gitlab::Database.bulk_insert(Migratable::KubernetesNamespace.table_name, + rows, + disable_quote: [:created_at, :updated_at]) + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb index c882241ef6a..aa627bdb009 100644 --- a/lib/gitlab/ci/pipeline/chain/create.rb +++ b/lib/gitlab/ci/pipeline/chain/create.rb @@ -7,26 +7,11 @@ module Gitlab class Create < Chain::Base include Chain::Helpers - # rubocop: disable CodeReuse/ActiveRecord def perform! - ::Ci::Pipeline.transaction do - pipeline.save! - - ## - # Create environments before the pipeline starts. - # - pipeline.builds.each do |build| - if build.has_environment? - project.environments.find_or_create_by( - name: build.expanded_environment_name - ) - end - end - end + pipeline.save! rescue ActiveRecord::RecordInvalid => e error("Failed to persist the pipeline: #{e}") end - # rubocop: enable CodeReuse/ActiveRecord def break? !pipeline.persisted? diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb index 4d89ee5a669..d6338b09e3d 100644 --- a/lib/gitlab/file_detector.rb +++ b/lib/gitlab/file_detector.rb @@ -8,7 +8,7 @@ module Gitlab module FileDetector PATTERNS = { # Project files - readme: %r{\Areadme[^/]*\z}i, + readme: %r{\A(readme|index)[^/]*\z}i, changelog: %r{\A(changelog|history|changes|news)[^/]*\z}i, license: %r{\A((un)?licen[sc]e|copying)(\.[^/]+)?\z}i, contributing: %r{\Acontributing[^/]*\z}i, diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index fcc92341c40..20cd257bb98 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -720,6 +720,26 @@ module Gitlab end end + # Fetch remote for repository + # + # remote - remote name + # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication + # forced - should we use --force flag? + # no_tags - should we use --no-tags flag? + # prune - should we use --prune flag? + def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, prune: true) + wrapped_gitaly_errors do + gitaly_repository_client.fetch_remote( + remote, + ssh_auth: ssh_auth, + forced: forced, + no_tags: no_tags, + prune: prune, + timeout: GITLAB_PROJECTS_TIMEOUT + ) + end + end + def blob_at(sha, path) Gitlab::Git::Blob.find(self, sha, path) unless Gitlab::Git.blank_ref?(sha) end diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb index 142b7d1a472..d419fa66e57 100644 --- a/lib/gitlab/markup_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -4,10 +4,11 @@ module Gitlab module MarkupHelper extend self - MARKDOWN_EXTENSIONS = %w(mdown mkd mkdn md markdown).freeze - ASCIIDOC_EXTENSIONS = %w(adoc ad asciidoc).freeze - OTHER_EXTENSIONS = %w(textile rdoc org creole wiki mediawiki rst).freeze + MARKDOWN_EXTENSIONS = %w[mdown mkd mkdn md markdown].freeze + ASCIIDOC_EXTENSIONS = %w[adoc ad asciidoc].freeze + OTHER_EXTENSIONS = %w[textile rdoc org creole wiki mediawiki rst].freeze EXTENSIONS = MARKDOWN_EXTENSIONS + ASCIIDOC_EXTENSIONS + OTHER_EXTENSIONS + PLAIN_FILENAMES = %w[readme index].freeze # Public: Determines if a given filename is compatible with GitHub::Markup. # @@ -43,7 +44,7 @@ module Gitlab # # Returns boolean def plain?(filename) - extension(filename) == 'txt' || filename.casecmp('readme').zero? + extension(filename) == 'txt' || plain_filename?(filename) end def previewable?(filename) @@ -55,5 +56,9 @@ module Gitlab def extension(filename) File.extname(filename).downcase.delete('.') end + + def plain_filename?(filename) + PLAIN_FILENAMES.include?(filename.downcase) + end end end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 16c1edb2f11..c6a6fb9b5ce 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -108,23 +108,6 @@ module Gitlab success end - # Fetch remote for repository - # - # repository - an instance of Git::Repository - # remote - remote name - # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication - # forced - should we use --force flag? - # no_tags - should we use --no-tags flag? - # - # Ex. - # fetch_remote(my_repo, "upstream") - # - def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true) - wrapped_gitaly_errors do - repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune) - end - end - # Move repository reroutes to mv_directory which is an alias for # mv_namespace. Given the underlying implementation is a move action, # indescriminate of what the folders might be. diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index e5b5f3548e4..663bebfe71a 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -1,6 +1,7 @@ namespace :gitlab do desc 'GitLab | Check the configuration of GitLab and its environment' task check: %w{gitlab:gitlab_shell:check + gitlab:gitaly:check gitlab:sidekiq:check gitlab:incoming_email:check gitlab:ldap:check @@ -44,13 +45,7 @@ namespace :gitlab do start_checking "GitLab Shell" check_gitlab_shell - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - check_repo_base_exists - check_repo_base_is_not_symlink - check_repo_base_user_and_group - check_repo_base_permissions - check_repos_hooks_directory_is_link - end + check_repos_hooks_directory_is_link check_gitlab_shell_self_test finished_checking "GitLab Shell" @@ -59,116 +54,6 @@ namespace :gitlab do # Checks ######################## - def check_repo_base_exists - puts "Repo base directory exists?" - - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_base_path = repository_storage.legacy_disk_path - print "#{name}... " - - if File.exist?(repo_base_path) - puts "yes".color(:green) - else - puts "no".color(:red) - puts "#{repo_base_path} is missing".color(:red) - try_fixing_it( - "This should have been created when setting up GitLab Shell.", - "Make sure it's set correctly in config/gitlab.yml", - "Make sure GitLab Shell is installed correctly." - ) - for_more_information( - see_installation_guide_section "GitLab Shell" - ) - fix_and_rerun - end - end - end - - def check_repo_base_is_not_symlink - puts "Repo storage directories are symlinks?" - - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_base_path = repository_storage.legacy_disk_path - print "#{name}... " - - unless File.exist?(repo_base_path) - puts "can't check because of previous errors".color(:magenta) - break - end - - unless File.symlink?(repo_base_path) - puts "no".color(:green) - else - puts "yes".color(:red) - try_fixing_it( - "Make sure it's set to the real directory in config/gitlab.yml" - ) - fix_and_rerun - end - end - end - - def check_repo_base_permissions - puts "Repo paths access is drwxrws---?" - - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_base_path = repository_storage.legacy_disk_path - print "#{name}... " - - unless File.exist?(repo_base_path) - puts "can't check because of previous errors".color(:magenta) - break - end - - if File.stat(repo_base_path).mode.to_s(8).ends_with?("2770") - puts "yes".color(:green) - else - puts "no".color(:red) - try_fixing_it( - "sudo chmod -R ug+rwX,o-rwx #{repo_base_path}", - "sudo chmod -R ug-s #{repo_base_path}", - "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s" - ) - for_more_information( - see_installation_guide_section "GitLab Shell" - ) - fix_and_rerun - end - end - end - - def check_repo_base_user_and_group - gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user - puts "Repo paths owned by #{gitlab_shell_ssh_user}:root, or #{gitlab_shell_ssh_user}:#{Gitlab.config.gitlab_shell.owner_group}?" - - Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_base_path = repository_storage.legacy_disk_path - print "#{name}... " - - unless File.exist?(repo_base_path) - puts "can't check because of previous errors".color(:magenta) - break - end - - user_id = uid_for(gitlab_shell_ssh_user) - root_group_id = gid_for('root') - group_ids = [root_group_id, gid_for(Gitlab.config.gitlab_shell.owner_group)] - if File.stat(repo_base_path).uid == user_id && group_ids.include?(File.stat(repo_base_path).gid) - puts "yes".color(:green) - else - puts "no".color(:red) - puts " User id for #{gitlab_shell_ssh_user}: #{user_id}. Groupd id for root: #{root_group_id}".color(:blue) - try_fixing_it( - "sudo chown -R #{gitlab_shell_ssh_user}:root #{repo_base_path}" - ) - for_more_information( - see_installation_guide_section "GitLab Shell" - ) - fix_and_rerun - end - end - end - def check_repos_hooks_directory_is_link print "hooks directories in repos are links: ... " @@ -247,6 +132,26 @@ namespace :gitlab do end end + namespace :gitaly do + desc 'GitLab | Check the health of Gitaly' + task check: :gitlab_environment do + warn_user_is_not_gitlab + start_checking 'Gitaly' + + Gitlab::HealthChecks::GitalyCheck.readiness.each do |result| + print "#{result.labels[:shard]} ... " + + if result.success + puts 'OK'.color(:green) + else + puts "FAIL: #{result.message}".color(:red) + end + end + + finished_checking 'Gitaly' + end + end + namespace :sidekiq do desc "GitLab | Check the configuration of Sidekiq" task check: :gitlab_environment do diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2f4b0e900c3..45fc072900a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1583,7 +1583,7 @@ msgstr "" msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications." msgstr "" -msgid "ClusterIntegration|RBAC-enabled cluster (experimental)" +msgid "ClusterIntegration|RBAC-enabled cluster" msgstr "" msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration." @@ -1655,9 +1655,6 @@ msgstr "" msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time." msgstr "" -msgid "ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application." -msgstr "" - msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below" msgstr "" @@ -4644,9 +4641,6 @@ msgstr "" msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible." msgstr "" -msgid "Profiles|%{author_name} made a private contribution" -msgstr "" - msgid "Profiles|Account scheduled for removal." msgstr "" @@ -4707,6 +4701,9 @@ msgstr "" msgid "Profiles|Invalid username" msgstr "" +msgid "Profiles|Made a private contribution" +msgstr "" + msgid "Profiles|Main settings" msgstr "" @@ -5850,6 +5847,9 @@ msgstr "" msgid "Status" msgstr "" +msgid "Stop environment" +msgstr "" + msgid "Stop impersonation" msgstr "" @@ -5859,6 +5859,9 @@ msgstr "" msgid "Stopped" msgstr "" +msgid "Stopping this environment is currently not possible as a deployment is in progress" +msgstr "" + msgid "Storage" msgstr "" @@ -6227,6 +6230,9 @@ msgstr "" msgid "This job is an out-of-date deployment to %{environmentLink}. View the most recent deployment %{deploymentLink}." msgstr "" +msgid "This job is archived. Only the complete pipeline can be retried." +msgstr "" + msgid "This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}." msgstr "" @@ -36,42 +36,40 @@ module QA ## # GitLab QA fabrication mechanisms # - module Factory - autoload :ApiFabricator, 'qa/factory/api_fabricator' - autoload :Base, 'qa/factory/base' - - module Resource - autoload :Sandbox, 'qa/factory/resource/sandbox' - autoload :Group, 'qa/factory/resource/group' - autoload :Issue, 'qa/factory/resource/issue' - autoload :Project, 'qa/factory/resource/project' - autoload :Label, 'qa/factory/resource/label' - autoload :MergeRequest, 'qa/factory/resource/merge_request' - autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github' - autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork' - autoload :DeployKey, 'qa/factory/resource/deploy_key' - autoload :DeployToken, 'qa/factory/resource/deploy_token' - autoload :Branch, 'qa/factory/resource/branch' - autoload :CiVariable, 'qa/factory/resource/ci_variable' - autoload :Runner, 'qa/factory/resource/runner' - autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' - autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster' - autoload :User, 'qa/factory/resource/user' - autoload :ProjectMilestone, 'qa/factory/resource/project_milestone' - autoload :Wiki, 'qa/factory/resource/wiki' - autoload :File, 'qa/factory/resource/file' - autoload :Fork, 'qa/factory/resource/fork' - autoload :SSHKey, 'qa/factory/resource/ssh_key' - end + module Resource + autoload :ApiFabricator, 'qa/resource/api_fabricator' + autoload :Base, 'qa/resource/base' + + autoload :Sandbox, 'qa/resource/sandbox' + autoload :Group, 'qa/resource/group' + autoload :Issue, 'qa/resource/issue' + autoload :Project, 'qa/resource/project' + autoload :Label, 'qa/resource/label' + autoload :MergeRequest, 'qa/resource/merge_request' + autoload :ProjectImportedFromGithub, 'qa/resource/project_imported_from_github' + autoload :MergeRequestFromFork, 'qa/resource/merge_request_from_fork' + autoload :DeployKey, 'qa/resource/deploy_key' + autoload :DeployToken, 'qa/resource/deploy_token' + autoload :Branch, 'qa/resource/branch' + autoload :CiVariable, 'qa/resource/ci_variable' + autoload :Runner, 'qa/resource/runner' + autoload :PersonalAccessToken, 'qa/resource/personal_access_token' + autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster' + autoload :User, 'qa/resource/user' + autoload :ProjectMilestone, 'qa/resource/project_milestone' + autoload :Wiki, 'qa/resource/wiki' + autoload :File, 'qa/resource/file' + autoload :Fork, 'qa/resource/fork' + autoload :SSHKey, 'qa/resource/ssh_key' module Repository - autoload :Push, 'qa/factory/repository/push' - autoload :ProjectPush, 'qa/factory/repository/project_push' - autoload :WikiPush, 'qa/factory/repository/wiki_push' + autoload :Push, 'qa/resource/repository/push' + autoload :ProjectPush, 'qa/resource/repository/project_push' + autoload :WikiPush, 'qa/resource/repository/wiki_push' end module Settings - autoload :HashedStorage, 'qa/factory/settings/hashed_storage' + autoload :HashedStorage, 'qa/resource/settings/hashed_storage' end end diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md deleted file mode 100644 index 42077f60611..00000000000 --- a/qa/qa/factory/README.md +++ /dev/null @@ -1,410 +0,0 @@ -# Factory objects in GitLab QA - -In GitLab QA we are using factories to create resources. - -Factories implementation are primarily done using Browser UI steps, but can also -be done via the API. - -## Why do we need that? - -We need factory objects because we need to reduce duplication when creating -resources for our QA tests. - -## How to properly implement a factory object? - -All factories should inherit from [`Factory::Base`](./base.rb). - -There is only one mandatory method to implement to define a factory. This is the -`#fabricate!` method, which is used to build a resource via the browser UI. -Note that you should only use [Page objects](../page/README.md) to interact with -a Web page in this method. - -Here is an imaginary example: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - def fabricate! - Page::Dashboard::Index.perform do |dashboard_index| - dashboard_index.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - end - end - end - end -end -``` - -### Define API implementation - -A factory may also implement the three following methods to be able to create a -resource via the public GitLab API: - -- `#api_get_path`: The `GET` path to fetch an existing resource. -- `#api_post_path`: The `POST` path to create a new resource. -- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource. - -Let's take the `Shirt` factory example, and add these three API methods: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - def fabricate! - # ... same as before - end - - def api_get_path - "/shirt/#{name}" - end - - def api_post_path - "/shirts" - end - - def api_post_body - { - name: name - } - end - end - end - end -end -``` - -The [`Project` factory](./resource/project.rb) is a good real example of Browser -UI and API implementations. - -#### Resource attributes - -A resource may need another resource to exist first. For instance, a project -needs a group to be created in. - -To define a resource attribute, you can use the `attribute` method with a -block using the other factory to fabricate the resource. - -That will allow access to the other resource from your resource object's -methods. You would usually use it in `#fabricate!`, `#api_get_path`, -`#api_post_path`, `#api_post_body`. - -Let's take the `Shirt` factory, and add a `project` attribute to it: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - end - - def api_get_path - "/project/#{project.path}/shirt/#{name}" - end - - def api_post_path - "/project/#{project.path}/shirts" - end - - def api_post_body - { - name: name - } - end - end - end - end -end -``` - -**Note that all the attributes are lazily constructed. This means if you want -a specific attribute to be fabricated first, you'll need to call the -attribute method first even if you're not using it.** - -#### Product data attributes - -Once created, you may want to populate a resource with attributes that can be -found in the Web page, or in the API response. -For instance, once you create a project, you may want to store its repository -SSH URL as an attribute. - -Again we could use the `attribute` method with a block, using a page object -to retrieve the data on the page. - -Let's take the `Shirt` factory, and define a `:brand` attribute: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - # Attribute populated from the Browser UI (using the block) - attribute :brand do - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page - end - end - - # ... same as before - end - end - end -end -``` - -**Note again that all the attributes are lazily constructed. This means if -you call `shirt.brand` after moving to the other page, it'll not properly -retrieve the data because we're no longer on the expected page.** - -Consider this: - -```ruby -shirt = - QA::Factory::Resource::Shirt.fabricate! do |resource| - resource.name = "GitLab QA" - end - -shirt.project.visit! - -shirt.brand # => FAIL! -``` - -The above example will fail because now we're on the project page, trying to -construct the brand data from the shirt page, however we moved to the project -page already. There are two ways to solve this, one is that we could try to -retrieve the brand before visiting the project again: - -```ruby -shirt = - QA::Factory::Resource::Shirt.fabricate! do |resource| - resource.name = "GitLab QA" - end - -shirt.brand # => OK! - -shirt.project.visit! - -shirt.brand # => OK! -``` - -The attribute will be stored in the instance therefore all the following calls -will be fine, using the data previously constructed. If we think that this -might be too brittle, we could eagerly construct the data right before -ending fabrication: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - # ... same as before - - def fabricate! - project.visit! - - Page::Project::Show.perform do |project_show| - project_show.go_to_new_shirt - end - - Page::Shirt::New.perform do |shirt_new| - shirt_new.set_name(name) - shirt_new.create_shirt! - end - - populate(:brand) # Eagerly construct the data - end - end - end - end -end -``` - -The `populate` method will iterate through its arguments and call each -attribute respectively. Here `populate(:brand)` has the same effect as -just `brand`. Using the populate method makes the intention clearer. - -With this, it will make sure we construct the data right after we create the -shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data. - -Alternatively, we could just make sure we're on the right page before -constructing the brand data: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-create-a-shirt' - end - end - - # Attribute populated from the Browser UI (using the block) - attribute :brand do - back_url = current_url - visit! - - Page::Shirt::Show.perform do |shirt_show| - shirt_show.fetch_brand_from_page - end - - visit(back_url) - end - - # ... same as before - end - end - end -end -``` - -This will make sure it's on the shirt page before constructing brand, and -move back to the previous page to avoid breaking the state. - -#### Define an attribute based on an API response - -Sometimes, you want to define a resource attribute based on the API response -from its `GET` or `POST` request. For instance, if the creation of a shirt via -the API returns - -```ruby -{ - brand: 'a-brand-new-brand', - style: 't-shirt', - materials: [[:cotton, 80], [:polyamide, 20]] -} -``` - -you may want to store `style` as-is in the resource, and fetch the first value -of the first `materials` item in a `main_fabric` attribute. - -Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric` -attributes: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - # ... same as before - - # Attribute from the Shirt factory if present, - # or fetched from the API response if present, - # or a QA::Factory::Base::NoValueError is raised otherwise - attribute :style - - # If the attribute from the Shirt factory is not present, - # and if the API does not contain this field, this block will be - # used to construct the value based on the API response. - attribute :main_fabric do - api_response.&dig(:materials, 0, 0) - end - - # ... same as before - end - end - end -end -``` - -**Notes on attributes precedence:** - -- factory instance variables have the highest precedence -- attributes from the API response take precedence over attributes from the - block (usually from Browser UI) -- attributes without a value will raise a `QA::Factory::Base::NoValueError` error - -## Creating resources in your tests - -To create a resource in your tests, you can call the `.fabricate!` method on the -factory class. -Note that if the factory supports API fabrication, this will use this -fabrication by default. - -Here is an example that will use the API fabrication method under the hood since -it's supported by the `Shirt` factory: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate! do |shirt| - shirt.name = 'my-shirt' -end - -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute -expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response -expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response -expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block -``` - -If you explicitly want to use the Browser UI fabrication method, you can call -the `.fabricate_via_browser_ui!` method instead: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt| - shirt.name = 'my-shirt' -end - -expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute -expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block -expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided -expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response) -``` - -You can also explicitly use the API fabrication method, by calling the -`.fabricate_via_api!` method: - -```ruby -my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt| - shirt.name = 'my-shirt' -end -``` - -In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`. - -## Where to ask for help? - -If you need more information, ask for help on `#quality` channel on Slack -(internal, GitLab Team only). - -If you are not a Team Member, and you still need help to contribute, please -open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb deleted file mode 100644 index b05d1e252ec..00000000000 --- a/qa/qa/factory/resource/branch.rb +++ /dev/null @@ -1,77 +0,0 @@ -module QA - module Factory - module Resource - class Branch < Factory::Base - attr_accessor :project, :branch_name, - :allow_to_push, :allow_to_merge, :protected - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'protected-branch-project' - end - end - - def initialize - @branch_name = 'test/branch' - @allow_to_push = true - @allow_to_merge = true - @protected = false - end - - def fabricate! - project.visit! - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'kick-off.txt' - resource.commit_message = 'First commit' - end - - branch = Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'README.md' - resource.commit_message = 'Add readme' - resource.branch_name = 'master' - resource.new_branch = false - resource.remote_branch = @branch_name - end - - Page::Project::Show.perform do |page| - page.wait { page.has_content?(branch_name) } - end - - # The upcoming process will make it access the Protected Branches page, - # select the already created branch and protect it according - # to `allow_to_push` variable. - return branch unless @protected - - Page::Project::Menu.perform(&:click_repository_settings) - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_protected_branches do |page| - page.select_branch(branch_name) - - if allow_to_push - page.allow_devs_and_maintainers_to_push - else - page.allow_no_one_to_push - end - - if allow_to_merge - page.allow_devs_and_maintainers_to_merge - else - page.allow_no_one_to_merge - end - - page.wait(reload: false) do - !page.first('.btn-success').disabled? - end - - page.protect_branch - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/ci_variable.rb b/qa/qa/factory/resource/ci_variable.rb deleted file mode 100644 index a0aefc61f9f..00000000000 --- a/qa/qa/factory/resource/ci_variable.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class CiVariable < Factory::Base - attr_accessor :key, :value - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-ci-variables' - resource.description = 'project for adding CI variable test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_ci_cd_settings) - - Page::Project::Settings::CICD.perform do |setting| - setting.expand_ci_variables do |page| - page.fill_variable(key, value) - - page.save_variables - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb deleted file mode 100644 index aea99c9f80d..00000000000 --- a/qa/qa/factory/resource/deploy_key.rb +++ /dev/null @@ -1,43 +0,0 @@ -module QA - module Factory - module Resource - class DeployKey < Factory::Base - attr_accessor :title, :key - - attribute :fingerprint do - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_keys do |key| - key_offset = key.key_titles.index do |key_title| - key_title.text == title - end - - key.key_fingerprints[key_offset].text - end - end - end - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-deploy' - resource.description = 'project for adding deploy key test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_repository_settings) - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_keys do |page| - page.fill_key_title(title) - page.fill_key_value(key) - - page.add_key - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb deleted file mode 100644 index 68e98f0aa01..00000000000 --- a/qa/qa/factory/resource/deploy_token.rb +++ /dev/null @@ -1,50 +0,0 @@ -module QA - module Factory - module Resource - class DeployToken < Factory::Base - attr_accessor :name, :expires_at - - attribute :username do - Page::Project::Settings::Repository.perform do |page| - page.expand_deploy_tokens do |token| - token.token_username - end - end - end - - attribute :password do - Page::Project::Settings::Repository.perform do |page| - page.expand_deploy_tokens do |token| - token.token_password - end - end - end - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-to-deploy' - resource.description = 'project for adding deploy token test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.act do - click_repository_settings - end - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_tokens do |page| - page.fill_token_name(name) - page.fill_token_expires_at(expires_at) - page.fill_scopes(read_repository: true, read_registry: false) - - page.add_token - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb deleted file mode 100644 index 1148876c2d3..00000000000 --- a/qa/qa/factory/resource/file.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class File < Factory::Base - attr_accessor :name, - :content, - :commit_message - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-new-file' - end - end - - def initialize - @name = 'QA Test - File name' - @content = 'QA Test - File content' - @commit_message = 'QA Test - Commit message' - end - - def fabricate! - project.visit! - - Page::Project::Show.perform(&:create_new_file!) - - Page::File::Form.perform do |page| - page.add_name(@name) - page.add_content(@content) - page.add_commit_message(@commit_message) - page.commit_changes - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb deleted file mode 100644 index d9bc44c9eb6..00000000000 --- a/qa/qa/factory/resource/fork.rb +++ /dev/null @@ -1,43 +0,0 @@ -module QA - module Factory - module Resource - class Fork < Factory::Base - attribute :push do - Factory::Repository::ProjectPush.fabricate! - end - - attribute :user do - Factory::Resource::User.fabricate! do |resource| - if Runtime::Env.forker? - resource.username = Runtime::Env.forker_username - resource.password = Runtime::Env.forker_password - end - end - end - - def fabricate! - populate(:push, :user) - - # Sign out as admin and sign is as the fork user - Page::Main::Menu.perform(&:sign_out) - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform do |login| - login.sign_in_using_credentials(user) - end - - push.project.visit! - - Page::Project::Show.perform(&:fork_project) - - Page::Project::Fork::New.perform do |fork_new| - fork_new.choose_namespace(user.name) - end - - Page::Layout::Banner.perform do |page| - page.has_notice?('The project was successfully forked.') - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb deleted file mode 100644 index 45e49da86f9..00000000000 --- a/qa/qa/factory/resource/group.rb +++ /dev/null @@ -1,68 +0,0 @@ -module QA - module Factory - module Resource - class Group < Factory::Base - attr_accessor :path, :description - - attribute :sandbox do - Factory::Resource::Sandbox.fabricate! - end - - attribute :id - - def initialize - @path = Runtime::Namespace.name - @description = "QA test run at #{Runtime::Namespace.time}" - end - - def fabricate! - sandbox.visit! - - Page::Group::Show.perform do |group_show| - if group_show.has_subgroup?(path) - group_show.go_to_subgroup(path) - else - group_show.go_to_new_subgroup - - Page::Group::New.perform do |group_new| - group_new.set_path(path) - group_new.set_description(description) - group_new.set_visibility('Public') - group_new.create - end - - # Ensure that the group was actually created - group_show.wait(time: 1) do - group_show.has_text?(path) && - group_show.has_new_project_or_subgroup_dropdown? - end - end - end - end - - def fabricate_via_api! - resource_web_url(api_get) - rescue ResourceNotFoundError - super - end - - def api_get_path - "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" - end - - def api_post_path - '/groups' - end - - def api_post_body - { - parent_id: sandbox.id, - path: path, - name: path, - visibility: 'public' - } - end - end - end - end -end diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb deleted file mode 100644 index 3a28e0d5aa6..00000000000 --- a/qa/qa/factory/resource/issue.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class Issue < Factory::Base - attr_writer :description - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-for-issues' - resource.description = 'project for adding issues' - end - end - - attribute :title - - def fabricate! - project.visit! - - Page::Project::Show.perform(&:go_to_new_issue) - - Page::Project::Issue::New.perform do |page| - page.add_title(@title) - page.add_description(@description) - page.create_new_issue - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb deleted file mode 100644 index aac6864f42f..00000000000 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class KubernetesCluster < Factory::Base - attr_writer :project, :cluster, - :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner - - attribute :ingress_ip do - Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) - end - - def fabricate! - @project.visit! - - Page::Project::Menu.perform( - &:click_operations_kubernetes) - - Page::Project::Operations::Kubernetes::Index.perform( - &:add_kubernetes_cluster) - - Page::Project::Operations::Kubernetes::Add.perform( - &:add_existing_cluster) - - Page::Project::Operations::Kubernetes::AddExisting.perform do |page| - page.set_cluster_name(@cluster.cluster_name) - page.set_api_url(@cluster.api_url) - page.set_ca_certificate(@cluster.ca_certificate) - page.set_token(@cluster.token) - page.check_rbac! if @cluster.rbac - page.add_cluster! - end - - if @install_helm_tiller - Page::Project::Operations::Kubernetes::Show.perform do |page| - # We must wait a few seconds for permissions to be set up correctly for new cluster - sleep 10 - - # Helm must be installed before everything else - page.install!(:helm) - page.await_installed(:helm) - - page.install!(:ingress) if @install_ingress - page.install!(:prometheus) if @install_prometheus - page.install!(:runner) if @install_runner - - page.await_installed(:ingress) if @install_ingress - page.await_installed(:prometheus) if @install_prometheus - page.await_installed(:runner) if @install_runner - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb deleted file mode 100644 index 32bc519b48c..00000000000 --- a/qa/qa/factory/resource/label.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Label < Factory::Base - attr_accessor :description, :color - - attribute :title - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-label' - end - end - - def initialize - @title = "qa-test-#{SecureRandom.hex(8)}" - @description = 'This is a test label' - @color = '#0033CC' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:go_to_labels) - Page::Label::Index.perform(&:go_to_new_label) - - Page::Label::New.perform do |page| - page.fill_title(@title) - page.fill_description(@description) - page.fill_color(@color) - page.create_label - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb deleted file mode 100644 index 4b7d2287f98..00000000000 --- a/qa/qa/factory/resource/merge_request.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class MergeRequest < Factory::Base - attr_accessor :title, - :description, - :source_branch, - :target_branch, - :assignee, - :milestone, - :labels - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-merge-request' - end - end - - attribute :target do - project.visit! - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.branch_name = 'master' - resource.remote_branch = target_branch - end - end - - attribute :source do - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.branch_name = target_branch - resource.remote_branch = source_branch - resource.new_branch = false - resource.file_name = "added_file.txt" - resource.file_content = "File Added" - end - end - - def initialize - @title = 'QA test - merge request' - @description = 'This is a test merge request' - @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" - @target_branch = "master" - @assignee = nil - @milestone = nil - @labels = [] - end - - def fabricate! - populate(:target, :source) - - project.visit! - Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform do |page| - page.fill_title(@title) - page.fill_description(@description) - page.choose_milestone(@milestone) if @milestone - labels.each do |label| - page.select_label(label) - end - - page.create_merge_request - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb deleted file mode 100644 index 1311bf625a6..00000000000 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ /dev/null @@ -1,31 +0,0 @@ -module QA - module Factory - module Resource - class MergeRequestFromFork < MergeRequest - attr_accessor :fork_branch - - attribute :fork do - Factory::Resource::Fork.fabricate! - end - - attribute :push do - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = fork - resource.branch_name = fork_branch - resource.file_name = 'file2.txt' - resource.user = fork.user - end - end - - def fabricate! - populate(:push) - - fork.visit! - - Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform(&:create_merge_request) - end - end - end - end -end diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb deleted file mode 100644 index ceb0f1c3d75..00000000000 --- a/qa/qa/factory/resource/personal_access_token.rb +++ /dev/null @@ -1,27 +0,0 @@ -module QA - module Factory - module Resource - ## - # Create a personal access token that can be used by the api - # - class PersonalAccessToken < Factory::Base - attr_accessor :name - - attribute :access_token do - Page::Profile::PersonalAccessTokens.perform(&:created_access_token) - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_profile_settings) - Page::Profile::Menu.perform(&:click_access_tokens) - - Page::Profile::PersonalAccessTokens.perform do |page| - page.fill_token_name(name || 'api-test-token') - page.check_api - page.create_token - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb deleted file mode 100644 index f691ae5a342..00000000000 --- a/qa/qa/factory/resource/project.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Project < Factory::Base - attribute :name - attribute :description - - attribute :group do - Factory::Resource::Group.fabricate! - end - - attribute :repository_ssh_location do - Page::Project::Show.perform do |page| - page.choose_repository_clone_ssh - page.repository_location - end - end - - attribute :repository_http_location do - Page::Project::Show.perform do |page| - page.choose_repository_clone_http - page.repository_location - end - end - - def initialize - @description = 'My awesome project' - end - - def name=(raw_name) - @name = "#{raw_name}-#{SecureRandom.hex(8)}" - end - - def fabricate! - group.visit! - - Page::Group::Show.perform(&:go_to_new_project) - - Page::Project::New.perform do |page| - page.choose_test_namespace - page.choose_name(@name) - page.add_description(@description) - page.set_visibility('Public') - page.create_new_project - end - end - - def api_get_path - "/projects/#{name}" - end - - def api_post_path - '/projects' - end - - def api_post_body - { - namespace_id: group.id, - path: name, - name: name, - description: description, - visibility: 'public' - } - end - - private - - def transform_api_resource(resource) - resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo]) - resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo]) - resource - end - end - end - end -end diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb deleted file mode 100644 index ce20641e6cc..00000000000 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class ProjectImportedFromGithub < Resource::Project - attr_accessor :name - attr_writer :personal_access_token, :github_repository_path - - attribute :group do - Factory::Resource::Group.fabricate! - end - - def fabricate! - group.visit! - - Page::Group::Show.perform(&:go_to_new_project) - - Page::Project::New.perform do |page| - page.go_to_import_project - end - - Page::Project::New.perform do |page| - page.go_to_github_import - end - - Page::Project::Import::Github.perform do |page| - page.add_personal_access_token(@personal_access_token) - page.list_repos - page.import!(@github_repository_path, @name) - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb deleted file mode 100644 index 383f534c12c..00000000000 --- a/qa/qa/factory/resource/project_milestone.rb +++ /dev/null @@ -1,36 +0,0 @@ -module QA - module Factory - module Resource - class ProjectMilestone < Factory::Base - attr_reader :title - attr_accessor :description - - attribute :project do - Factory::Resource::Project.fabricate! - end - - def title=(title) - @title = "#{title}-#{SecureRandom.hex(4)}" - @description = 'A milestone' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform do |page| - page.click_issues - page.click_milestones - end - - Page::Project::Milestone::Index.perform(&:click_new_milestone) - - Page::Project::Milestone::New.perform do |milestone_new| - milestone_new.set_title(@title) - milestone_new.set_description(@description) - milestone_new.create_new_milestone - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb deleted file mode 100644 index 7108db1e55a..00000000000 --- a/qa/qa/factory/resource/runner.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Runner < Factory::Base - attr_writer :name, :tags, :image - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-ci-cd' - resource.description = 'Project with CI/CD Pipelines' - end - end - - def name - @name || "qa-runner-#{SecureRandom.hex(4)}" - end - - def tags - @tags || %w[qa e2e] - end - - def image - @image || 'gitlab/gitlab-runner:alpine' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_ci_cd_settings) - - Service::Runner.new(name).tap do |runner| - Page::Project::Settings::CICD.perform do |settings| - settings.expand_runners_settings do |runners| - runner.pull - runner.token = runners.registration_token - runner.address = runners.coordinator_address - runner.tags = tags - runner.image = image - runner.register! - end - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb deleted file mode 100644 index a125bac65dd..00000000000 --- a/qa/qa/factory/resource/sandbox.rb +++ /dev/null @@ -1,60 +0,0 @@ -module QA - module Factory - module Resource - ## - # Ensure we're in our sandbox namespace, either by navigating to it or by - # creating it if it doesn't yet exist. - # - class Sandbox < Factory::Base - attr_reader :path - - attribute :id - - def initialize - @path = Runtime::Namespace.sandbox_name - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_groups) - - Page::Dashboard::Groups.perform do |page| - if page.has_group?(path) - page.go_to_group(path) - else - page.go_to_new_group - - Page::Group::New.perform do |group| - group.set_path(path) - group.set_description('GitLab QA Sandbox Group') - group.set_visibility('Public') - group.create - end - end - end - end - - def fabricate_via_api! - resource_web_url(api_get) - rescue ResourceNotFoundError - super - end - - def api_get_path - "/groups/#{path}" - end - - def api_post_path - '/groups' - end - - def api_post_body - { - path: path, - name: path, - visibility: 'public' - } - end - end - end - end -end diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb deleted file mode 100644 index 6f952eda36f..00000000000 --- a/qa/qa/factory/resource/ssh_key.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class SSHKey < Factory::Base - extend Forwardable - - attr_accessor :title - - def_delegators :key, :private_key, :public_key, :fingerprint - - def key - @key ||= Runtime::Key::RSA.new - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_profile_settings) - Page::Profile::Menu.perform(&:click_ssh_keys) - - Page::Profile::SSHKeys.perform do |page| - page.add_key(public_key, title) - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb deleted file mode 100644 index 68faadddd1c..00000000000 --- a/qa/qa/factory/resource/user.rb +++ /dev/null @@ -1,92 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class User < Factory::Base - attr_reader :unique_id - attr_writer :username, :password - - def initialize - @unique_id = SecureRandom.hex(8) - end - - def username - @username ||= "qa-user-#{unique_id}" - end - - def password - @password ||= 'password' - end - - def name - @name ||= username - end - - def email - @email ||= "#{username}@example.com" - end - - def credentials_given? - defined?(@username) && defined?(@password) - end - - def fabricate! - # Don't try to log-out if we're not logged-in - if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } - Page::Main::Menu.perform { |main| main.sign_out } - end - - if credentials_given? - Page::Main::Login.perform do |login| - login.sign_in_using_credentials(self) - end - else - Page::Main::Login.perform do |login| - login.switch_to_register_tab - end - Page::Main::SignUp.perform do |signup| - signup.sign_up!(self) - end - end - end - - def fabricate_via_api! - resource_web_url(api_get) - rescue ResourceNotFoundError - super - end - - def api_get_path - "/users/#{fetch_id(username)}" - end - - def api_post_path - '/users' - end - - def api_post_body - { - email: email, - password: password, - username: username, - name: name, - skip_confirmation: true - } - end - - private - - def fetch_id(username) - users = parse_body(api_get_from("/users?username=#{username}")) - - unless users.size == 1 && users.first[:username] == username - raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`." - end - - users.first[:id] - end - end - end - end -end diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb deleted file mode 100644 index 769f394e85c..00000000000 --- a/qa/qa/factory/resource/wiki.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class Wiki < Factory::Base - attr_accessor :title, :content, :message - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-for-wikis' - resource.description = 'project for adding wikis' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform { |menu_side| menu_side.click_wiki } - - Page::Project::Wiki::New.perform do |wiki_new| - wiki_new.go_to_create_first_page - wiki_new.set_title(@title) - wiki_new.set_content(@content) - wiki_new.set_message(@message) - wiki_new.create_new_page - end - end - end - end - end -end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 94b9486b0d5..97ffe0e5716 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -65,7 +65,7 @@ module QA end def sign_in_using_admin_credentials - admin = QA::Factory::Resource::User.new.tap do |user| + admin = QA::Resource::User.new.tap do |user| user.username = QA::Runtime::User.admin_username user.password = QA::Runtime::User.admin_password end diff --git a/qa/qa/resource/README.md b/qa/qa/resource/README.md new file mode 100644 index 00000000000..4cdeb3f42a2 --- /dev/null +++ b/qa/qa/resource/README.md @@ -0,0 +1,392 @@ +# Resource class in GitLab QA + +Resources are primarily created using Browser UI steps, but can also +be created via the API. + +## How to properly implement a resource class? + +All resource classes should inherit from [`Resource::Base`](./base.rb). + +There is only one mandatory method to implement to define a resource class. +This is the `#fabricate!` method, which is used to build the resource via the +browser UI. Note that you should only use [Page objects](../page/README.md) to +interact with a Web page in this method. + +Here is an imaginary example: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + def fabricate! + Page::Dashboard::Index.perform do |dashboard_index| + dashboard_index.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + end + end +end +``` + +### Define API implementation + +A resource class may also implement the three following methods to be able to +create the resource via the public GitLab API: + +- `#api_get_path`: The `GET` path to fetch an existing resource. +- `#api_post_path`: The `POST` path to create a new resource. +- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource. + +Let's take the `Shirt` resource class, and add these three API methods: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + def fabricate! + # ... same as before + end + + def api_get_path + "/shirt/#{name}" + end + + def api_post_path + "/shirts" + end + + def api_post_body + { + name: name + } + end + end + end +end +``` + +The [`Project` resource](./project.rb) is a good real example of Browser +UI and API implementations. + +#### Resource attributes + +A resource may need another resource to exist first. For instance, a project +needs a group to be created in. + +To define a resource attribute, you can use the `attribute` method with a +block using the other resource class to fabricate the resource. + +That will allow access to the other resource from your resource object's +methods. You would usually use it in `#fabricate!`, `#api_get_path`, +`#api_post_path`, `#api_post_body`. + +Let's take the `Shirt` resource class, and add a `project` attribute to it: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + + def api_get_path + "/project/#{project.path}/shirt/#{name}" + end + + def api_post_path + "/project/#{project.path}/shirts" + end + + def api_post_body + { + name: name + } + end + end + end +end +``` + +**Note that all the attributes are lazily constructed. This means if you want +a specific attribute to be fabricated first, you'll need to call the +attribute method first even if you're not using it.** + +#### Product data attributes + +Once created, you may want to populate a resource with attributes that can be +found in the Web page, or in the API response. +For instance, once you create a project, you may want to store its repository +SSH URL as an attribute. + +Again we could use the `attribute` method with a block, using a page object +to retrieve the data on the page. + +Let's take the `Shirt` resource class, and define a `:brand` attribute: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + # Attribute populated from the Browser UI (using the block) + attribute :brand do + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + end + + # ... same as before + end + end +end +``` + +**Note again that all the attributes are lazily constructed. This means if +you call `shirt.brand` after moving to the other page, it'll not properly +retrieve the data because we're no longer on the expected page.** + +Consider this: + +```ruby +shirt = + QA::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.project.visit! + +shirt.brand # => FAIL! +``` + +The above example will fail because now we're on the project page, trying to +construct the brand data from the shirt page, however we moved to the project +page already. There are two ways to solve this, one is that we could try to +retrieve the brand before visiting the project again: + +```ruby +shirt = + QA::Resource::Shirt.fabricate! do |resource| + resource.name = "GitLab QA" + end + +shirt.brand # => OK! + +shirt.project.visit! + +shirt.brand # => OK! +``` + +The attribute will be stored in the instance therefore all the following calls +will be fine, using the data previously constructed. If we think that this +might be too brittle, we could eagerly construct the data right before +ending fabrication: + +```ruby +module QA + module Resource + class Shirt < Base + # ... same as before + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + + populate(:brand) # Eagerly construct the data + end + end + end +end +``` + +The `populate` method will iterate through its arguments and call each +attribute respectively. Here `populate(:brand)` has the same effect as +just `brand`. Using the populate method makes the intention clearer. + +With this, it will make sure we construct the data right after we create the +shirt. The drawback is that this will always construct the data when the +resource is fabricated even if we don't need to use the data. + +Alternatively, we could just make sure we're on the right page before +constructing the brand data: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + end + + # Attribute populated from the Browser UI (using the block) + attribute :brand do + back_url = current_url + visit! + + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + + visit(back_url) + end + + # ... same as before + end + end +end +``` + +This will make sure it's on the shirt page before constructing brand, and +move back to the previous page to avoid breaking the state. + +#### Define an attribute based on an API response + +Sometimes, you want to define a resource attribute based on the API response +from its `GET` or `POST` request. For instance, if the creation of a shirt via +the API returns + +```ruby +{ + brand: 'a-brand-new-brand', + style: 't-shirt', + materials: [[:cotton, 80], [:polyamide, 20]] +} +``` + +you may want to store `style` as-is in the resource, and fetch the first value +of the first `materials` item in a `main_fabric` attribute. + +Let's take the `Shirt` resource class, and define a `:style` and a +`:main_fabric` attributes: + +```ruby +module QA + module Resource + class Shirt < Base + # ... same as before + + # @style from the instance if present, + # or fetched from the API response if present, + # or a QA::Resource::Base::NoValueError is raised otherwise + attribute :style + + # If @main_fabric is not present, + # and if the API does not contain this field, this block will be + # used to construct the value based on the API response, and + # store the result in @main_fabric + attribute :main_fabric do + api_response.&dig(:materials, 0, 0) + end + + # ... same as before + end + end +end +``` + +**Notes on attributes precedence:** + +- resource instance variables have the highest precedence +- attributes from the API response take precedence over attributes from the + block (usually from Browser UI) +- attributes without a value will raise a `QA::Resource::Base::NoValueError` error + +## Creating resources in your tests + +To create a resource in your tests, you can call the `.fabricate!` method on +the resource class. +Note that if the resource class supports API fabrication, this will use this +fabrication by default. + +Here is an example that will use the API fabrication method under the hood +since it's supported by the `Shirt` resource class: + +```ruby +my_shirt = Resource::Shirt.fabricate! do |shirt| + shirt.name = 'my-shirt' +end + +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable +expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response +expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response +expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block +``` + +If you explicitly want to use the Browser UI fabrication method, you can call +the `.fabricate_via_browser_ui!` method instead: + +```ruby +my_shirt = Resource::Shirt.fabricate_via_browser_ui! do |shirt| + shirt.name = 'my-shirt' +end + +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable +expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block +expect(page).to have_text(my_shirt.style) # => QA::Resource::Base::NoValueError will be raised because no API response nor a block is provided +expect(page).to have_text(my_shirt.main_fabric) # => QA::Resource::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response) +``` + +You can also explicitly use the API fabrication method, by calling the +`.fabricate_via_api!` method: + +```ruby +my_shirt = Resource::Shirt.fabricate_via_api! do |shirt| + shirt.name = 'my-shirt' +end +``` + +In this case, the result will be similar to calling +`Resource::Shirt.fabricate!`. + +## Where to ask for help? + +If you need more information, ask for help on `#quality` channel on Slack +(internal, GitLab Team only). + +If you are not a Team Member, and you still need help to contribute, please +open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/factory/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index 887150cadf1..3762a94f312 100644 --- a/qa/qa/factory/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/object/deep_dup' require 'capybara/dsl' module QA - module Factory + module Resource module ApiFabricator include Airborne include Capybara::DSL @@ -27,7 +27,7 @@ module QA def fabricate_via_api! unless api_support? - raise NotImplementedError, "Factory #{self.class.name} does not support fabrication via the API!" + raise NotImplementedError, "Resource #{self.class.name} does not support fabrication via the API!" end resource_web_url(api_post) @@ -93,8 +93,8 @@ module QA self.api_resource = transform_api_resource(parsed_response.deep_dup) end - def transform_api_resource(resource) - resource + def transform_api_resource(api_resource) + api_resource end end end diff --git a/qa/qa/factory/base.rb b/qa/qa/resource/base.rb index 75438b77bf3..f3eefb70520 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/resource/base.rb @@ -4,7 +4,7 @@ require 'forwardable' require 'capybara/dsl' module QA - module Factory + module Resource class Base extend SingleForwardable include ApiFabricator @@ -58,11 +58,11 @@ module QA def self.fabricate_via_browser_ui!(*args, &prepare_block) options = args.extract_options! - factory = options.fetch(:factory) { new } + resource = options.fetch(:resource) { new } parents = options.fetch(:parents) { [] } - do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do - log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) } + do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do + log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) } current_url end @@ -70,29 +70,29 @@ module QA def self.fabricate_via_api!(*args, &prepare_block) options = args.extract_options! - factory = options.fetch(:factory) { new } + resource = options.fetch(:resource) { new } parents = options.fetch(:parents) { [] } - raise NotImplementedError unless factory.api_support? + raise NotImplementedError unless resource.api_support? - factory.eager_load_api_client! + resource.eager_load_api_client! - do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do - log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! } + do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do + log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! } end end - def self.do_fabricate!(factory:, prepare_block:, parents: []) - prepare_block.call(factory) if prepare_block + def self.do_fabricate!(resource:, prepare_block:, parents: []) + prepare_block.call(resource) if prepare_block resource_web_url = yield - factory.web_url = resource_web_url + resource.web_url = resource_web_url - factory + resource end private_class_method :do_fabricate! - def self.log_fabrication(method, factory, parents, args) + def self.log_fabrication(method, resource, parents, args) return yield unless Runtime::Env.debug? start = Time.now @@ -111,7 +111,7 @@ module QA private_class_method :log_fabrication def self.evaluator - @evaluator ||= Factory::Base::DSL.new(self) + @evaluator ||= Base::DSL.new(self) end private_class_method :evaluator diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/branch.rb new file mode 100644 index 00000000000..bd52c4abe02 --- /dev/null +++ b/qa/qa/resource/branch.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module QA + module Resource + class Branch < Base + attr_accessor :project, :branch_name, + :allow_to_push, :allow_to_merge, :protected + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'protected-branch-project' + end + end + + def initialize + @branch_name = 'test/branch' + @allow_to_push = true + @allow_to_merge = true + @protected = false + end + + def fabricate! + project.visit! + + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'kick-off.txt' + resource.commit_message = 'First commit' + end + + branch = Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'README.md' + resource.commit_message = 'Add readme' + resource.branch_name = 'master' + resource.new_branch = false + resource.remote_branch = @branch_name + end + + Page::Project::Show.perform do |page| + page.wait { page.has_content?(branch_name) } + end + + # The upcoming process will make it access the Protected Branches page, + # select the already created branch and protect it according + # to `allow_to_push` variable. + return branch unless @protected + + Page::Project::Menu.perform(&:click_repository_settings) + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_protected_branches do |page| + page.select_branch(branch_name) + + if allow_to_push + page.allow_devs_and_maintainers_to_push + else + page.allow_no_one_to_push + end + + if allow_to_merge + page.allow_devs_and_maintainers_to_merge + else + page.allow_no_one_to_merge + end + + page.wait(reload: false) do + !page.first('.btn-success').disabled? + end + + page.protect_branch + end + end + end + end + end +end diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb new file mode 100644 index 00000000000..0570c47d41c --- /dev/null +++ b/qa/qa/resource/ci_variable.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class CiVariable < Base + attr_accessor :key, :value + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-ci-variables' + resource.description = 'project for adding CI variable test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Page::Project::Settings::CICD.perform do |setting| + setting.expand_ci_variables do |page| + page.fill_variable(key, value) + + page.save_variables + end + end + end + end + end +end diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb new file mode 100644 index 00000000000..9ed8fb7726e --- /dev/null +++ b/qa/qa/resource/deploy_key.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Resource + class DeployKey < Base + attr_accessor :title, :key + + attribute :fingerprint do + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |key| + key_offset = key.key_titles.index do |key_title| + key_title.text == title + end + + key.key_fingerprints[key_offset].text + end + end + end + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy key test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_repository_settings) + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |page| + page.fill_key_title(title) + page.fill_key_value(key) + + page.add_key + end + end + end + end + end +end diff --git a/qa/qa/resource/deploy_token.rb b/qa/qa/resource/deploy_token.rb new file mode 100644 index 00000000000..cee4422f6b4 --- /dev/null +++ b/qa/qa/resource/deploy_token.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module QA + module Resource + class DeployToken < Base + attr_accessor :name, :expires_at + + attribute :username do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| + token.token_username + end + end + end + + attribute :password do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| + token.token_password + end + end + end + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy token test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.act do + click_repository_settings + end + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_tokens do |page| + page.fill_token_name(name) + page.fill_token_expires_at(expires_at) + page.fill_scopes(read_repository: true, read_registry: false) + + page.add_token + end + end + end + end + end +end diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb new file mode 100644 index 00000000000..effc5a7940b --- /dev/null +++ b/qa/qa/resource/file.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + class File < Base + attr_accessor :name, + :content, + :commit_message + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-new-file' + end + end + + def initialize + @name = 'QA Test - File name' + @content = 'QA Test - File content' + @commit_message = 'QA Test - Commit message' + end + + def fabricate! + project.visit! + + Page::Project::Show.perform(&:create_new_file!) + + Page::File::Form.perform do |page| + page.add_name(@name) + page.add_content(@content) + page.add_commit_message(@commit_message) + page.commit_changes + end + end + end + end +end diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb new file mode 100644 index 00000000000..9fd66f3a36a --- /dev/null +++ b/qa/qa/resource/fork.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Resource + class Fork < Base + attribute :push do + Repository::ProjectPush.fabricate! + end + + attribute :user do + User.fabricate! do |resource| + if Runtime::Env.forker? + resource.username = Runtime::Env.forker_username + resource.password = Runtime::Env.forker_password + end + end + end + + def fabricate! + populate(:push, :user) + + # Sign out as admin and sign is as the fork user + Page::Main::Menu.perform(&:sign_out) + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(user) + end + + push.project.visit! + + Page::Project::Show.perform(&:fork_project) + + Page::Project::Fork::New.perform do |fork_new| + fork_new.choose_namespace(user.name) + end + + Page::Layout::Banner.perform do |page| + page.has_notice?('The project was successfully forked.') + end + end + end + end +end diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb new file mode 100644 index 00000000000..dce15e4f10b --- /dev/null +++ b/qa/qa/resource/group.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module QA + module Resource + class Group < Base + attr_accessor :path, :description + + attribute :sandbox do + Sandbox.fabricate! + end + + attribute :id + + def initialize + @path = Runtime::Namespace.name + @description = "QA test run at #{Runtime::Namespace.time}" + end + + def fabricate! + sandbox.visit! + + Page::Group::Show.perform do |group_show| + if group_show.has_subgroup?(path) + group_show.go_to_subgroup(path) + else + group_show.go_to_new_subgroup + + Page::Group::New.perform do |group_new| + group_new.set_path(path) + group_new.set_description(description) + group_new.set_visibility('Public') + group_new.create + end + + # Ensure that the group was actually created + group_show.wait(time: 1) do + group_show.has_text?(path) && + group_show.has_new_project_or_subgroup_dropdown? + end + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" + end + + def api_post_path + '/groups' + end + + def api_post_body + { + parent_id: sandbox.id, + path: path, + name: path, + visibility: 'public' + } + end + end + end +end diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb new file mode 100644 index 00000000000..2c2f27fe231 --- /dev/null +++ b/qa/qa/resource/issue.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class Issue < Base + attr_writer :description + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-for-issues' + resource.description = 'project for adding issues' + end + end + + attribute :title + + def fabricate! + project.visit! + + Page::Project::Show.perform(&:go_to_new_issue) + + Page::Project::Issue::New.perform do |page| + page.add_title(@title) + page.add_description(@description) + page.create_new_issue + end + end + end + end +end diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb new file mode 100644 index 00000000000..96c8843fb99 --- /dev/null +++ b/qa/qa/resource/kubernetes_cluster.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class KubernetesCluster < Base + attr_writer :project, :cluster, + :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner + + attribute :ingress_ip do + Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) + end + + def fabricate! + @project.visit! + + Page::Project::Menu.perform( + &:click_operations_kubernetes) + + Page::Project::Operations::Kubernetes::Index.perform( + &:add_kubernetes_cluster) + + Page::Project::Operations::Kubernetes::Add.perform( + &:add_existing_cluster) + + Page::Project::Operations::Kubernetes::AddExisting.perform do |page| + page.set_cluster_name(@cluster.cluster_name) + page.set_api_url(@cluster.api_url) + page.set_ca_certificate(@cluster.ca_certificate) + page.set_token(@cluster.token) + page.check_rbac! if @cluster.rbac + page.add_cluster! + end + + if @install_helm_tiller + Page::Project::Operations::Kubernetes::Show.perform do |page| + # We must wait a few seconds for permissions to be set up correctly for new cluster + sleep 10 + + # Helm must be installed before everything else + page.install!(:helm) + page.await_installed(:helm) + + page.install!(:ingress) if @install_ingress + page.install!(:prometheus) if @install_prometheus + page.install!(:runner) if @install_runner + + page.await_installed(:ingress) if @install_ingress + page.await_installed(:prometheus) if @install_prometheus + page.await_installed(:runner) if @install_runner + end + end + end + end + end +end diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb new file mode 100644 index 00000000000..c0869cb1f2a --- /dev/null +++ b/qa/qa/resource/label.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Label < Base + attr_accessor :description, :color + + attribute :title + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-label' + end + end + + def initialize + @title = "qa-test-#{SecureRandom.hex(8)}" + @description = 'This is a test label' + @color = '#0033CC' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:go_to_labels) + Page::Label::Index.perform(&:go_to_new_label) + + Page::Label::New.perform do |page| + page.fill_title(@title) + page.fill_description(@description) + page.fill_color(@color) + page.create_label + end + end + end + end +end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb new file mode 100644 index 00000000000..466a7942dc6 --- /dev/null +++ b/qa/qa/resource/merge_request.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class MergeRequest < Base + attr_accessor :title, + :description, + :source_branch, + :target_branch, + :assignee, + :milestone, + :labels + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-merge-request' + end + end + + attribute :target do + project.visit! + + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = 'master' + resource.remote_branch = target_branch + end + end + + attribute :source do + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = target_branch + resource.remote_branch = source_branch + resource.new_branch = false + resource.file_name = "added_file.txt" + resource.file_content = "File Added" + end + end + + def initialize + @title = 'QA test - merge request' + @description = 'This is a test merge request' + @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" + @target_branch = "master" + @assignee = nil + @milestone = nil + @labels = [] + end + + def fabricate! + populate(:target, :source) + + project.visit! + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform do |page| + page.fill_title(@title) + page.fill_description(@description) + page.choose_milestone(@milestone) if @milestone + labels.each do |label| + page.select_label(label) + end + + page.create_merge_request + end + end + end + end +end diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb new file mode 100644 index 00000000000..f91ae299d76 --- /dev/null +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + module Resource + class MergeRequestFromFork < MergeRequest + attr_accessor :fork_branch + + attribute :fork do + Fork.fabricate! + end + + attribute :push do + Repository::ProjectPush.fabricate! do |resource| + resource.project = fork + resource.branch_name = fork_branch + resource.file_name = 'file2.txt' + resource.user = fork.user + end + end + + def fabricate! + populate(:push) + + fork.visit! + + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform(&:create_merge_request) + end + end + end +end diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb new file mode 100644 index 00000000000..b8dd0a3562f --- /dev/null +++ b/qa/qa/resource/personal_access_token.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Resource + ## + # Create a personal access token that can be used by the api + # + class PersonalAccessToken < Base + attr_accessor :name + + attribute :access_token do + Page::Profile::PersonalAccessTokens.perform(&:created_access_token) + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_access_tokens) + + Page::Profile::PersonalAccessTokens.perform do |page| + page.fill_token_name(name || 'api-test-token') + page.check_api + page.create_token + end + end + end + end +end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb new file mode 100644 index 00000000000..7fdf69278f9 --- /dev/null +++ b/qa/qa/resource/project.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Project < Base + attribute :name + attribute :description + + attribute :group do + Group.fabricate! + end + + attribute :repository_ssh_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_ssh + page.repository_location + end + end + + attribute :repository_http_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_http + page.repository_location + end + end + + def initialize + @description = 'My awesome project' + end + + def name=(raw_name) + @name = "#{raw_name}-#{SecureRandom.hex(8)}" + end + + def fabricate! + group.visit! + + Page::Group::Show.perform(&:go_to_new_project) + + Page::Project::New.perform do |page| + page.choose_test_namespace + page.choose_name(@name) + page.add_description(@description) + page.set_visibility('Public') + page.create_new_project + end + end + + def api_get_path + "/projects/#{name}" + end + + def api_post_path + '/projects' + end + + def api_post_body + { + namespace_id: group.id, + path: name, + name: name, + description: description, + visibility: 'public' + } + end + + private + + def transform_api_resource(api_resource) + api_resource[:repository_ssh_location] = + Git::Location.new(api_resource[:ssh_url_to_repo]) + api_resource[:repository_http_location] = + Git::Location.new(api_resource[:http_url_to_repo]) + api_resource + end + end + end +end diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb new file mode 100644 index 00000000000..3f02fe885a9 --- /dev/null +++ b/qa/qa/resource/project_imported_from_github.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class ProjectImportedFromGithub < Project + attr_accessor :name + attr_writer :personal_access_token, :github_repository_path + + attribute :group do + Group.fabricate! + end + + def fabricate! + group.visit! + + Page::Group::Show.perform(&:go_to_new_project) + + Page::Project::New.perform do |page| + page.go_to_import_project + end + + Page::Project::New.perform do |page| + page.go_to_github_import + end + + Page::Project::Import::Github.perform do |page| + page.add_personal_access_token(@personal_access_token) + page.list_repos + page.import!(@github_repository_path, @name) + end + end + end + end +end diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb new file mode 100644 index 00000000000..a4d6657caff --- /dev/null +++ b/qa/qa/resource/project_milestone.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + class ProjectMilestone < Base + attr_reader :title + attr_accessor :description + + attribute :project do + Project.fabricate! + end + + def title=(title) + @title = "#{title}-#{SecureRandom.hex(4)}" + @description = 'A milestone' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform do |page| + page.click_issues + page.click_milestones + end + + Page::Project::Milestone::Index.perform(&:click_new_milestone) + + Page::Project::Milestone::New.perform do |milestone_new| + milestone_new.set_title(@title) + milestone_new.set_description(@description) + milestone_new.create_new_milestone + end + end + end + end +end diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb index 272b7fc5818..c9fafe3419f 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/resource/repository/project_push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Repository - class ProjectPush < Factory::Repository::Push + class ProjectPush < Repository::Push attribute :project do - Factory::Resource::Project.fabricate! do |resource| + Project.fabricate! do |resource| resource.name = 'project-with-code' resource.description = 'Project with repository' end diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/resource/repository/push.rb index ffa755b9e88..c14d97ff7fb 100644 --- a/qa/qa/factory/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'pathname' module QA - module Factory + module Resource module Repository - class Push < Factory::Base + class Push < Base attr_accessor :file_name, :file_content, :commit_message, :branch_name, :new_branch, :output, :repository_http_uri, :repository_ssh_uri, :ssh_key, :user diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb index 25b6ffe8323..f1c39d507fe 100644 --- a/qa/qa/factory/repository/wiki_push.rb +++ b/qa/qa/resource/repository/wiki_push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Repository - class WikiPush < Factory::Repository::Push + class WikiPush < Repository::Push attribute :wiki do - Factory::Resource::Wiki.fabricate! do |resource| + Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' resource.message = 'Update home' diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb new file mode 100644 index 00000000000..08ae3f22117 --- /dev/null +++ b/qa/qa/resource/runner.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Runner < Base + attr_writer :name, :tags, :image + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-ci-cd' + resource.description = 'Project with CI/CD Pipelines' + end + end + + def name + @name || "qa-runner-#{SecureRandom.hex(4)}" + end + + def tags + @tags || %w[qa e2e] + end + + def image + @image || 'gitlab/gitlab-runner:alpine' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Service::Runner.new(name).tap do |runner| + Page::Project::Settings::CICD.perform do |settings| + settings.expand_runners_settings do |runners| + runner.pull + runner.token = runners.registration_token + runner.address = runners.coordinator_address + runner.tags = tags + runner.image = image + runner.register! + end + end + end + end + end + end +end diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb new file mode 100644 index 00000000000..41ce857a8b8 --- /dev/null +++ b/qa/qa/resource/sandbox.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module QA + module Resource + ## + # Ensure we're in our sandbox namespace, either by navigating to it or by + # creating it if it doesn't yet exist. + # + class Sandbox < Base + attr_reader :path + + attribute :id + + def initialize + @path = Runtime::Namespace.sandbox_name + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_groups) + + Page::Dashboard::Groups.perform do |page| + if page.has_group?(path) + page.go_to_group(path) + else + page.go_to_new_group + + Page::Group::New.perform do |group| + group.set_path(path) + group.set_description('GitLab QA Sandbox Group') + group.set_visibility('Public') + group.create + end + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/groups/#{path}" + end + + def api_post_path + '/groups' + end + + def api_post_body + { + path: path, + name: path, + visibility: 'public' + } + end + end + end +end diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/resource/settings/hashed_storage.rb index 4e32382f910..40c06768ffe 100644 --- a/qa/qa/factory/settings/hashed_storage.rb +++ b/qa/qa/resource/settings/hashed_storage.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Settings - class HashedStorage < Factory::Base + class HashedStorage < Base def fabricate!(*traits) raise ArgumentError unless traits.include?(:enabled) diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb new file mode 100644 index 00000000000..c6c97c8532f --- /dev/null +++ b/qa/qa/resource/ssh_key.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + module Resource + class SSHKey < Base + extend Forwardable + + attr_accessor :title + + def_delegators :key, :private_key, :public_key, :fingerprint + + def key + @key ||= Runtime::Key::RSA.new + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_ssh_keys) + + Page::Profile::SSHKeys.perform do |page| + page.add_key(public_key, title) + end + end + end + end +end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb new file mode 100644 index 00000000000..16f0b311fa9 --- /dev/null +++ b/qa/qa/resource/user.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class User < Base + attr_reader :unique_id + attr_writer :username, :password + + def initialize + @unique_id = SecureRandom.hex(8) + end + + def username + @username ||= "qa-user-#{unique_id}" + end + + def password + @password ||= 'password' + end + + def name + @name ||= username + end + + def email + @email ||= "#{username}@example.com" + end + + def credentials_given? + defined?(@username) && defined?(@password) + end + + def fabricate! + # Don't try to log-out if we're not logged-in + if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } + Page::Main::Menu.perform { |main| main.sign_out } + end + + if credentials_given? + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(self) + end + else + Page::Main::Login.perform do |login| + login.switch_to_register_tab + end + Page::Main::SignUp.perform do |signup| + signup.sign_up!(self) + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/users/#{fetch_id(username)}" + end + + def api_post_path + '/users' + end + + def api_post_body + { + email: email, + password: password, + username: username, + name: name, + skip_confirmation: true + } + end + + private + + def fetch_id(username) + users = parse_body(api_get_from("/users?username=#{username}")) + + unless users.size == 1 && users.first[:username] == username + raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`." + end + + users.first[:id] + end + end + end +end diff --git a/qa/qa/resource/wiki.rb b/qa/qa/resource/wiki.rb new file mode 100644 index 00000000000..e942e9718a0 --- /dev/null +++ b/qa/qa/resource/wiki.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class Wiki < Base + attr_accessor :title, :content, :message + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-for-wikis' + resource.description = 'project for adding wikis' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform { |menu_side| menu_side.click_wiki } + + Page::Project::Wiki::New.perform do |wiki_new| + wiki_new.go_to_create_first_page + wiki_new.set_title(@title) + wiki_new.set_content(@content) + wiki_new.set_message(@message) + wiki_new.create_new_page + end + end + end + end +end diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index 0545b500e4c..aff84c89f0e 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -32,7 +32,7 @@ module QA def do_create_personal_access_token Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::PersonalAccessToken.fabricate!.access_token + Resource::PersonalAccessToken.fabricate!.access_token end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 4f960ee26a9..185837edacf 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -5,7 +5,7 @@ module QA it 'user registers and logs in' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Factory::Resource::User.fabricate_via_browser_ui! + Resource::User.fabricate_via_browser_ui! # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb index 2cd5bf01c1f..bef89d5be24 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -7,9 +7,9 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - user = Factory::Resource::User.fabricate! + user = Resource::User.fabricate! - project = Factory::Resource::Project.fabricate! do |resource| + project = Resource::Project.fabricate! do |resource| resource.name = 'add-member-project' end project.visit! diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index a242f2158da..6632c2977ef 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project| + created_project = Resource::Project.fabricate_via_browser_ui! do |project| project.name = 'awesome-project' project.description = 'create awesome project test' end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb index a99b0522e73..3ce48de2c25 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb @@ -4,7 +4,7 @@ module QA context 'Manage', :orchestrated, :github do describe 'Project import from GitHub' do let(:imported_project) do - Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| + Resource::ProjectImportedFromGithub.fabricate! do |project| project.name = 'imported-project' project.personal_access_token = Runtime::Env.github_access_token project.github_repository_path = 'gitlab-qa/test-project' diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb index 768d40f3acf..275de3d332c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' push.file_content = '# This is a test project' push.commit_message = 'Add README.md' diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index e67561b3a39..f5002c8032f 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate! do |issue| issue.title = issue_title end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index 24877d937d2..83603f1cda7 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate! do |issue| issue.title = issue_title end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index 037ff5efbd4..d33947f41da 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -7,22 +7,22 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - current_project = Factory::Resource::Project.fabricate! do |project| + current_project = Resource::Project.fabricate! do |project| project.name = 'project-with-merge-request-and-milestone' end - current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone| + current_milestone = Resource::ProjectMilestone.fabricate! do |milestone| milestone.title = 'unique-milestone' milestone.project = current_project end - new_label = Factory::Resource::Label.fabricate! do |label| + new_label = Resource::Label.fabricate! do |label| label.project = current_project label.title = 'qa-mr-test-label' label.description = 'Merge Request label' end - Factory::Resource::MergeRequest.fabricate! do |merge_request| + Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request with a milestone' merge_request.description = 'Great feature with milestone' merge_request.project = current_project @@ -49,11 +49,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - current_project = Factory::Resource::Project.fabricate! do |project| + current_project = Resource::Project.fabricate! do |project| project.name = 'project-with-merge-request' end - Factory::Resource::MergeRequest.fabricate! do |merge_request| + Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request' merge_request.description = 'Great feature' merge_request.project = current_project diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 058af8aebdd..6dcd74471fe 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request| + merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request| merge_request.fork_branch = 'feature-branch' end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index 3bcf086d332..e2d639fd150 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = "only-fast-forward" end project.visit! @@ -15,12 +15,12 @@ module QA Page::Project::Menu.act { go_to_settings } Page::Project::Settings::MergeRequest.act { enable_ff_only } - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project merge_request.title = 'Needs rebasing' end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.file_name = "other.txt" push.file_content = "New file added!" diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb index 724c48cd125..6ff7360c413 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -7,16 +7,16 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = "squash-before-merge" end - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project merge_request.title = 'Squashing commits' end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.commit_message = 'to be squashed' push.branch_name = merge_request.source_branch diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index 7705e12b95e..297485dd81e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -13,7 +13,7 @@ module QA before(:all) do login - @project = Factory::Resource::Project.fabricate! do |project| + @project = Resource::Project.fabricate! do |project| project.name = 'file-template-project' project.description = 'Add file templates via the Files view' end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb index df70b9608d9..94be66782c6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - key = Factory::Resource::SSHKey.fabricate! do |resource| + key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index b18dee53cbc..6a0add56fe0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -14,7 +14,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |scenario| + project = Resource::Project.fabricate! do |scenario| scenario.name = 'project-with-code' scenario.description = 'project for git clone tests' end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb index f65a1569fb0..46346d1b984 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb @@ -12,7 +12,7 @@ module QA file_content = 'QA Test - File content' commit_message_for_create = 'QA Test - Create new file' - Factory::Resource::File.fabricate! do |file| + Resource::File.fabricate! do |file| file.name = file_name file.content = file_content file.commit_message = commit_message_for_create diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb index 8e4210482a2..a63b7dce8d6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -7,14 +7,14 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - access_token = Factory::Resource::PersonalAccessToken.fabricate!.access_token + access_token = Resource::PersonalAccessToken.fabricate!.access_token - user = Factory::Resource::User.new.tap do |user| + user = Resource::User.new.tap do |user| user.username = Runtime::User.username user.password = access_token end - push = Factory::Repository::ProjectPush.fabricate! do |push| + push = Resource::Repository::ProjectPush.fabricate! do |push| push.user = user push.file_name = 'README.md' push.file_content = '# This is a test project' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index 2f63a07e0c3..92f596a44d9 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' push.file_content = '# This is a test project' push.commit_message = 'Add README.md' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index ac71cf52b6f..73a3dc14a65 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -6,7 +6,7 @@ module QA let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } let(:project) do - Factory::Resource::Project.fabricate! do |resource| + Resource::Project.fabricate! do |resource| resource.name = 'protected-branch-project' end end @@ -47,7 +47,7 @@ module QA end def create_protected_branch(allow_to_push:) - Factory::Resource::Branch.fabricate! do |resource| + Resource::Branch.fabricate! do |resource| resource.branch_name = branch_name resource.project = project resource.allow_to_push = allow_to_push @@ -56,7 +56,7 @@ module QA end def push_new_file(branch) - Factory::Repository::ProjectPush.fabricate! do |resource| + Resource::Repository::ProjectPush.fabricate! do |resource| resource.project = project resource.file_name = 'new_file.md' resource.file_content = '# This is a new file' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb index 36068ffba69..9c764424129 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb @@ -12,11 +12,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - key = Factory::Resource::SSHKey.fabricate! do |resource| + key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.ssh_key = key push.file_name = 'README.md' push.file_content = '# Test Use SSH Key' diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index 07dbf39a8a3..e7374377104 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -13,7 +13,7 @@ module QA before(:all) do login - @project = Factory::Resource::Project.fabricate! do |project| + @project = Resource::Project.fabricate! do |project| project.name = 'file-template-project' project.description = 'Add file templates via the Web IDE' end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 4126fd9fd3e..210271705d9 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -18,7 +18,7 @@ module QA end it 'user creates, edits, clones, and pushes to the wiki' do - wiki = Factory::Resource::Wiki.fabricate! do |resource| + wiki = Resource::Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' resource.message = 'Update home' @@ -34,7 +34,7 @@ module QA validate_content('My Second Wiki Content') - Factory::Repository::WikiPush.fabricate! do |push| + Resource::Repository::WikiPush.fabricate! do |push| push.wiki = wiki push.file_name = 'Home.md' push.file_content = '# My Third Wiki Content' diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 58b272adcf1..0837b720df1 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' resource.value = 'some CI variable' end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb index d66bcce879b..25cbe41c684 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -13,18 +13,18 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = 'project-with-pipelines' project.description = 'Project with CI/CD Pipelines.' end - Factory::Resource::Runner.fabricate! do |runner| + Resource::Runner.fabricate! do |runner| runner.project = project runner.name = executor runner.tags = %w[qa test] end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.file_name = '.gitlab-ci.yml' push.commit_message = 'Add .gitlab-ci.yml' diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb index 5d9aa00582f..3af7db751e7 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb @@ -13,7 +13,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Runner.fabricate! do |runner| + Resource::Runner.fabricate! do |runner| runner.name = executor end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 64b98da8bf5..84757f25379 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -11,7 +11,7 @@ module QA deploy_key_title = 'deploy key title' deploy_key_value = key.public_key - deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| + deploy_key = Resource::DeployKey.fabricate! do |resource| resource.title = deploy_key_title resource.key = deploy_key_value end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 604641e54b8..e2320c92343 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -15,13 +15,13 @@ module QA @runner_name = "qa-runner-#{Time.now.to_i}" - @project = Factory::Resource::Project.fabricate! do |resource| + @project = Resource::Project.fabricate! do |resource| resource.name = 'deploy-key-clone-project' end @repository_location = @project.repository_ssh_location - Factory::Resource::Runner.fabricate! do |resource| + Resource::Runner.fabricate! do |resource| resource.project = @project resource.name = @runner_name resource.tags = %w[qa docker] @@ -47,7 +47,7 @@ module QA login - Factory::Resource::DeployKey.fabricate! do |resource| + Resource::DeployKey.fabricate! do |resource| resource.project = @project resource.title = "deploy key #{key.name}(#{key.bits})" resource.key = key.public_key @@ -55,7 +55,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key @@ -78,7 +78,7 @@ module QA - docker YAML - Factory::Repository::ProjectPush.fabricate! do |resource| + Resource::Repository::ProjectPush.fabricate! do |resource| resource.project = @project resource.file_name = '.gitlab-ci.yml' resource.commit_message = 'Add .gitlab-ci.yml' diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb index 263ba6a6800..9f34e4218c1 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb @@ -10,7 +10,7 @@ module QA deploy_token_name = 'deploy token name' deploy_token_expires_at = Date.today + 7 # 1 Week from now - deploy_token = Factory::Resource::DeployToken.fabricate! do |resource| + deploy_token = Resource::DeployToken.fabricate! do |resource| resource.name = deploy_token_name resource.expires_at = deploy_token_expires_at end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index c2fce1e7df1..30ec0665973 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -15,21 +15,21 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |p| + project = Resource::Project.fabricate! do |p| p.name = 'project-with-autodevops' p.description = 'Project with Auto Devops' end # Disable code_quality check in Auto DevOps pipeline as it takes # too long and times out the test - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.project = project resource.key = 'CODE_QUALITY_DISABLED' resource.value = '1' end # Create Auto Devops compatible repo - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.directory = Pathname .new(__dir__) @@ -41,7 +41,7 @@ module QA # Create and connect K8s cluster @cluster = Service::KubernetesCluster.new(rbac: rbac).create! - kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster| + kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster| cluster.project = project cluster.cluster = @cluster cluster.install_helm_tiller = true diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb index 2f6c59b3e69..820c506b715 100644 --- a/qa/spec/factory/resource/user_spec.rb +++ b/qa/spec/factory/resource/user_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Factory::Resource::User do +describe QA::Resource::User do describe "#fabricate_via_api!" do Response = Struct.new(:code, :body) diff --git a/qa/spec/factory/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb index e5fbc064911..a5ed4422f6e 100644 --- a/qa/spec/factory/api_fabricator_spec.rb +++ b/qa/spec/resource/api_fabricator_spec.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true -describe QA::Factory::ApiFabricator do - let(:factory_without_api_support) do +describe QA::Resource::ApiFabricator do + let(:resource_without_api_support) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end end end - let(:factory_with_api_support) do + let(:resource_with_api_support) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end def api_get_path @@ -33,22 +33,22 @@ describe QA::Factory::ApiFabricator do allow(subject).to receive(:current_url).and_return('') end - subject { factory.tap { |f| f.include(described_class) }.new } + subject { resource.tap { |f| f.include(described_class) }.new } describe '#api_support?' do let(:api_client) { spy('Runtime::API::Client') } let(:api_client_instance) { double('API Client') } - context 'when factory does not support fabrication via the API' do - let(:factory) { factory_without_api_support } + context 'when resource does not support fabrication via the API' do + let(:resource) { resource_without_api_support } it 'returns false' do expect(subject).not_to be_api_support end end - context 'when factory supports fabrication via the API' do - let(:factory) { factory_with_api_support } + context 'when resource supports fabrication via the API' do + let(:resource) { resource_with_api_support } it 'returns false' do expect(subject).to be_api_support @@ -67,20 +67,20 @@ describe QA::Factory::ApiFabricator do allow(api_client_instance).to receive(:personal_access_token).and_return('foo') end - context 'when factory does not support fabrication via the API' do - let(:factory) { factory_without_api_support } + context 'when resource does not support fabrication via the API' do + let(:resource) { resource_without_api_support } it 'raises a NotImplementedError exception' do - expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Factory FooBarFactory does not support fabrication via the API!") + expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Resource FooBarResource does not support fabrication via the API!") end end - context 'when factory supports fabrication via the API' do - let(:factory) { factory_with_api_support } + context 'when resource supports fabrication via the API' do + let(:resource) { resource_with_api_support } let(:api_request) { spy('Runtime::API::Request') } let(:resource_web_url) { 'http://example.org/api/v4/foo' } - let(:resource) { { id: 1, name: 'John Doe', web_url: resource_web_url } } - let(:raw_post) { double('Raw POST response', code: 201, body: resource.to_json) } + let(:response) { { id: 1, name: 'John Doe', web_url: resource_web_url } } + let(:raw_post) { double('Raw POST response', code: 201, body: response.to_json) } before do stub_const('QA::Runtime::API::Request', api_request) @@ -103,7 +103,7 @@ describe QA::Factory::ApiFabricator do it 'populates api_resource with the resource' do subject.fabricate_via_api! - expect(subject.api_resource).to eq(resource) + expect(subject.api_resource).to eq(response) end context 'when the POST fails' do @@ -114,17 +114,17 @@ describe QA::Factory::ApiFabricator do expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url)) expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) - expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarFactory using the API failed (400) with `#{raw_post}`.") + expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.") expect(subject.api_resource).to be_nil end end end context '#transform_api_resource' do - let(:factory) do + let(:resource) do Class.new do def self.name - 'FooBarFactory' + 'FooBarResource' end def api_get_path @@ -146,12 +146,12 @@ describe QA::Factory::ApiFabricator do end end - let(:resource) { { existing: 'foo', web_url: resource_web_url } } + let(:response) { { existing: 'foo', web_url: resource_web_url } } let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } } it 'transforms the resource' do expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) - expect(subject).to receive(:transform_api_resource).with(resource).and_return(transformed_resource) + expect(subject).to receive(:transform_api_resource).with(response).and_return(transformed_resource) subject.fabricate_via_api! end diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/resource/base_spec.rb index 990eba76460..dc9e16792d3 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -1,49 +1,49 @@ # frozen_string_literal: true -describe QA::Factory::Base do +describe QA::Resource::Base do include Support::StubENV - let(:factory) { spy('factory') } + let(:resource) { spy('resource') } let(:location) { 'http://location' } shared_context 'fabrication context' do subject do Class.new(described_class) do def self.name - 'MyFactory' + 'MyResource' end end end before do allow(subject).to receive(:current_url).and_return(location) - allow(subject).to receive(:new).and_return(factory) + allow(subject).to receive(:new).and_return(resource) end end shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil| let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called } - it 'yields factory before calling factory method' do - expect(factory).to receive(:something!).ordered - expect(factory).to receive(fabrication_method_used).ordered.and_return(location) + it 'yields resource before calling resource method' do + expect(resource).to receive(:something!).ordered + expect(resource).to receive(fabrication_method_used).ordered.and_return(location) - subject.public_send(fabrication_method_called, factory: factory) do |factory| - factory.something! + subject.public_send(fabrication_method_called, resource: resource) do |resource| + resource.something! end end - it 'does not log the factory and build method when QA_DEBUG=false' do + it 'does not log the resource and build method when QA_DEBUG=false' do stub_env('QA_DEBUG', 'false') - expect(factory).to receive(fabrication_method_used).and_return(location) + expect(resource).to receive(fabrication_method_used).and_return(location) - expect { subject.public_send(fabrication_method_called, 'something', factory: factory) } + expect { subject.public_send(fabrication_method_called, 'something', resource: resource) } .not_to output.to_stdout end end describe '.fabricate!' do - context 'when factory does not support fabrication via the API' do + context 'when resource does not support fabrication via the API' do before do expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError) end @@ -55,7 +55,7 @@ describe QA::Factory::Base do end end - context 'when factory supports fabrication via the API' do + context 'when resource supports fabrication via the API' do it 'calls .fabricate_via_browser_ui!' do expect(described_class).to receive(:fabricate_via_api!) @@ -69,20 +69,20 @@ describe QA::Factory::Base do it_behaves_like 'fabrication method', :fabricate_via_api! - it 'instantiates the factory, calls factory method returns the resource' do - expect(factory).to receive(:fabricate_via_api!).and_return(location) + it 'instantiates the resource, calls resource method returns the resource' do + expect(resource).to receive(:fabricate_via_api!).and_return(location) - result = subject.fabricate_via_api!(factory: factory, parents: []) + result = subject.fabricate_via_api!(resource: resource, parents: []) - expect(result).to eq(factory) + expect(result).to eq(resource) end - it 'logs the factory and build method when QA_DEBUG=true' do + it 'logs the resource and build method when QA_DEBUG=true' do stub_env('QA_DEBUG', 'true') - expect(factory).to receive(:fabricate_via_api!).and_return(location) + expect(resource).to receive(:fabricate_via_api!).and_return(location) - expect { subject.fabricate_via_api!('something', factory: factory, parents: []) } - .to output(/==> Built a MyFactory via api in [\d\.\-e]+ seconds+/) + expect { subject.fabricate_via_api!('something', resource: resource, parents: []) } + .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/) .to_stdout end end @@ -92,30 +92,30 @@ describe QA::Factory::Base do it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! - it 'instantiates the factory and calls factory method' do - subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) + it 'instantiates the resource and calls resource method' do + subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(factory).to have_received(:fabricate!).with('something') + expect(resource).to have_received(:fabricate!).with('something') end it 'returns fabrication resource' do - result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) + result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(result).to eq(factory) + expect(result).to eq(resource) end - it 'logs the factory and build method when QA_DEBUG=true' do + it 'logs the resource and build method when QA_DEBUG=true' do stub_env('QA_DEBUG', 'true') - expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) } - .to output(/==> Built a MyFactory via browser_ui in [\d\.\-e]+ seconds+/) + expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) } + .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/) .to_stdout end end - shared_context 'simple factory' do + shared_context 'simple resource' do subject do - Class.new(QA::Factory::Base) do + Class.new(QA::Resource::Base) do attribute :test do 'block' end @@ -132,11 +132,11 @@ describe QA::Factory::Base do end end - let(:factory) { subject.new } + let(:resource) { subject.new } end describe '.attribute' do - include_context 'simple factory' + include_context 'simple resource' it 'appends new attribute' do expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) @@ -144,7 +144,7 @@ describe QA::Factory::Base do context 'when the attribute is populated via a block' do it 'returns value from the block' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('block') @@ -155,11 +155,11 @@ describe QA::Factory::Base do let(:api_resource) { { no_block: 'api' } } before do - expect(factory).to receive(:api_resource).and_return(api_resource) + expect(resource).to receive(:api_resource).and_return(api_resource) end it 'returns value from api' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.no_block).to eq('api') @@ -173,7 +173,7 @@ describe QA::Factory::Base do end it 'returns value from api and emits an INFO log entry' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('api_with_block') @@ -185,11 +185,11 @@ describe QA::Factory::Base do context 'when the attribute is populated via direct assignment' do before do - factory.test = 'value' + resource.test = 'value' end it 'returns value from the assignment' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('value') @@ -197,11 +197,11 @@ describe QA::Factory::Base do context 'when the api also has such response' do before do - allow(factory).to receive(:api_resource).and_return({ test: 'api' }) + allow(resource).to receive(:api_resource).and_return({ test: 'api' }) end it 'returns value from the assignment' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('value') @@ -211,36 +211,36 @@ describe QA::Factory::Base do context 'when the attribute has no value' do it 'raises an error because no values could be found' do - result = subject.fabricate!(factory: factory) + result = subject.fabricate!(resource: resource) expect { result.no_block } - .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{factory.class.name}.") + .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.") end end end describe '#web_url' do - include_context 'simple factory' + include_context 'simple resource' it 'sets #web_url to #current_url after fabrication' do - subject.fabricate!(factory: factory) + subject.fabricate!(resource: resource) - expect(factory.web_url).to eq(subject.current_url) + expect(resource.web_url).to eq(subject.current_url) end end describe '#visit!' do - include_context 'simple factory' + include_context 'simple resource' before do - allow(factory).to receive(:visit) + allow(resource).to receive(:visit) end it 'calls #visit with the underlying #web_url' do - factory.web_url = subject.current_url - factory.visit! + resource.web_url = subject.current_url + resource.visit! - expect(factory).to have_received(:visit).with(subject.current_url) + expect(resource).to have_received(:visit).with(subject.current_url) end end end diff --git a/qa/spec/factory/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb index 2eb6c008248..bf3ebce0cfe 100644 --- a/qa/spec/factory/repository/push_spec.rb +++ b/qa/spec/resource/repository/push_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Factory::Repository::Push do +describe QA::Resource::Repository::Push do describe '.files=' do let(:files) do [ diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 98946e4287b..6d0483f0032 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -50,7 +50,7 @@ describe Boards::IssuesController do parsed_response = JSON.parse(response.body) - expect(response).to match_response_schema('issues') + expect(response).to match_response_schema('entities/issue_boards') expect(parsed_response['issues'].length).to eq 2 expect(development.issues.map(&:relative_position)).not_to include(nil) end @@ -121,7 +121,7 @@ describe Boards::IssuesController do parsed_response = JSON.parse(response.body) - expect(response).to match_response_schema('issues') + expect(response).to match_response_schema('entities/issue_boards') expect(parsed_response['issues'].length).to eq 2 end end @@ -168,7 +168,7 @@ describe Boards::IssuesController do it 'returns the created issue' do create_issue user: user, board: board, list: list1, title: 'New issue' - expect(response).to match_response_schema('issue') + expect(response).to match_response_schema('entities/issue_board') end end diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb index d1c960e895d..5b7da81b6a1 100644 --- a/spec/controllers/projects/deployments_controller_spec.rb +++ b/spec/controllers/projects/deployments_controller_spec.rb @@ -15,9 +15,9 @@ describe Projects::DeploymentsController do describe 'GET #index' do it 'returns list of deployments from last 8 hours' do - create(:deployment, environment: environment, created_at: 9.hours.ago) - create(:deployment, environment: environment, created_at: 7.hours.ago) - create(:deployment, environment: environment) + create(:deployment, :success, environment: environment, created_at: 9.hours.ago) + create(:deployment, :success, environment: environment, created_at: 7.hours.ago) + create(:deployment, :success, environment: environment) get :index, deployment_params(after: 8.hours.ago) @@ -27,7 +27,7 @@ describe Projects::DeploymentsController do end it 'returns a list with deployments information' do - create(:deployment, environment: environment) + create(:deployment, :success, environment: environment) get :index, deployment_params @@ -37,7 +37,7 @@ describe Projects::DeploymentsController do end describe 'GET #metrics' do - let(:deployment) { create(:deployment, project: project, environment: environment) } + let(:deployment) { create(:deployment, :success, project: project, environment: environment) } before do allow(controller).to receive(:deployment).and_return(deployment) @@ -110,7 +110,7 @@ describe Projects::DeploymentsController do end describe 'GET #additional_metrics' do - let(:deployment) { create(:deployment, project: project, environment: environment) } + let(:deployment) { create(:deployment, :success, project: project, environment: environment) } before do allow(controller).to receive(:deployment).and_return(deployment) diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 8eb01145ed5..da3d658d061 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -231,7 +231,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do context 'with deployment' do let(:merge_request) { create(:merge_request, source_project: project) } let(:environment) { create(:environment, project: project, name: 'staging', state: :available) } - let(:job) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } + let(:job) { create(:ci_build, :running, environment: environment.name, pipeline: pipeline) } it 'exposes the deployment information' do expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index dafff4ee405..e62523c65c9 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -755,7 +755,7 @@ describe Projects::MergeRequestsController do let(:environment) { create(:environment, project: forked) } let(:pipeline) { create(:ci_pipeline, sha: sha, project: forked) } let(:build) { create(:ci_build, pipeline: pipeline) } - let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: 'master', deployable: build) } + let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: 'master', deployable: build) } let(:merge_request) do create(:merge_request, source_project: forked, target_project: project, target_branch: 'master', head_pipeline: pipeline) @@ -780,7 +780,7 @@ describe Projects::MergeRequestsController do let(:merge_commit_sha) { project.repository.merge(user, forked.commit.id, merge_request, "merged in test") } let(:post_merge_pipeline) { create(:ci_pipeline, sha: merge_commit_sha, project: project) } let(:post_merge_build) { create(:ci_build, pipeline: post_merge_pipeline) } - let!(:source_deployment) { create(:deployment, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) } + let!(:source_deployment) { create(:deployment, :succeed, environment: source_environment, sha: merge_commit_sha, ref: 'master', deployable: post_merge_build) } before do merge_request.update!(merge_commit_sha: merge_commit_sha) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0cacdf7931f..90754319f05 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -100,6 +100,30 @@ FactoryBot.define do url: 'http://staging.example.com/$CI_JOB_NAME' } end + trait :deploy_to_production do + environment 'production' + + options environment: { name: 'production', + url: 'http://prd.example.com/$CI_JOB_NAME' } + end + + trait :start_review_app do + environment 'review/$CI_COMMIT_REF_NAME' + + options environment: { name: 'review/$CI_COMMIT_REF_NAME', + url: 'http://staging.example.com/$CI_JOB_NAME', + on_stop: 'stop_review_app' } + end + + trait :stop_review_app do + name 'stop_review_app' + environment 'review/$CI_COMMIT_REF_NAME' + + options environment: { name: 'review/$CI_COMMIT_REF_NAME', + url: 'http://staging.example.com/$CI_JOB_NAME', + action: 'stop' } + end + trait :allowed_to_fail do allow_failure true end diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 90d6a338479..011c98599a3 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -21,5 +21,31 @@ FactoryBot.define do sha { TestEnv::BRANCH_SHA['pages-deploy'] } ref 'pages-deploy' end + + trait :running do + status :running + end + + trait :success do + status :success + finished_at { Time.now } + end + + trait :failed do + status :failed + finished_at { Time.now } + end + + trait :canceled do + status :canceled + finished_at { Time.now } + end + + # This trait hooks the state maechine's events + trait :succeed do + after(:create) do |deployment, evaluator| + deployment.succeed! + end + end end end diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb index b5db57d5148..9d9e3d693b8 100644 --- a/spec/factories/environments.rb +++ b/spec/factories/environments.rb @@ -22,6 +22,7 @@ FactoryBot.define do pipeline: pipeline) deployment = create(:deployment, + :success, environment: environment, project: environment.project, deployable: deployable, diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index aa3ca8923ff..a1f93bd3fbd 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -153,7 +153,7 @@ describe 'Contributions Calendar', :js do include_context 'visit user page' it 'displays calendar activity log' do - expect(find('.tab-pane#activity .content_list .event-note')).to have_content issue_title + expect(find('.tab-pane#activity .content_list .event-target-title')).to have_content issue_title end end end diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb index 498775acff3..16919fe63ad 100644 --- a/spec/features/dashboard/project_member_activity_index_spec.rb +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -14,14 +14,15 @@ describe 'Project member activity', :js do wait_for_requests end - subject { page.find(".event-title").text } - context 'when a user joins the project' do before do visit_activities_and_wait_with_event(Event::JOINED) end - it { is_expected.to eq("#{user.name} joined project") } + it "presents the correct message" do + expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}") + expect(page.find('.event-title').text).to eq("joined project") + end end context 'when a user leaves the project' do @@ -29,7 +30,10 @@ describe 'Project member activity', :js do visit_activities_and_wait_with_event(Event::LEFT) end - it { is_expected.to eq("#{user.name} left project") } + it "presents the correct message" do + expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}") + expect(page.find('.event-title').text).to eq("left project") + end end context 'when a users membership expires for the project' do @@ -38,8 +42,8 @@ describe 'Project member activity', :js do end it "presents the correct message" do - message = "#{user.name} removed due to membership expiration from project" - is_expected.to eq(message) + expect(page.find('.event-user-info').text).to eq("#{user.name} #{user.to_reference}") + expect(page.find('.event-title').text).to eq("removed due to membership expiration from project") end end end diff --git a/spec/features/merge_request/user_sees_deployment_widget_spec.rb b/spec/features/merge_request/user_sees_deployment_widget_spec.rb index a298ead43db..0e439c8cb2d 100644 --- a/spec/features/merge_request/user_sees_deployment_widget_spec.rb +++ b/spec/features/merge_request/user_sees_deployment_widget_spec.rb @@ -11,7 +11,7 @@ describe 'Merge request > User sees deployment widget', :js do let(:sha) { project.commit(ref).id } let(:pipeline) { create(:ci_pipeline_without_jobs, sha: sha, project: project, ref: ref) } let(:build) { create(:ci_build, :success, pipeline: pipeline) } - let!(:deployment) { create(:deployment, environment: environment, sha: sha, ref: ref, deployable: build) } + let!(:deployment) { create(:deployment, :succeed, environment: environment, sha: sha, ref: ref, deployable: build) } let!(:manual) { } before do @@ -38,7 +38,7 @@ describe 'Merge request > User sees deployment widget', :js do end it 'does start build when stop button clicked' do - accept_confirm { click_button('Stop environment') } + accept_confirm { find('.js-stop-env').click } expect(page).to have_content('close_app') end @@ -47,7 +47,7 @@ describe 'Merge request > User sees deployment widget', :js do let(:role) { :reporter } it 'does not show stop button' do - expect(page).not_to have_button('Stop environment') + expect(page).not_to have_selector('.js-stop-env') end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index 0c610edd6d1..d907ed4198c 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -45,7 +45,8 @@ describe 'Merge request > User sees merge widget', :js do let(:build) { create(:ci_build, :success, pipeline: pipeline) } let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :succeed, + environment: environment, ref: merge_request.source_branch, deployable: build, sha: sha) @@ -179,7 +180,7 @@ describe 'Merge request > User sees merge widget', :js do # Wait for the `ci_status` and `merge_check` requests wait_for_requests - expect(page).to have_text(%r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>}) + expect(page).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.") end end diff --git a/spec/features/merge_request/user_sees_pipelines_spec.rb b/spec/features/merge_request/user_sees_pipelines_spec.rb index 41f447fba95..8faddee4daa 100644 --- a/spec/features/merge_request/user_sees_pipelines_spec.rb +++ b/spec/features/merge_request/user_sees_pipelines_spec.rb @@ -41,8 +41,7 @@ describe 'Merge request > User sees pipelines', :js do visit project_merge_request_path(project, merge_request) wait_for_requests - expect(page.find('.ci-widget')).to have_text( - %r{Could not retrieve the pipeline status\. For troubleshooting steps, read the <a href=\".+\">documentation\.</a>}) + expect(page.find('.ci-widget')).to have_text("Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.") end end diff --git a/spec/features/milestones/user_creates_milestone_spec.rb b/spec/features/milestones/user_creates_milestone_spec.rb index 8fd057d587c..5de0c381cdf 100644 --- a/spec/features/milestones/user_creates_milestone_spec.rb +++ b/spec/features/milestones/user_creates_milestone_spec.rb @@ -24,6 +24,6 @@ describe "User creates milestone", :js do visit(activity_project_path(project)) - expect(page).to have_content("#{user.name} opened milestone") + expect(page).to have_content("#{user.name} #{user.to_reference} opened milestone") end end diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb index a8c296b4cd2..f68ed1cde07 100644 --- a/spec/features/milestones/user_deletes_milestone_spec.rb +++ b/spec/features/milestones/user_deletes_milestone_spec.rb @@ -23,7 +23,7 @@ describe "User deletes milestone", :js do visit(activity_project_path(project)) - expect(page).to have_content("#{user.name} destroyed milestone") + expect(page).to have_content("#{user.name} #{user.to_reference} destroyed milestone") end end diff --git a/spec/features/projects/activity/user_sees_activity_spec.rb b/spec/features/projects/activity/user_sees_activity_spec.rb index ebaa137772d..bb4b2abc3c7 100644 --- a/spec/features/projects/activity/user_sees_activity_spec.rb +++ b/spec/features/projects/activity/user_sees_activity_spec.rb @@ -19,13 +19,13 @@ describe 'Projects > Activity > User sees activity' do it 'shows the last push in the activity page', :js do visit activity_project_path(project) - expect(page).to have_content "#{user.name} pushed new branch fix" + expect(page).to have_content "#{user.name} #{user.to_reference} pushed new branch fix" end it 'allows to filter event with the "event_filter=issue" URL param', :js do visit activity_project_path(project, event_filter: 'issue') - expect(page).not_to have_content "#{user.name} pushed new branch fix" - expect(page).to have_content "#{user.name} opened issue #{issue.to_reference}" + expect(page).not_to have_content "#{user.name} #{user.to_reference} pushed new branch fix" + expect(page).to have_content "#{user.name} #{user.to_reference} opened issue #{issue.to_reference}" end end diff --git a/spec/features/projects/activity/user_sees_private_activity_spec.rb b/spec/features/projects/activity/user_sees_private_activity_spec.rb index d7dc0a6712a..61ec2ce9d29 100644 --- a/spec/features/projects/activity/user_sees_private_activity_spec.rb +++ b/spec/features/projects/activity/user_sees_private_activity_spec.rb @@ -5,7 +5,7 @@ describe 'Project > Activity > User sees private activity', :js do let(:author) { create(:user) } let(:user) { create(:user) } let(:issue) { create(:issue, :confidential, project: project, author: author) } - let(:message) { "#{author.name} opened issue #{issue.to_reference}" } + let(:message) { "#{author.name} #{author.to_reference} opened issue #{issue.to_reference}" } before do project.add_developer(author) diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 4f8f67aab22..056f4ee2e22 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -33,7 +33,7 @@ describe 'Environment' do context 'with deployments' do context 'when there is no related deployable' do let(:deployment) do - create(:deployment, environment: environment, deployable: nil) + create(:deployment, :success, environment: environment, deployable: nil) end it 'does show deployment SHA' do @@ -48,7 +48,7 @@ describe 'Environment' do let(:build) { create(:ci_build, pipeline: pipeline) } let(:deployment) do - create(:deployment, environment: environment, deployable: build) + create(:deployment, :success, environment: environment, deployable: build) end it 'does show build name' do @@ -108,7 +108,7 @@ describe 'Environment' do context 'with external_url' do let(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } let(:build) { create(:ci_build, pipeline: pipeline) } - let(:deployment) { create(:deployment, environment: environment, deployable: build) } + let(:deployment) { create(:deployment, :success, environment: environment, deployable: build) } it 'does show an external link button' do expect(page).to have_link(nil, href: environment.external_url) @@ -169,7 +169,8 @@ describe 'Environment' do end let(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, on_stop: 'close_app') end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 22d0187ac81..d0ddf69d574 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -132,7 +132,8 @@ describe 'Environments page', :js do let(:project) { create(:project, :repository) } let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, sha: project.commit.id) end @@ -152,7 +153,8 @@ describe 'Environments page', :js do end let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, sha: project.commit.id) end @@ -196,7 +198,7 @@ describe 'Environments page', :js do context 'with external_url' do let(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') } let(:build) { create(:ci_build, pipeline: pipeline) } - let(:deployment) { create(:deployment, environment: environment, deployable: build) } + let(:deployment) { create(:deployment, :success, environment: environment, deployable: build) } it 'shows an external link button' do expect(page).to have_link(nil, href: environment.external_url) @@ -209,7 +211,8 @@ describe 'Environments page', :js do end let(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, on_stop: 'close_app') end @@ -275,6 +278,7 @@ describe 'Environments page', :js do let!(:deployment) do create(:deployment, + :success, environment: environment, deployable: build, sha: project.commit.id) diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 5cb3f7c732f..cbb935abd53 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -396,8 +396,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end context 'job is successful and has deployment' do - let(:build) { create(:ci_build, :success, :trace_live, environment: environment.name, pipeline: pipeline) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + let(:build) { create(:ci_build, :success, :trace_live, environment: environment.name, pipeline: pipeline, deployment: deployment) } + let(:deployment) { create(:deployment, :success, environment: environment, project: environment.project) } it 'shows a link for the job' do expect(page).to have_link environment.name @@ -419,7 +419,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end context 'deployment still not finished' do - let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } + let(:build) { create(:ci_build, :running, environment: environment.name, pipeline: pipeline) } it 'shows a link to latest deployment' do expect(page).to have_link environment.name @@ -456,6 +456,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do describe 'environment info in job view', :js do before do + allow_any_instance_of(Ci::Build).to receive(:create_deployment) + visit project_job_path(project, job) wait_for_requests end @@ -464,8 +466,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do let(:job) { create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) } let(:second_build) { create(:ci_build, :success, :trace_artifact, environment: 'staging', pipeline: pipeline) } let(:environment) { create(:environment, name: 'staging', project: project) } - let!(:first_deployment) { create(:deployment, environment: environment, deployable: job) } - let!(:second_deployment) { create(:deployment, environment: environment, deployable: second_build) } + let!(:first_deployment) { create(:deployment, :success, environment: environment, deployable: job) } + let!(:second_deployment) { create(:deployment, :success, environment: environment, deployable: second_build) } it 'shows deployment message' do expected_text = 'This job is an out-of-date deployment ' \ @@ -505,7 +507,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end context 'when it has deployment' do - let!(:deployment) { create(:deployment, environment: environment) } + let!(:deployment) { create(:deployment, :success, environment: environment) } it 'shows that deployment will be overwritten' do expected_text = 'This job is creating a deployment to staging' diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index a48ad94e9fa..7bfcd46713e 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -44,7 +44,7 @@ describe 'View on environment', :js do context 'and an active deployment' do let(:sha) { project.commit(branch_name).sha } let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') } - let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) } + let!(:deployment) { create(:deployment, :success, environment: environment, ref: branch_name, sha: sha) } context 'when visiting the diff of a merge request for the branch' do let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) } diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb index 3cd421f22eb..25835bb4d94 100644 --- a/spec/finders/environments_finder_spec.rb +++ b/spec/finders/environments_finder_spec.rb @@ -12,7 +12,7 @@ describe EnvironmentsFinder do context 'tagged deployment' do before do - create(:deployment, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id) + create(:deployment, :success, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id) end it 'returns environment when with_tags is set' do @@ -33,7 +33,7 @@ describe EnvironmentsFinder do context 'branch deployment' do before do - create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + create(:deployment, :success, environment: environment, ref: 'master', sha: project.commit.id) end it 'returns environment when ref is set' do @@ -59,7 +59,7 @@ describe EnvironmentsFinder do context 'commit deployment' do before do - create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + create(:deployment, :success, environment: environment, ref: 'master', sha: project.commit.id) end it 'returns environment' do @@ -71,7 +71,7 @@ describe EnvironmentsFinder do context 'recently updated' do context 'when last deployment to environment is the most recent one' do before do - create(:deployment, environment: environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'feature') end it 'finds recently updated environment' do @@ -82,8 +82,8 @@ describe EnvironmentsFinder do context 'when last deployment to environment is not the most recent' do before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: environment, ref: 'master') + create(:deployment, :success, environment: environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'master') end it 'does not find environment' do @@ -96,8 +96,8 @@ describe EnvironmentsFinder do let(:second_environment) { create(:environment, project: project) } before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: second_environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'feature') + create(:deployment, :success, environment: second_environment, ref: 'feature') end it 'finds both environments' do diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json new file mode 100644 index 00000000000..8d821ebb843 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/issue_board.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "properties" : { + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "title": { "type": "string" }, + "confidential": { "type": "boolean" }, + "due_date": { "type": "date" }, + "project_id": { "type": "integer" }, + "relative_position": { "type": ["integer", "null"] }, + "weight": { "type": "integer" }, + "project": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "path": { "type": "string" } + } + }, + "milestone": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "title": { "type": "string" } + } + }, + "assignees": { "type": ["array", "null"] }, + "labels": { + "type": "array", + "items": { "$ref": "label.json" } + }, + "reference_path": { "type": "string" }, + "real_path": { "type": "string" }, + "issue_sidebar_endpoint": { "type": "string" }, + "toggle_subscription_endpoint": { "type": "string" }, + "assignable_labels_endpoint": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/issue_boards.json b/spec/fixtures/api/schemas/entities/issue_boards.json new file mode 100644 index 00000000000..0ac1d9468c8 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/issue_boards.json @@ -0,0 +1,15 @@ +{ + "type": "object", + "required" : [ + "issues", + "size" + ], + "properties" : { + "issues": { + "type": "array", + "items": { "$ref": "issue_board.json" } + }, + "size": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js index b7b29190c31..093fec97951 100644 --- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js +++ b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js @@ -1,23 +1,35 @@ import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; import { setInput, createForm } from './helper'; +function expectToToggleDisableOnDirtyUpdate(submit, input) { + const originalValue = input.value; + + expect(submit.disabled).toBe(true); + + return setInput(input, `${originalValue} changes`) + .then(() => expect(submit.disabled).toBe(false)) + .then(() => setInput(input, originalValue)) + .then(() => expect(submit.disabled).toBe(true)); +} + describe('DirtySubmitForm', () => { it('disables submit until there are changes', done => { const { form, input, submit } = createForm(); - const originalValue = input.value; new DirtySubmitForm(form); // eslint-disable-line no-new - expect(submit.disabled).toBe(true); + return expectToToggleDisableOnDirtyUpdate(submit, input) + .then(done) + .catch(done.fail); + }); + + it('disables submit until there are changes when initializing with a falsy value', done => { + const { form, input, submit } = createForm(); + input.value = ''; + + new DirtySubmitForm(form); // eslint-disable-line no-new - return setInput(input, `${originalValue} changes`) - .then(() => { - expect(submit.disabled).toBe(false); - }) - .then(() => setInput(input, originalValue)) - .then(() => { - expect(submit.disabled).toBe(true); - }) + return expectToToggleDisableOnDirtyUpdate(submit, input) .then(done) .catch(done.fail); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index ba1889c4dcd..f8ca43fc150 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -423,6 +423,40 @@ describe('Job App ', () => { }); }); + describe('archived job', () => { + beforeEach(() => { + mock.onGet(props.endpoint).reply(200, Object.assign({}, job, { archived: true }), {}); + vm = mountComponentWithStore(Component, { + props, + store, + }); + }); + + it('renders warning about job being archived', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-archived-job ')).not.toBeNull(); + done(); + }, 0); + }); + }); + + describe('non-archived job', () => { + beforeEach(() => { + mock.onGet(props.endpoint).reply(200, job, {}); + vm = mountComponentWithStore(Component, { + props, + store, + }); + }); + + it('does not warning about job being archived', done => { + setTimeout(() => { + expect(vm.$el.querySelector('.js-archived-job ')).toBeNull(); + done(); + }, 0); + }); + }); + describe('trace output', () => { beforeEach(() => { mock.onGet(props.endpoint).reply(200, job, {}); diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js index f58515daa4f..69767d9cf1c 100644 --- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js +++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js @@ -151,11 +151,11 @@ describe('Grouped Test Reports App', () => { it('renders resolved failures', done => { setTimeout(() => { - expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain( + expect(vm.$el.querySelector('.report-block-container').textContent).toContain( resolvedFailures.suites[0].resolved_failures[0].name, ); - expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain( + expect(vm.$el.querySelector('.report-block-container').textContent).toContain( resolvedFailures.suites[0].resolved_failures[1].name, ); done(); diff --git a/spec/javascripts/reports/components/report_section_spec.js b/spec/javascripts/reports/components/report_section_spec.js index eb7307605d7..b02af8baaec 100644 --- a/spec/javascripts/reports/components/report_section_spec.js +++ b/spec/javascripts/reports/components/report_section_spec.js @@ -120,7 +120,7 @@ describe('Report section', () => { 'Code quality improved on 1 point and degraded on 1 point', ); - expect(vm.$el.querySelectorAll('.js-mr-code-resolved-issues li').length).toEqual( + expect(vm.$el.querySelectorAll('.report-block-container li').length).toEqual( resolvedIssues.length, ); }); diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js index 3d44af11153..2f1bd00fa10 100644 --- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js @@ -242,6 +242,10 @@ describe('Deployment component', () => { it('renders information about running deployment', () => { expect(vm.$el.querySelector('.js-deployment-info').textContent).toContain('Deploying to'); }); + + it('renders disabled stop button', () => { + expect(vm.$el.querySelector('.js-stop-env').getAttribute('disabled')).toBe('disabled'); + }); }); describe('success', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index 6c7637eed13..d905bbe4040 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -73,7 +73,7 @@ describe('MRWidgetPipeline', () => { }); expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain( - 'Could not retrieve the pipeline status. For troubleshooting steps, read the <a href="help">documentation.</a>', + 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.', ); }); diff --git a/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js new file mode 100644 index 00000000000..e723fead65e --- /dev/null +++ b/spec/javascripts/vue_shared/components/smart_virtual_list_spec.js @@ -0,0 +1,83 @@ +import Vue from 'vue'; +import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('Toggle Button', () => { + let vm; + + const createComponent = ({ length, remain }) => { + const smartListProperties = { + rtag: 'section', + wtag: 'ul', + wclass: 'test-class', + // Size in pixels does not matter for our tests here + size: 35, + length, + remain, + }; + + const Component = Vue.extend({ + components: { + SmartVirtualScrollList, + }, + smartListProperties, + items: Array(length).fill(1), + template: ` + <smart-virtual-scroll-list v-bind="$options.smartListProperties"> + <li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li> + </smart-virtual-scroll-list>`, + }); + + return mountComponent(Component); + }; + + afterEach(() => { + vm.$destroy(); + }); + + describe('if the list is shorter than the maximum shown elements', () => { + const listLength = 10; + + beforeEach(() => { + vm = createComponent({ length: listLength, remain: 20 }); + }); + + it('renders without the vue-virtual-scroll-list component', () => { + expect(vm.$el.classList).not.toContain('js-virtual-list'); + expect(vm.$el.classList).toContain('js-plain-element'); + }); + + it('renders list with provided tags and classes for the wrapper elements', () => { + expect(vm.$el.tagName).toEqual('SECTION'); + expect(vm.$el.firstChild.tagName).toEqual('UL'); + expect(vm.$el.firstChild.classList).toContain('test-class'); + }); + + it('renders all children list elements', () => { + expect(vm.$el.querySelectorAll('li').length).toEqual(listLength); + }); + }); + + describe('if the list is longer than the maximum shown elements', () => { + const maxItemsShown = 20; + + beforeEach(() => { + vm = createComponent({ length: 1000, remain: maxItemsShown }); + }); + + it('uses the vue-virtual-scroll-list component', () => { + expect(vm.$el.classList).toContain('js-virtual-list'); + expect(vm.$el.classList).not.toContain('js-plain-element'); + }); + + it('renders list with provided tags and classes for the wrapper elements', () => { + expect(vm.$el.tagName).toEqual('SECTION'); + expect(vm.$el.firstChild.tagName).toEqual('UL'); + expect(vm.$el.firstChild.classList).toContain('test-class'); + }); + + it('renders at max twice the maximum shown elements', () => { + expect(vm.$el.querySelectorAll('li').length).toBeLessThanOrEqual(2 * maxItemsShown); + }); + }); +}); diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index 0d0554a2259..a0270d93d50 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -101,15 +101,24 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do context "redmine project" do let(:project) { create(:redmine_project) } - let(:issue) { ExternalIssue.new("#123", project) } - let(:reference) { issue.to_reference } before do - project.issues_enabled = false - project.save! + project.update!(issues_enabled: false) + end + + context "with a hash prefix" do + let(:issue) { ExternalIssue.new("#123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" end - it_behaves_like "external issue tracker" + context "with a single-letter prefix" do + let(:issue) { ExternalIssue.new("T-123", project) } + let(:reference) { issue.to_reference } + + it_behaves_like "external issue tracker" + end end context "jira project" do @@ -122,6 +131,15 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do it_behaves_like "external issue tracker" end + context "with a single-letter prefix" do + let(:issue) { ExternalIssue.new("J-123", project) } + + it "ignores reference" do + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end + end + context "with wrong markdown" do let(:issue) { ExternalIssue.new("#123", project) } diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb new file mode 100644 index 00000000000..4f1b01eed41 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :migration, schema: 20181022173835 do + let(:migration) { described_class.new } + let(:clusters) { create_list(:cluster, 10, :project, :provided_by_gcp) } + + before do + clusters + end + + shared_examples 'consistent kubernetes namespace attributes' do + it 'should populate namespace and service account information' do + subject + + clusters_with_namespace.each do |cluster| + project = cluster.project + cluster_project = cluster.cluster_projects.first + namespace = "#{project.path}-#{project.id}" + kubernetes_namespace = cluster.reload.kubernetes_namespace + + expect(kubernetes_namespace).to be_present + expect(kubernetes_namespace.cluster_project).to eq(cluster_project) + expect(kubernetes_namespace.project).to eq(cluster_project.project) + expect(kubernetes_namespace.cluster).to eq(cluster_project.cluster) + expect(kubernetes_namespace.namespace).to eq(namespace) + expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") + end + end + end + + subject { migration.perform } + + context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do + let(:cluster_projects) { Clusters::Project.all } + + it 'should create a Clusters::KubernetesNamespace per Clusters::Project' do + expect do + subject + end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects.count) + end + + it_behaves_like 'consistent kubernetes namespace attributes' do + let(:clusters_with_namespace) { clusters } + end + end + + context 'when every Clusters::Project has Clusters::KubernetesNamespace' do + before do + clusters.each do |cluster| + create(:cluster_kubernetes_namespace, + cluster_project: cluster.cluster_projects.first, + cluster: cluster, + project: cluster.project) + end + end + + it 'should not create any Clusters::KubernetesNamespace' do + expect do + subject + end.not_to change(Clusters::KubernetesNamespace, :count) + end + end + + context 'when only some Clusters::Project have Clusters::KubernetesNamespace related' do + let(:with_kubernetes_namespace) { clusters.first(6) } + let(:with_no_kubernetes_namespace) { clusters.last(4) } + + before do + with_kubernetes_namespace.each do |cluster| + create(:cluster_kubernetes_namespace, + cluster_project: cluster.cluster_projects.first, + cluster: cluster, + project: cluster.project) + end + end + + it 'creates limited number of Clusters::KubernetesNamespace' do + expect do + subject + end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count) + end + + it 'should not modify clusters with Clusters::KubernetesNamespace' do + subject + + with_kubernetes_namespace.each do |cluster| + expect(cluster.kubernetes_namespaces.count).to eq(1) + end + end + + it_behaves_like 'consistent kubernetes namespace attributes' do + let(:clusters_with_namespace) { with_no_kubernetes_namespace } + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 8095a231cf3..1140bfdf6c3 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Ci::Config::Entry::Reports do where(:keyword, :file) do :junit | 'junit.xml' - :codequality | 'codequality.json' + :codequality | 'gl-code-quality-report.json' :sast | 'gl-sast-report.json' :dependency_scanning | 'gl-dependency-scanning-report.json' :container_scanning | 'gl-container-scanning-report.json' diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 2e67c1c7f78..f8009709ce2 100644 --- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -44,15 +44,15 @@ describe Gitlab::CycleAnalytics::StageSummary do describe "#deploys" do it "finds the number of deploys made created after the 'from date'" do - Timecop.freeze(5.days.ago) { create(:deployment, project: project) } - Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } + Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) } + Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) } expect(subject.third[:value]).to eq(1) end it "doesn't find commits from other projects" do Timecop.freeze(5.days.from_now) do - create(:deployment, project: create(:project, :repository)) + create(:deployment, :success, project: create(:project, :repository)) end expect(subject.third[:value]).to eq(0) diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb index 294ec2c2fd6..edab53247e9 100644 --- a/spec/lib/gitlab/file_detector_spec.rb +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -15,7 +15,12 @@ describe Gitlab::FileDetector do describe '.type_of' do it 'returns the type of a README file' do - expect(described_class.type_of('README.md')).to eq(:readme) + %w[README readme INDEX index].each do |filename| + expect(described_class.type_of(filename)).to eq(:readme) + %w[.md .adoc .rst].each do |extname| + expect(described_class.type_of(filename + extname)).to eq(:readme) + end + end end it 'returns nil for a README file in a directory' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 9a443fa7f20..54291e847d8 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -476,6 +476,27 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#fetch_remote' do + it 'delegates to the gitaly RepositoryService' do + ssh_auth = double(:ssh_auth) + expected_opts = { + ssh_auth: ssh_auth, + forced: true, + no_tags: true, + timeout: described_class::GITLAB_PROJECTS_TIMEOUT, + prune: false + } + + expect(repository.gitaly_repository_client).to receive(:fetch_remote).with('remote-name', expected_opts) + + repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false) + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :fetch_remote do + subject { repository.fetch_remote('remote-name') } + end + end + describe '#find_remote_root_ref' do it 'gets the remote root ref from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::RemoteService) diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 1547d447197..d605fcbafee 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::GitalyClient::RepositoryService do + using RSpec::Parameterized::TableSyntax + let(:project) { create(:project) } let(:storage_name) { project.repository_storage } let(:relative_path) { project.disk_path + '.git' } @@ -107,16 +109,67 @@ describe Gitlab::GitalyClient::RepositoryService do end describe '#fetch_remote' do - let(:ssh_auth) { double(:ssh_auth, ssh_import?: true, ssh_key_auth?: false, ssh_known_hosts: nil) } - let(:import_url) { 'ssh://example.com' } + let(:remote) { 'remote-name' } it 'sends a fetch_remote_request message' do + expected_request = gitaly_request_with_params( + remote: remote, + ssh_key: '', + known_hosts: '', + force: false, + no_tags: false, + no_prune: false + ) + expect_any_instance_of(Gitaly::RepositoryService::Stub) .to receive(:fetch_remote) - .with(gitaly_request_with_params(no_prune: false), kind_of(Hash)) + .with(expected_request, kind_of(Hash)) .and_return(double(value: true)) - client.fetch_remote(import_url, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 60) + client.fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false, timeout: 1) + end + + context 'SSH auth' do + where(:ssh_import, :ssh_key_auth, :ssh_private_key, :ssh_known_hosts, :expected_params) do + false | false | 'key' | 'known_hosts' | {} + false | true | 'key' | 'known_hosts' | {} + true | false | 'key' | 'known_hosts' | { known_hosts: 'known_hosts' } + true | true | 'key' | 'known_hosts' | { ssh_key: 'key', known_hosts: 'known_hosts' } + true | true | 'key' | nil | { ssh_key: 'key' } + true | true | nil | 'known_hosts' | { known_hosts: 'known_hosts' } + true | true | nil | nil | {} + true | true | '' | '' | {} + end + + with_them do + let(:ssh_auth) do + double( + :ssh_auth, + ssh_import?: ssh_import, + ssh_key_auth?: ssh_key_auth, + ssh_private_key: ssh_private_key, + ssh_known_hosts: ssh_known_hosts + ) + end + + it do + expected_request = gitaly_request_with_params({ + remote: remote, + ssh_key: '', + known_hosts: '', + force: false, + no_tags: false, + no_prune: false + }.update(expected_params)) + + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:fetch_remote) + .with(expected_request, kind_of(Hash)) + .and_return(double(value: true)) + + client.fetch_remote(remote, ssh_auth: ssh_auth, forced: false, no_tags: false, timeout: 1) + end + end end end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index b1b7c427313..6ce9d515a0f 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -498,126 +498,6 @@ describe Gitlab::Shell do end end - describe '#fetch_remote' do - def fetch_remote(ssh_auth = nil, prune = true) - gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth, prune: prune) - end - - def expect_call(fail, options = {}) - receive_fetch_remote = - if fail - receive(:fetch_remote).and_raise(GRPC::NotFound) - else - receive(:fetch_remote).and_return(true) - end - - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive_fetch_remote - end - - def build_ssh_auth(opts = {}) - defaults = { - ssh_import?: true, - ssh_key_auth?: false, - ssh_known_hosts: nil, - ssh_private_key: nil - } - - double(:ssh_auth, defaults.merge(opts)) - end - - it 'returns true when the command succeeds' do - expect_call(false, force: false, tags: true, prune: true) - - expect(fetch_remote).to be_truthy - end - - it 'returns true when the command succeeds' do - expect_call(false, force: false, tags: true, prune: false) - - expect(fetch_remote(nil, false)).to be_truthy - end - - it 'raises an exception when the command fails' do - expect_call(true, force: false, tags: true, prune: true) - - expect { fetch_remote }.to raise_error(Gitlab::Shell::Error) - end - - it 'allows forced and no_tags to be changed' do - expect_call(false, force: true, tags: false, prune: true) - - result = gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', forced: true, no_tags: true, prune: true) - expect(result).to be_truthy - end - - context 'SSH auth' do - it 'passes the SSH key if specified' do - expect_call(false, force: false, tags: true, prune: true, ssh_key: 'foo') - - ssh_auth = build_ssh_auth(ssh_key_auth?: true, ssh_private_key: 'foo') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'does not pass an empty SSH key' do - expect_call(false, force: false, tags: true, prune: true) - - ssh_auth = build_ssh_auth(ssh_key_auth: true, ssh_private_key: '') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'does not pass the key unless SSH key auth is to be used' do - expect_call(false, force: false, tags: true, prune: true) - - ssh_auth = build_ssh_auth(ssh_key_auth: false, ssh_private_key: 'foo') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'passes the known_hosts data if specified' do - expect_call(false, force: false, tags: true, prune: true, known_hosts: 'foo') - - ssh_auth = build_ssh_auth(ssh_known_hosts: 'foo') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'does not pass empty known_hosts data' do - expect_call(false, force: false, tags: true, prune: true) - - ssh_auth = build_ssh_auth(ssh_known_hosts: '') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - - it 'does not pass known_hosts data unless SSH is to be used' do - expect_call(false, force: false, tags: true, prune: true) - - ssh_auth = build_ssh_auth(ssh_import?: false, ssh_known_hosts: 'foo') - - expect(fetch_remote(ssh_auth)).to be_truthy - end - end - - context 'gitaly call' do - let(:remote_name) { 'remote-name' } - let(:ssh_auth) { double(:ssh_auth) } - - subject do - gitlab_shell.fetch_remote(repository.raw_repository, remote_name, - forced: true, no_tags: true, ssh_auth: ssh_auth) - end - - it 'passes the correct params to the gitaly service' do - expect(repository.gitaly_repository_client).to receive(:fetch_remote) - .with(remote_name, ssh_auth: ssh_auth, forced: true, no_tags: true, prune: true, timeout: timeout) - - subject - end - end - end - describe '#import_repository' do let(:import_url) { 'https://gitlab.com/gitlab-org/gitlab-ce.git' } diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb index 194cae8c645..eceacac58af 100644 --- a/spec/lib/gitlab/slash_commands/command_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::SlashCommands::Command do let!(:build) { create(:ci_build, pipeline: pipeline) } let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:staging) { create(:environment, name: 'staging', project: project) } - let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: staging, deployable: build) } let!(:manual) do create(:ci_build, :manual, pipeline: pipeline, diff --git a/spec/lib/gitlab/slash_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb index 0d57334aa4c..25f3e8a0409 100644 --- a/spec/lib/gitlab/slash_commands/deploy_spec.rb +++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb @@ -31,7 +31,7 @@ describe Gitlab::SlashCommands::Deploy do let!(:staging) { create(:environment, name: 'staging', project: project) } let!(:pipeline) { create(:ci_pipeline, project: project) } let!(:build) { create(:ci_build, pipeline: pipeline) } - let!(:deployment) { create(:deployment, environment: staging, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: staging, deployable: build) } context 'without actions' do it 'does not execute an action' do diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb index becb71cf427..4af51217031 100644 --- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb +++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb @@ -65,6 +65,21 @@ describe DeleteInconsistentInternalIdRecords, :migration do context 'for deployments' do let(:scope) { :deployment } + let(:deployments) { table(:deployments) } + let(:internal_ids) { table(:internal_ids) } + + before do + internal_ids.create!(project_id: project1.id, usage: 2, last_value: 2) + internal_ids.create!(project_id: project2.id, usage: 2, last_value: 2) + internal_ids.create!(project_id: project3.id, usage: 2, last_value: 2) + end + + let(:create_models) do + 3.times { |i| deployments.create!(project_id: project1.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) } + 3.times { |i| deployments.create!(project_id: project2.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) } + 3.times { |i| deployments.create!(project_id: project3.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) } + end + it_behaves_like 'deleting inconsistent internal_id records' end diff --git a/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb b/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb new file mode 100644 index 00000000000..cf5c10f77e1 --- /dev/null +++ b/spec/migrations/fill_empty_finished_at_in_deployments_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20181030135124_fill_empty_finished_at_in_deployments') + +describe FillEmptyFinishedAtInDeployments, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:environments) { table(:environments) } + let(:deployments) { table(:deployments) } + + context 'when a deployment row does not have a value on finished_at' do + context 'when a deployment succeeded' do + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1) + deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false) + end + + it 'correctly replicates finished_at by created_at' do + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to be_nil + + migrate! + + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to eq(deployments.last.created_at) + end + end + + context 'when a deployment is running' do + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1) + deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false, status: 1) + end + + it 'does not fill finished_at' do + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to be_nil + + migrate! + + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to be_nil + end + end + end + + context 'when a deployment row does has a value on finished_at' do + let(:finished_at) { '2018-10-30 11:12:02 UTC' } + + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + environments.create!(id: 1, name: 'production', slug: 'production', project_id: 1) + deployments.create!(id: 1, iid: 1, project_id: 1, environment_id: 1, ref: 'master', sha: 'xxx', tag: false, finished_at: finished_at) + end + + it 'does not affect existing value' do + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).not_to be_nil + + migrate! + + expect(deployments.last.created_at).not_to be_nil + expect(deployments.last.finished_at).to eq(finished_at) + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4089f099fdf..2e65a6a2a0f 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -17,8 +17,8 @@ describe Ci::Build do it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } - it { is_expected.to have_many(:deployments) } it { is_expected.to have_many(:trace_sections)} + it { is_expected.to have_one(:deployment) } it { is_expected.to have_one(:runner_session)} it { is_expected.to validate_presence_of(:ref) } it { is_expected.to respond_to(:has_trace?) } @@ -799,17 +799,100 @@ describe Ci::Build do end end + describe 'state transition as a deployable' do + let!(:build) { create(:ci_build, :start_review_app) } + let(:deployment) { build.deployment } + let(:environment) { deployment.environment } + + it 'has deployments record with created status' do + expect(deployment).to be_created + expect(environment.name).to eq('review/master') + end + + context 'when transits to running' do + before do + build.run! + end + + it 'transits deployment status to running' do + expect(deployment).to be_running + end + end + + context 'when transits to success' do + before do + allow(Deployments::SuccessWorker).to receive(:perform_async) + build.success! + end + + it 'transits deployment status to success' do + expect(deployment).to be_success + end + end + + context 'when transits to failed' do + before do + build.drop! + end + + it 'transits deployment status to failed' do + expect(deployment).to be_failed + end + end + + context 'when transits to skipped' do + before do + build.skip! + end + + it 'transits deployment status to canceled' do + expect(deployment).to be_canceled + end + end + + context 'when transits to canceled' do + before do + build.cancel! + end + + it 'transits deployment status to canceled' do + expect(deployment).to be_canceled + end + end + end + + describe '#on_stop' do + subject { build.on_stop } + + context 'when a job has a specification that it can be stopped from the other job' do + let(:build) { create(:ci_build, :start_review_app) } + + it 'returns the other job name' do + is_expected.to eq('stop_review_app') + end + end + + context 'when a job does not have environment information' do + let(:build) { create(:ci_build) } + + it 'returns nil' do + is_expected.to be_nil + end + end + end + describe 'deployment' do - describe '#last_deployment' do - subject { build.last_deployment } + describe '#has_deployment?' do + subject { build.has_deployment? } + + context 'when build has a deployment' do + let!(:deployment) { create(:deployment, deployable: build) } - context 'when multiple deployments are created' do - let!(:deployment1) { create(:deployment, deployable: build) } - let!(:deployment2) { create(:deployment, deployable: build) } + it { is_expected.to be_truthy } + end - it 'returns the latest one' do - is_expected.to eq(deployment2) - end + context 'when build does not have a deployment' do + it { is_expected.to be_falsy } end end @@ -818,14 +901,14 @@ describe Ci::Build do context 'when build succeeded' do let(:build) { create(:ci_build, :success) } - let!(:deployment) { create(:deployment, deployable: build) } + let!(:deployment) { create(:deployment, :success, deployable: build) } context 'current deployment is latest' do it { is_expected.to be_falsey } end context 'current deployment is not latest on environment' do - let!(:deployment2) { create(:deployment, environment: deployment.environment) } + let!(:deployment2) { create(:deployment, :success, environment: deployment.environment) } it { is_expected.to be_truthy } end @@ -3209,10 +3292,14 @@ describe Ci::Build do end describe '#deployment_status' do + before do + allow_any_instance_of(described_class).to receive(:create_deployment) + end + context 'when build is a last deployment' do let(:build) { create(:ci_build, :success, environment: 'production') } let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } it { expect(build.deployment_status).to eq(:last) } end @@ -3220,8 +3307,8 @@ describe Ci::Build do context 'when there is a newer build with deployment' do let(:build) { create(:ci_build, :success, environment: 'production') } let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } - let!(:last_deployment) { create(:deployment, environment: environment, project: environment.project) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } + let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) } it { expect(build.deployment_status).to eq(:out_of_date) } end @@ -3229,7 +3316,7 @@ describe Ci::Build do context 'when build with deployment has failed' do let(:build) { create(:ci_build, :failed, environment: 'production') } let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } it { expect(build.deployment_status).to eq(:failed) } end @@ -3237,14 +3324,7 @@ describe Ci::Build do context 'when build with deployment is running' do let(:build) { create(:ci_build, environment: 'production') } let(:environment) { create(:environment, name: 'production', project: build.project) } - let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } - - it { expect(build.deployment_status).to eq(:creating) } - end - - context 'when build is successful but deployment is not ready yet' do - let(:build) { create(:ci_build, :success, environment: 'production') } - let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) } it { expect(build.deployment_status).to eq(:creating) } end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 153244b2159..9e6146b8a44 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1043,6 +1043,11 @@ describe Ci::Pipeline, :mailer do expect(described_class.newest_first.pluck(:status)) .to eq(%w[skipped failed success canceled]) end + + it 'searches limited backlog' do + expect(described_class.newest_first(limit: 1).pluck(:status)) + .to eq(%w[skipped]) + end end describe '.latest_status' do @@ -1148,6 +1153,19 @@ describe Ci::Pipeline, :mailer do end end + describe '.latest_successful_ids_per_project' do + let(:projects) { create_list(:project, 2) } + let!(:pipeline1) { create(:ci_pipeline, :success, project: projects[0]) } + let!(:pipeline2) { create(:ci_pipeline, :success, project: projects[0]) } + let!(:pipeline3) { create(:ci_pipeline, :failed, project: projects[0]) } + let!(:pipeline4) { create(:ci_pipeline, :success, project: projects[1]) } + + it 'returns expected pipeline ids' do + expect(described_class.latest_successful_ids_per_project) + .to contain_exactly(pipeline2, pipeline4) + end + end + describe '.internal_sources' do subject { described_class.internal_sources } diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index debc02fa51f..5713106418d 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -37,8 +37,8 @@ describe Awardable do create(:award_emoji, awardable: issue3, name: "star", user: award_emoji.user) create(:award_emoji, awardable: issue3, name: "star", user: award_emoji2.user) - expect(Issue.awarded(award_emoji.user)).to eq [issue, issue3] - expect(Issue.awarded(award_emoji2.user)).to eq [issue2, issue3] + expect(Issue.awarded(award_emoji.user)).to contain_exactly(issue, issue3) + expect(Issue.awarded(award_emoji2.user)).to contain_exactly(issue2, issue3) end end diff --git a/spec/models/concerns/deployable_spec.rb b/spec/models/concerns/deployable_spec.rb new file mode 100644 index 00000000000..ac79c75a55e --- /dev/null +++ b/spec/models/concerns/deployable_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe Deployable do + describe '#create_deployment' do + let(:deployment) { job.deployment } + let(:environment) { deployment&.environment } + + before do + job.reload + end + + context 'when the deployable object will deploy to production' do + let!(:job) { create(:ci_build, :start_review_app) } + + it 'creates a deployment and environment record' do + expect(deployment.project).to eq(job.project) + expect(deployment.ref).to eq(job.ref) + expect(deployment.tag).to eq(job.tag) + expect(deployment.sha).to eq(job.sha) + expect(deployment.user).to eq(job.user) + expect(deployment.deployable).to eq(job) + expect(deployment.on_stop).to eq('stop_review_app') + expect(environment.name).to eq('review/master') + end + end + + context 'when the deployable object will stop an environment' do + let!(:job) { create(:ci_build, :stop_review_app) } + + it 'does not create a deployment record' do + expect(deployment).to be_nil + end + end + + context 'when the deployable object has already had a deployment' do + let!(:job) { create(:ci_build, :start_review_app, deployment: race_deployment) } + let!(:race_deployment) { create(:deployment, :success) } + + it 'does not create a new deployment' do + expect(deployment).to eq(race_deployment) + end + end + + context 'when the deployable object will not deploy' do + let!(:job) { create(:ci_build) } + + it 'does not create a deployment and environment record' do + expect(deployment).to be_nil + expect(environment).to be_nil + end + end + end +end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index 146d35122f7..270b2767c68 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -42,16 +42,174 @@ describe Deployment do end end - describe 'after_create callbacks' do - let(:environment) { create(:environment) } - let(:store) { Gitlab::EtagCaching::Store.new } + describe '.success' do + subject { described_class.success } - it 'invalidates the environment etag cache' do - old_value = store.get(environment.etag_cache_key) + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } - create(:deployment, environment: environment) + it { is_expected.to eq([deployment]) } + end + + context 'when deployment status is created' do + let(:deployment) { create(:deployment, :created) } + + it { is_expected.to be_empty } + end + + context 'when deployment status is running' do + let(:deployment) { create(:deployment, :running) } + + it { is_expected.to be_empty } + end + end + + describe 'state machine' do + context 'when deployment runs' do + let(:deployment) { create(:deployment) } + + before do + deployment.run! + end + + it 'starts running' do + Timecop.freeze do + expect(deployment).to be_running + expect(deployment.finished_at).to be_nil + end + end + end + + context 'when deployment succeeded' do + let(:deployment) { create(:deployment, :running) } + + it 'has correct status' do + Timecop.freeze do + deployment.succeed! + + expect(deployment).to be_success + expect(deployment.finished_at).to be_like_time(Time.now) + end + end + + it 'executes Deployments::SuccessWorker asynchronously' do + expect(Deployments::SuccessWorker) + .to receive(:perform_async).with(deployment.id) - expect(store.get(environment.etag_cache_key)).not_to eq(old_value) + deployment.succeed! + end + end + + context 'when deployment failed' do + let(:deployment) { create(:deployment, :running) } + + it 'has correct status' do + Timecop.freeze do + deployment.drop! + + expect(deployment).to be_failed + expect(deployment.finished_at).to be_like_time(Time.now) + end + end + end + + context 'when deployment was canceled' do + let(:deployment) { create(:deployment, :running) } + + it 'has correct status' do + Timecop.freeze do + deployment.cancel! + + expect(deployment).to be_canceled + expect(deployment.finished_at).to be_like_time(Time.now) + end + end + end + end + + describe '#success?' do + subject { deployment.success? } + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to be_truthy } + end + + context 'when deployment status is failed' do + let(:deployment) { create(:deployment, :failed) } + + it { is_expected.to be_falsy } + end + end + + describe '#status_name' do + subject { deployment.status_name } + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to eq(:success) } + end + + context 'when deployment status is failed' do + let(:deployment) { create(:deployment, :failed) } + + it { is_expected.to eq(:failed) } + end + end + + describe '#finished_at' do + subject { deployment.finished_at } + + context 'when deployment status is created' do + let(:deployment) { create(:deployment) } + + it { is_expected.to be_nil } + end + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to eq(deployment.read_attribute(:finished_at)) } + end + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success, finished_at: nil) } + + before do + deployment.update_column(:finished_at, nil) + end + + it { is_expected.to eq(deployment.read_attribute(:created_at)) } + end + + context 'when deployment status is running' do + let(:deployment) { create(:deployment, :running) } + + it { is_expected.to be_nil } + end + end + + describe '#deployed_at' do + subject { deployment.deployed_at } + + context 'when deployment status is created' do + let(:deployment) { create(:deployment) } + + it { is_expected.to be_nil } + end + + context 'when deployment status is success' do + let(:deployment) { create(:deployment, :success) } + + it { is_expected.to eq(deployment.read_attribute(:finished_at)) } + end + + context 'when deployment status is running' do + let(:deployment) { create(:deployment, :running) } + + it { is_expected.to be_nil } end end @@ -112,7 +270,7 @@ describe Deployment do end describe '#metrics' do - let(:deployment) { create(:deployment) } + let(:deployment) { create(:deployment, :success) } let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) } subject { deployment.metrics } @@ -141,7 +299,7 @@ describe Deployment do describe '#additional_metrics' do let(:project) { create(:project, :repository) } - let(:deployment) { create(:deployment, project: project) } + let(:deployment) { create(:deployment, :succeed, project: project) } subject { deployment.additional_metrics } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 1de95d881a7..e121369f6ac 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -95,7 +95,7 @@ describe Environment do context 'with a last deployment' do let!(:deployment) do - create(:deployment, environment: environment, sha: project.commit('master').id) + create(:deployment, :success, environment: environment, sha: project.commit('master').id) end context 'in the same branch' do @@ -136,8 +136,8 @@ describe Environment do describe '#first_deployment_for' do let(:project) { create(:project, :repository) } - let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } - let!(:deployment1) { create(:deployment, environment: environment, ref: commit.id) } + let!(:deployment) { create(:deployment, :succeed, environment: environment, ref: commit.parent.id) } + let!(:deployment1) { create(:deployment, :succeed, environment: environment, ref: commit.id) } let(:head_commit) { project.commit } let(:commit) { project.commit.parent } @@ -181,7 +181,8 @@ describe Environment do let(:build) { create(:ci_build) } let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, on_stop: 'close_app') end @@ -249,7 +250,8 @@ describe Environment do let(:build) { create(:ci_build, pipeline: pipeline) } let!(:deployment) do - create(:deployment, environment: environment, + create(:deployment, :success, + environment: environment, deployable: build, on_stop: 'close_app') end @@ -304,7 +306,7 @@ describe Environment do context 'when last deployment to environment is the most recent one' do before do - create(:deployment, environment: environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'feature') end it { is_expected.to be true } @@ -312,8 +314,8 @@ describe Environment do context 'when last deployment to environment is not the most recent' do before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: environment, ref: 'master') + create(:deployment, :success, environment: environment, ref: 'feature') + create(:deployment, :success, environment: environment, ref: 'master') end it { is_expected.to be false } @@ -321,7 +323,7 @@ describe Environment do end describe '#actions_for' do - let(:deployment) { create(:deployment, environment: environment) } + let(:deployment) { create(:deployment, :success, environment: environment) } let(:pipeline) { deployment.deployable.pipeline } let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' )} let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )} @@ -331,6 +333,70 @@ describe Environment do end end + describe '.deployments' do + subject { environment.deployments } + + context 'when there is a deployment record with created status' do + let(:deployment) { create(:deployment, :created, environment: environment) } + + it 'does not return the record' do + is_expected.to be_empty + end + end + + context 'when there is a deployment record with running status' do + let(:deployment) { create(:deployment, :running, environment: environment) } + + it 'does not return the record' do + is_expected.to be_empty + end + end + + context 'when there is a deployment record with success status' do + let(:deployment) { create(:deployment, :success, environment: environment) } + + it 'returns the record' do + is_expected.to eq([deployment]) + end + end + end + + describe '.last_deployment' do + subject { environment.last_deployment } + + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end + + context 'when there is an old deployment record' do + let!(:previous_deployment) { create(:deployment, :success, environment: environment) } + + context 'when there is a deployment record with created status' do + let!(:deployment) { create(:deployment, environment: environment) } + + it 'returns the previous deployment' do + is_expected.to eq(previous_deployment) + end + end + + context 'when there is a deployment record with running status' do + let!(:deployment) { create(:deployment, :running, environment: environment) } + + it 'returns the previous deployment' do + is_expected.to eq(previous_deployment) + end + end + + context 'when there is a deployment record with success status' do + let!(:deployment) { create(:deployment, :success, environment: environment) } + + it 'returns the latest successful deployment' do + is_expected.to eq(deployment) + end + end + end + end + describe '#has_terminals?' do subject { environment.has_terminals? } @@ -338,7 +404,7 @@ describe Environment do context 'with a deployment service' do shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do context 'and a deployment' do - let!(:deployment) { create(:deployment, environment: environment) } + let!(:deployment) { create(:deployment, :success, environment: environment) } it { is_expected.to be_truthy } end diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index e7805d52d75..52b98552184 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe EnvironmentStatus do - let(:deployment) { create(:deployment, :review_app) } + let(:deployment) { create(:deployment, :succeed, :review_app) } let(:environment) { deployment.environment} let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } @@ -12,7 +12,7 @@ describe EnvironmentStatus do it { is_expected.to delegate_method(:id).to(:environment) } it { is_expected.to delegate_method(:name).to(:environment) } it { is_expected.to delegate_method(:project).to(:environment) } - it { is_expected.to delegate_method(:deployed_at).to(:deployment).as(:created_at) } + it { is_expected.to delegate_method(:deployed_at).to(:deployment) } it { is_expected.to delegate_method(:status).to(:deployment) } describe '#project' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 2eb5e39ccfd..3a54725c7ec 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1836,8 +1836,8 @@ describe MergeRequest do let(:environments) { create_list(:environment, 3, project: project) } before do - create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id) - create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id) + create(:deployment, :success, environment: environments.first, ref: 'master', sha: project.commit('master').id) + create(:deployment, :success, environment: environments.second, ref: 'feature', sha: project.commit('feature').id) end it 'selects deployed environments' do @@ -1857,7 +1857,7 @@ describe MergeRequest do let(:source_environment) { create(:environment, project: source_project) } before do - create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha) + create(:deployment, :success, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha) end it 'selects deployed environments' do @@ -1868,7 +1868,7 @@ describe MergeRequest do let(:target_environment) { create(:environment, project: project) } before do - create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha) + create(:deployment, :success, environment: target_environment, tag: true, sha: merge_request.diff_head_sha) end it 'selects deployed environments' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 8913644a3ce..2db42fe802a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -562,6 +562,17 @@ describe Namespace do it { expect(group.all_projects.to_a).to match_array([project2, project1]) } end + describe '#all_pipelines' do + let(:group) { create(:group) } + let(:child) { create(:group, parent: group) } + let!(:project1) { create(:project_empty_repo, namespace: group) } + let!(:project2) { create(:project_empty_repo, namespace: child) } + let!(:pipeline1) { create(:ci_empty_pipeline, project: project1) } + let!(:pipeline2) { create(:ci_empty_pipeline, project: project2) } + + it { expect(group.all_pipelines.to_a).to match_array([pipeline1, pipeline2]) } + end + describe '#share_with_group_lock with subgroups', :nested_groups do context 'when creating a subgroup' do let(:subgroup) { create(:group, parent: root_group )} diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 84326724118..f020557e4af 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3976,6 +3976,40 @@ describe Project do end end + describe '.deployments' do + subject { project.deployments } + + let(:project) { create(:project) } + + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end + + context 'when there is a deployment record with created status' do + let(:deployment) { create(:deployment, :created, project: project) } + + it 'does not return the record' do + is_expected.to be_empty + end + end + + context 'when there is a deployment record with running status' do + let(:deployment) { create(:deployment, :running, project: project) } + + it 'does not return the record' do + is_expected.to be_empty + end + end + + context 'when there is a deployment record with success status' do + let(:deployment) { create(:deployment, :success, project: project) } + + it 'returns the record' do + is_expected.to eq([deployment]) + end + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 61ae053cea7..3dac7225b7a 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -10,9 +10,9 @@ describe API::Deployments do describe 'GET /projects/:id/deployments' do let(:project) { create(:project) } - let!(:deployment_1) { create(:deployment, project: project, iid: 11, ref: 'master', created_at: Time.now) } - let!(:deployment_2) { create(:deployment, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) } - let!(:deployment_3) { create(:deployment, project: project, iid: 8, ref: 'feature', created_at: 2.days.ago) } + let!(:deployment_1) { create(:deployment, :success, project: project, iid: 11, ref: 'master', created_at: Time.now) } + let!(:deployment_2) { create(:deployment, :success, project: project, iid: 12, ref: 'feature', created_at: 1.day.ago) } + let!(:deployment_3) { create(:deployment, :success, project: project, iid: 8, ref: 'patch', created_at: 2.days.ago) } context 'as member of the project' do it 'returns projects deployments sorted by id asc' do @@ -53,8 +53,8 @@ describe API::Deployments do 'id' | 'desc' | [:deployment_3, :deployment_2, :deployment_1] 'iid' | 'asc' | [:deployment_3, :deployment_1, :deployment_2] 'iid' | 'desc' | [:deployment_2, :deployment_1, :deployment_3] - 'ref' | 'asc' | [:deployment_2, :deployment_3, :deployment_1] - 'ref' | 'desc' | [:deployment_1, :deployment_2, :deployment_3] + 'ref' | 'asc' | [:deployment_2, :deployment_1, :deployment_3] + 'ref' | 'desc' | [:deployment_3, :deployment_1, :deployment_2] end with_them do @@ -76,7 +76,7 @@ describe API::Deployments do describe 'GET /projects/:id/deployments/:deployment_id' do let(:project) { deployment.environment.project } - let!(:deployment) { create(:deployment) } + let!(:deployment) { create(:deployment, :success) } context 'as a member of the project' do it 'returns the projects deployment' do diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb index 0f0ab5ac796..87493a28d1f 100644 --- a/spec/serializers/environment_serializer_spec.rb +++ b/spec/serializers/environment_serializer_spec.rb @@ -14,7 +14,8 @@ describe EnvironmentSerializer do let(:project) { create(:project, :repository) } let(:deployable) { create(:ci_build) } let(:deployment) do - create(:deployment, deployable: deployable, + create(:deployment, :success, + deployable: deployable, user: user, project: project, sha: project.commit.id) diff --git a/spec/serializers/environment_status_entity_spec.rb b/spec/serializers/environment_status_entity_spec.rb index 1b4d8b70aa6..962ec919092 100644 --- a/spec/serializers/environment_status_entity_spec.rb +++ b/spec/serializers/environment_status_entity_spec.rb @@ -4,8 +4,8 @@ describe EnvironmentStatusEntity do let(:user) { create(:user) } let(:request) { double('request') } - let(:deployment) { create(:deployment, :review_app) } - let(:environment) { deployment.environment} + let(:deployment) { create(:deployment, :succeed, :review_app) } + let(:environment) { deployment.environment } let(:project) { deployment.project } let(:merge_request) { create(:merge_request, :deployed_review_app, deployment: deployment) } diff --git a/spec/serializers/issue_board_entity_spec.rb b/spec/serializers/issue_board_entity_spec.rb new file mode 100644 index 00000000000..06d9d3657e6 --- /dev/null +++ b/spec/serializers/issue_board_entity_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe IssueBoardEntity do + let(:project) { create(:project) } + let(:resource) { create(:issue, project: project) } + let(:user) { create(:user) } + + let(:request) { double('request', current_user: user) } + + subject { described_class.new(resource, request: request).as_json } + + it 'has basic attributes' do + expect(subject).to include(:id, :iid, :title, :confidential, :due_date, :project_id, :relative_position, + :project, :labels) + end + + it 'has path and endpoints' do + expect(subject).to include(:reference_path, :real_path, :issue_sidebar_endpoint, + :toggle_subscription_endpoint, :assignable_labels_endpoint) + end +end diff --git a/spec/serializers/issue_serializer_spec.rb b/spec/serializers/issue_serializer_spec.rb index 75578816e75..e8c46c0cdee 100644 --- a/spec/serializers/issue_serializer_spec.rb +++ b/spec/serializers/issue_serializer_spec.rb @@ -24,4 +24,12 @@ describe IssueSerializer do expect(json_entity).to match_schema('entities/issue_sidebar') end end + + context 'board issue serialization' do + let(:serializer) { 'board' } + + it 'matches board issue json schema' do + expect(json_entity).to match_schema('entities/issue_board') + end + end end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 368abded448..e779675744c 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -32,7 +32,7 @@ describe Ci::RetryBuildService do IGNORE_ACCESSORS = %i[type lock_version target_url base_tags trace_sections - commit_id deployments erased_by_id last_deployment project_id + commit_id deployment erased_by_id project_id runner_id tag_taggings taggings tags trigger_request_id user_id auto_canceled_by_id retried failure_reason artifacts_file_store artifacts_metadata_store diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb deleted file mode 100644 index b9bfbb11511..00000000000 --- a/spec/services/create_deployment_service_spec.rb +++ /dev/null @@ -1,335 +0,0 @@ -require 'spec_helper' - -describe CreateDeploymentService do - let(:user) { create(:user) } - let(:options) { nil } - - let(:job) do - create(:ci_build, - ref: 'master', - tag: false, - environment: 'production', - options: { environment: options }) - end - - let(:project) { job.project } - - let!(:environment) do - create(:environment, project: project, name: 'production') - end - - let(:service) { described_class.new(job) } - - before do - allow_any_instance_of(Deployment).to receive(:create_ref) - end - - describe '#execute' do - subject { service.execute } - - context 'when environment exists' do - it 'creates a deployment' do - expect(subject).to be_persisted - end - end - - context 'when environment does not exist' do - let(:environment) {} - - it 'does not create a deployment' do - expect do - expect(subject).to be_nil - end.not_to change { Deployment.count } - end - end - - context 'when start action is defined' do - let(:options) { { action: 'start' } } - - context 'and environment is stopped' do - before do - environment.stop - end - - it 'makes environment available' do - subject - - expect(environment.reload).to be_available - end - - it 'creates a deployment' do - expect(subject).to be_persisted - end - end - end - - context 'when stop action is defined' do - let(:options) { { action: 'stop' } } - - context 'and environment is available' do - before do - environment.start - end - - it 'makes environment stopped' do - subject - - expect(environment.reload).to be_stopped - end - - it 'does not create a deployment' do - expect(subject).to be_nil - end - end - end - - context 'when variables are used' do - let(:options) do - { name: 'review-apps/$CI_COMMIT_REF_NAME', - url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' } - end - - before do - environment.update(name: 'review-apps/master') - job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME') - end - - it 'creates a new deployment' do - expect(subject).to be_persisted - end - - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end - - it 'updates external url' do - subject - - expect(subject.environment.name).to eq('review-apps/master') - expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') - end - end - - context 'when project was removed' do - let(:environment) {} - - before do - job.update(project: nil) - end - - it 'does not create deployment or environment' do - expect { subject }.not_to raise_error - - expect(Environment.count).to be_zero - expect(Deployment.count).to be_zero - end - end - end - - describe '#expanded_environment_url' do - subject { service.send(:expanded_environment_url) } - - context 'when yaml environment uses $CI_COMMIT_REF_NAME' do - let(:job) do - create(:ci_build, - ref: 'master', - options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } }) - end - - it { is_expected.to eq('http://review/master') } - end - - context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do - let(:job) do - create(:ci_build, - ref: 'master', - environment: 'production', - options: { environment: { url: 'http://review/$CI_ENVIRONMENT_SLUG' } }) - end - - let!(:environment) do - create(:environment, - project: job.project, - name: 'production', - slug: 'prod-slug', - external_url: 'http://review/old') - end - - it { is_expected.to eq('http://review/prod-slug') } - end - - context 'when yaml environment uses yaml_variables containing symbol keys' do - let(:job) do - create(:ci_build, - yaml_variables: [{ key: :APP_HOST, value: 'host' }], - options: { environment: { url: 'http://review/$APP_HOST' } }) - end - - it { is_expected.to eq('http://review/host') } - end - - context 'when yaml environment does not have url' do - let(:job) { create(:ci_build, environment: 'staging') } - - let!(:environment) do - create(:environment, project: job.project, name: job.environment) - end - - it 'returns the external_url from persisted environment' do - is_expected.to be_nil - end - end - end - - describe 'processing of builds' do - shared_examples 'does not create deployment' do - it 'does not create a new deployment' do - expect { subject }.not_to change { Deployment.count } - end - - it 'does not call a service' do - expect_any_instance_of(described_class).not_to receive(:execute) - - subject - end - end - - shared_examples 'creates deployment' do - it 'creates a new deployment' do - expect { subject }.to change { Deployment.count }.by(1) - end - - it 'calls a service' do - expect_any_instance_of(described_class).to receive(:execute) - - subject - end - - it 'is set as deployable' do - subject - - expect(Deployment.last.deployable).to eq(deployable) - end - - it 'updates environment URL' do - subject - - expect(Deployment.last.environment.external_url).not_to be_nil - end - end - - context 'without environment specified' do - let(:job) { create(:ci_build) } - - it_behaves_like 'does not create deployment' do - subject { job.success } - end - end - - context 'when environment is specified' do - let(:deployable) { job } - - let(:options) do - { environment: { name: 'production', url: 'http://gitlab.com' } } - end - - context 'when job succeeds' do - it_behaves_like 'creates deployment' do - subject { job.success } - end - end - - context 'when job fails' do - it_behaves_like 'does not create deployment' do - subject { job.drop } - end - end - - context 'when job is retried' do - it_behaves_like 'creates deployment' do - before do - stub_not_protect_default_branch - - project.add_developer(user) - end - - let(:deployable) { Ci::Build.retry(job, user) } - - subject { deployable.success } - end - end - end - end - - describe "merge request metrics" do - let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } - - context "while updating the 'first_deployed_to_production_at' time" do - before do - merge_request.metrics.update!(merged_at: Time.now) - end - - context "for merge requests merged before the current deploy" do - it "sets the time if the deploy's environment is 'production'" do - time = Time.now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - end - - it "doesn't set the time if the deploy's environment is not 'production'" do - job.update(environment: 'staging') - service = described_class.new(job) - service.execute - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - end - - it 'does not raise errors if the merge request does not have a metrics record' do - merge_request.metrics.destroy - - expect(merge_request.reload.metrics).to be_nil - expect { service.execute }.not_to raise_error - end - end - - context "for merge requests merged before the previous deploy" do - context "if the 'first_deployed_to_production_at' time is already set" do - it "does not overwrite the older 'first_deployed_to_production_at' time" do - # Previous deploy - time = Time.now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - - # Current deploy - service = described_class.new(job) - Timecop.freeze(time + 12.hours) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) - end - end - - context "if the 'first_deployed_to_production_at' time is not already set" do - it "does not overwrite the older 'first_deployed_to_production_at' time" do - # Previous deploy - time = 5.minutes.from_now - Timecop.freeze(time) { service.execute } - - expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at - - merge_request.reload.metrics.update(first_deployed_to_production_at: nil) - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - - # Current deploy - service = described_class.new(job) - Timecop.freeze(time + 12.hours) { service.execute } - - expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil - end - end - end - end - end -end diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb new file mode 100644 index 00000000000..3c55dd9659a --- /dev/null +++ b/spec/services/update_deployment_service_spec.rb @@ -0,0 +1,217 @@ +require 'spec_helper' + +describe UpdateDeploymentService do + let(:user) { create(:user) } + let(:options) { { name: 'production' } } + + let(:job) do + create(:ci_build, + ref: 'master', + tag: false, + environment: 'production', + options: { environment: options }, + project: project) + end + + let(:project) { create(:project, :repository) } + let(:environment) { deployment.environment } + let(:deployment) { job.deployment } + let(:service) { described_class.new(deployment) } + + before do + job.success! # Create/Succeed deployment + end + + describe '#execute' do + subject { service.execute } + + let(:store) { Gitlab::EtagCaching::Store.new } + + it 'invalidates the environment etag cache' do + old_value = store.get(environment.etag_cache_key) + + subject + + expect(store.get(environment.etag_cache_key)).not_to eq(old_value) + end + + it 'creates ref' do + expect_any_instance_of(Repository) + .to receive(:create_ref) + .with(deployment.ref, deployment.send(:ref_path)) + + subject + end + + it 'updates merge request metrics' do + expect_any_instance_of(Deployment) + .to receive(:update_merge_request_metrics!) + + subject + end + + context 'when start action is defined' do + let(:options) { { name: 'production', action: 'start' } } + + context 'and environment is stopped' do + before do + environment.stop + end + + it 'makes environment available' do + subject + + expect(environment.reload).to be_available + end + end + end + + context 'when variables are used' do + let(:options) do + { name: 'review-apps/$CI_COMMIT_REF_NAME', + url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' } + end + + before do + environment.update(name: 'review-apps/master') + job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME') + end + + it 'does not create a new environment' do + expect { subject }.not_to change { Environment.count } + end + + it 'updates external url' do + subject + + expect(subject.environment.name).to eq('review-apps/master') + expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') + end + end + end + + describe '#expanded_environment_url' do + subject { service.send(:expanded_environment_url) } + + context 'when yaml environment uses $CI_COMMIT_REF_NAME' do + let(:job) do + create(:ci_build, + ref: 'master', + environment: 'production', + project: project, + options: { environment: { name: 'production', url: 'http://review/$CI_COMMIT_REF_NAME' } }) + end + + it { is_expected.to eq('http://review/master') } + end + + context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do + let(:job) do + create(:ci_build, + ref: 'master', + environment: 'prod-slug', + project: project, + options: { environment: { name: 'prod-slug', url: 'http://review/$CI_ENVIRONMENT_SLUG' } }) + end + + it { is_expected.to eq('http://review/prod-slug') } + end + + context 'when yaml environment uses yaml_variables containing symbol keys' do + let(:job) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + environment: 'production', + project: project, + options: { environment: { name: 'production', url: 'http://review/$APP_HOST' } }) + end + + it { is_expected.to eq('http://review/host') } + end + + context 'when yaml environment does not have url' do + let(:job) { create(:ci_build, environment: 'staging', project: project) } + + it 'returns the external_url from persisted environment' do + is_expected.to be_nil + end + end + end + + describe "merge request metrics" do + let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } + + context "while updating the 'first_deployed_to_production_at' time" do + before do + merge_request.metrics.update!(merged_at: 1.hour.ago) + end + + context "for merge requests merged before the current deploy" do + it "sets the time if the deploy's environment is 'production'" do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + end + + context 'when job deploys to staging' do + let(:job) do + create(:ci_build, + ref: 'master', + tag: false, + environment: 'staging', + options: { environment: { name: 'staging' } }, + project: project) + end + + it "doesn't set the time if the deploy's environment is not 'production'" do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil + end + end + + it 'does not raise errors if the merge request does not have a metrics record' do + merge_request.metrics.destroy + + expect(merge_request.reload.metrics).to be_nil + expect { service.execute }.not_to raise_error + end + end + + context "for merge requests merged before the previous deploy" do + context "if the 'first_deployed_to_production_at' time is already set" do + it "does not overwrite the older 'first_deployed_to_production_at' time" do + # Previous deploy + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + + # Current deploy + Timecop.travel(12.hours.from_now) do + service.execute + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(deployment.finished_at) + end + end + end + + context "if the 'first_deployed_to_production_at' time is not already set" do + it "does not overwrite the older 'first_deployed_to_production_at' time" do + # Previous deploy + time = 5.minutes.from_now + Timecop.freeze(time) { service.execute } + + expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at + + previous_time = merge_request.reload.metrics.first_deployed_to_production_at + + # Current deploy + Timecop.freeze(time + 12.hours) { service.execute } + + expect(merge_request.reload.metrics.first_deployed_to_production_at).to eq(previous_time) + end + end + end + end + end +end diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 83035788a56..ecefdc23811 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -85,7 +85,7 @@ module CycleAnalyticsHelpers raise ArgumentError end - CreateDeploymentService.new(dummy_job).execute + dummy_job.success! # State machine automatically update associated deployment/environment record end def dummy_production_job(user, project) @@ -97,7 +97,7 @@ module CycleAnalyticsHelpers end def dummy_pipeline(project) - Ci::Pipeline.new( + create(:ci_pipeline, sha: project.repository.commit('master').sha, ref: 'master', source: :push, @@ -106,9 +106,7 @@ module CycleAnalyticsHelpers end def new_dummy_job(user, project, environment) - project.environments.find_or_create_by(name: environment) - - Ci::Build.new( + create(:ci_build, project: project, user: user, environment: environment, diff --git a/spec/support/shared_examples/services/boards/issues_move_service.rb b/spec/support/shared_examples/services/boards/issues_move_service.rb index 6d29a97c56d..ec44b99d10e 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service.rb @@ -34,7 +34,7 @@ shared_examples 'issues move service' do |group| described_class.new(parent, user, params).execute(issue) issue.reload - expect(issue.labels).to contain_exactly(bug) + expect(issue.labels).to contain_exactly(bug, regression) expect(issue).to be_closed end end diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb index dba70883130..5eb9709ded9 100644 --- a/spec/workers/build_success_worker_spec.rb +++ b/spec/workers/build_success_worker_spec.rb @@ -2,15 +2,39 @@ require 'spec_helper' describe BuildSuccessWorker do describe '#perform' do + subject { described_class.new.perform(build.id) } + + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end + context 'when build exists' do - context 'when build belogs to the environment' do - let!(:build) { create(:ci_build, environment: 'production') } + context 'when deployment was not created with the build creation' do # An edge case during the transition period + let!(:build) { create(:ci_build, :deploy_to_production) } + + before do + Deployment.delete_all + build.reload + end - it 'executes deployment service' do - expect_any_instance_of(CreateDeploymentService) - .to receive(:execute) + it 'creates a successful deployment' do + expect(build).not_to be_has_deployment - described_class.new.perform(build.id) + subject + + build.reload + expect(build).to be_has_deployment + expect(build.deployment).to be_success + end + end + + context 'when deployment was created with the build creation' do # Counter part of the above edge case + let!(:build) { create(:ci_build, :deploy_to_production) } + + it 'does not create a new deployment' do + expect(build).to be_has_deployment + + expect { subject }.not_to change { Deployment.count } end end @@ -18,10 +42,22 @@ describe BuildSuccessWorker do let!(:build) { create(:ci_build, project: nil) } it 'does not create deployment' do - expect_any_instance_of(CreateDeploymentService) - .not_to receive(:execute) + subject + + expect(build.reload).not_to be_has_deployment + end + end + + context 'when the build will stop an environment' do + let!(:build) { create(:ci_build, :stop_review_app, environment: environment.name, project: environment.project) } + let(:environment) { create(:environment, state: :available) } + + it 'stops the environment' do + expect(environment).to be_available + + subject - described_class.new.perform(build.id) + expect(environment.reload).to be_stopped end end end diff --git a/spec/workers/deployments/success_worker_spec.rb b/spec/workers/deployments/success_worker_spec.rb new file mode 100644 index 00000000000..ba7d45eca01 --- /dev/null +++ b/spec/workers/deployments/success_worker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Deployments::SuccessWorker do + subject { described_class.new.perform(deployment&.id) } + + context 'when successful deployment' do + let(:deployment) { create(:deployment, :success) } + + it 'executes UpdateDeploymentService' do + expect(UpdateDeploymentService) + .to receive(:new).with(deployment).and_call_original + + subject + end + end + + context 'when canceled deployment' do + let(:deployment) { create(:deployment, :canceled) } + + it 'does not execute UpdateDeploymentService' do + expect(UpdateDeploymentService).not_to receive(:new) + + subject + end + end + + context 'when deploy record does not exist' do + let(:deployment) { nil } + + it 'does not execute UpdateDeploymentService' do + expect(UpdateDeploymentService).not_to receive(:new) + + subject + end + end +end |