diff options
177 files changed, 2001 insertions, 435 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2de6c0d2cc7..ad5c194bffe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,8 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29" +include: + - local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml + .dedicated-runner: &dedicated-runner retry: 1 tags: @@ -774,31 +777,14 @@ jest: code_quality: <<: *dedicated-no-docs-no-db-pull-cache-job - image: docker:stable - allow_failure: true # gitlab-org runners set `privileged: false` but we need to have it set to true # since we're using Docker in Docker tags: [] before_script: [] - services: - - docker:stable-dind - variables: - SETUP_DB: "false" - DOCKER_DRIVER: overlay2 cache: {} dependencies: [] - script: - # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products - - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') - - docker run - --env SOURCE_CODE="$PWD" - --volume "$PWD":/code - --volume /var/run/docker.sock:/var/run/docker.sock - "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code - artifacts: - reports: - codequality: gl-code-quality-report.json - expire_in: 1 week + variables: + SETUP_DB: "false" sast: <<: *dedicated-no-docs-no-db-pull-cache-job diff --git a/CHANGELOG.md b/CHANGELOG.md index bec6a8b3e21..0c33596d9c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.8.3 (2019-03-19) + +### Security (1 change) + +- Remove project serialization in quick actions response. + + ## 11.8.2 (2019-03-13) ### Security (1 change) @@ -264,6 +271,14 @@ entry. - Creates mixin to reduce code duplication between CE and EE in graph component. +## 11.7.7 (2019-03-19) + +### Security (2 changes) + +- Remove project serialization in quick actions response. +- Fixed ability to see private groups by users not belonging to given group. + + ## 11.7.5 (2019-02-06) ### Fixed (8 changes) @@ -170,7 +170,7 @@ gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch' gem 'fugit', '~> 1.1' # HTTP requests -gem 'httparty', '~> 0.13.3' +gem 'httparty', '~> 0.16.4' # Colored output to console gem 'rainbow', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index face24489f1..6943428a7d8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -379,8 +379,8 @@ GEM domain_name (~> 0.5) http-form_data (2.1.1) http_parser.rb (0.6.0) - httparty (0.13.7) - json (~> 1.8) + httparty (0.16.4) + mime-types (~> 3.0) multi_xml (>= 0.5.2) httpclient (2.8.3) i18n (1.2.0) @@ -1040,7 +1040,7 @@ DEPENDENCIES health_check (~> 2.6.0) html-pipeline (~> 2.8) html2text - httparty (~> 0.13.3) + httparty (~> 0.16.4) icalendar influxdb (~> 0.2) jaeger-client (~> 0.10.0) diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 31164f74201..47951591e82 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -22,7 +22,7 @@ export default { }, selectedValue: { type: Number, - default: null, + default: DISCUSSION_FILTERS_DEFAULT_VALUE, required: false, }, }, diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index f50cab81efe..be8e42af9ea 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -18,7 +18,7 @@ export default { <div class="note-actions-item"> <gl-button ref="button" - v-gl-tooltip.bottom + v-gl-tooltip class="note-action-button" variant="transparent" :title="__('Reply to comment')" diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js index 5c5f38a3fb0..cdf9a46c5aa 100644 --- a/app/assets/javascripts/notes/discussion_filters.js +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -6,12 +6,16 @@ export default store => { if (discussionFilterEl) { const { defaultFilter, notesFilters } = discussionFilterEl.dataset; - const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; const filters = Object.keys(filterValues).map(entry => ({ title: entry, value: filterValues[entry], })); + const props = { filters }; + + if (defaultFilter) { + props.selectedValue = parseInt(defaultFilter, 10); + } return new Vue({ el: discussionFilterEl, @@ -21,12 +25,7 @@ export default store => { }, store, render(createElement) { - return createElement('discussion-filter', { - props: { - filters, - selectedValue, - }, - }); + return createElement('discussion-filter', { props }); }, }); } diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 4883266dae5..30372103590 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -6,9 +6,8 @@ import createStore from './stores'; document.addEventListener('DOMContentLoaded', () => { const store = createStore(); - initDiscussionFilters(store); - - return new Vue({ + // eslint-disable-next-line no-new + new Vue({ el: '#js-vue-notes', components: { notesApp, @@ -49,4 +48,6 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); + + initDiscussionFilters(store); }); diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 91dbb2a6365..cbd390e7145 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -69,6 +69,7 @@ @include media-breakpoint-up(sm) { display: flex; + height: 100%; align-items: center; padding: 50px 30px; } @@ -99,3 +100,30 @@ } } } + +@include media-breakpoint-up(lg) { + .column-large { + flex: 2; + } + + .column-small { + flex: 1; + margin-bottom: 15px; + + .blank-state { + max-width: 400px; + flex-wrap: wrap; + margin-left: 15px; + } + + .blank-state-icon { + margin-bottom: 30px; + } + } +} + +@include media-breakpoint-down(xs) { + .blank-state-icon svg { + width: 315px; + } +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 5efd484b51b..97a9a55c968 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -120,7 +120,7 @@ hr { text-overflow: ellipsis; white-space: nowrap; - > div, + > div:not(.block), .str-truncated { display: inline; } @@ -381,6 +381,7 @@ img.emoji { .prepend-left-5 { margin-left: 5px; } .prepend-left-8 { margin-left: 8px; } .prepend-left-10 { margin-left: 10px; } +.prepend-left-15 { margin-left: 15px; } .prepend-left-default { margin-left: $gl-padding; } .prepend-left-20 { margin-left: 20px; } .prepend-left-32 { margin-left: 32px; } @@ -388,6 +389,7 @@ img.emoji { .append-right-5 { margin-right: 5px; } .append-right-8 { margin-right: 8px; } .append-right-10 { margin-right: 10px; } +.append-right-15 { margin-right: 15px; } .append-right-default { margin-right: $gl-padding; } .append-right-20 { margin-right: 20px; } .prepend-right-32 { margin-right: 32px; } @@ -402,6 +404,8 @@ img.emoji { .prepend-bottom-32 { margin-bottom: 32px; } .inline { display: inline-block; } .center { text-align: center; } +.block { display: block; } +.flex { display: flex; } .vertical-align-middle { vertical-align: middle; } .vertical-align-sub { vertical-align: sub; } .flex-align-self-center { align-self: center; } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 49b9b7014ae..3ab61cc5c47 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -31,6 +31,7 @@ } } +.ci-status-icon-preparing, .ci-status-icon-running { svg { fill: $blue-400; diff --git a/app/assets/stylesheets/framework/system_messages.scss b/app/assets/stylesheets/framework/system_messages.scss index 3d66136938f..e5edddec71e 100644 --- a/app/assets/stylesheets/framework/system_messages.scss +++ b/app/assets/stylesheets/framework/system_messages.scss @@ -12,7 +12,7 @@ p { @include str-truncated(100%); - margin-top: 0; + margin-top: -1px; margin-bottom: 0; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 0333b9445c5..08dbe3d5b0f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -277,7 +277,7 @@ $general-hover-transition-duration: 100ms; $general-hover-transition-curve: linear; $highlight-changes-color: rgb(235, 255, 232); $performance-bar-height: 35px; -$system-header-height: 35px; +$system-header-height: 16px; $system-footer-height: $system-header-height; $flash-height: 52px; $context-header-height: 60px; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index fa5a182243c..916f6cd3137 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -50,7 +50,6 @@ position: relative; } - .build-trace { @include build-trace(); } @@ -392,3 +391,14 @@ right: 0; margin-top: -17px; } + +@include media-breakpoint-down(sm) { + .top-bar { + .truncated-info { + white-space: nowrap; + overflow: hidden; + max-width: 220px; + text-overflow: ellipsis; + } + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index df3a4be6559..7f8b8ea8100 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -166,6 +166,7 @@ float: left; .accept-merge-request { + &.ci-preparing, &.ci-pending, &.ci-running { @include btn-blue; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 2b6319ddd4f..bb08440fda8 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -498,7 +498,8 @@ list-style: none; } - &:last-child { + // when downstream pipelines are present, the last stage isn't the last column + &:last-child:not(.has-downstream) { .build { // Remove right connecting horizontal line from first build in last stage &:first-child::after { @@ -515,7 +516,8 @@ } } - &:first-child { + // when upstream pipelines are present, the first stage isn't the first column + &:first-child:not(.has-upstream) { .build { // Remove left curved connectors from all builds in first stage &:not(:first-child)::before { @@ -793,6 +795,7 @@ @include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700); } + &.ci-status-icon-preparing, &.ci-status-icon-running { @include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700); } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index ab26259c007..8e933b62dd9 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -266,21 +266,6 @@ padding-top: 20px; } - .cover-controls { - position: static; - padding: 0 16px; - margin-bottom: 20px; - display: flex; - - .btn { - flex-grow: 1; - - &:first-child { - margin-left: 0; - } - } - } - .user-profile-nav { a { margin-right: 0; diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index f4d568d02ac..a59bb31bdcb 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -44,6 +44,7 @@ } &.ci-info, + &.ci-preparing, &.ci-running { @include status-color($blue-100, $blue-500, $blue-600); } diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index b4fee93713b..f96d1821095 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -48,7 +48,7 @@ module NotesActions respond_to do |format| format.json do json = { - commands_changes: @note.commands_changes + commands_changes: @note.commands_changes&.slice(:emoji_award, :time_estimate, :spend_time) } if @note.persisted? && return_discussion? diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 0bc082246a1..f1d6fb00cfc 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -12,6 +12,7 @@ class Groups::GroupMembersController < Groups::ApplicationController # Authorize before_action :authorize_admin_group_member!, except: admin_not_required_endpoints + skip_before_action :check_two_factor_requirement, only: :leave skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, :approve_access_request, :leave, :resend_invite, :override diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index cc2bb99f55b..e90e8278c13 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -3,6 +3,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController include AuthenticatesWithTwoFactor include Devise::Controllers::Rememberable + include AuthHelper protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true @@ -80,10 +81,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end if current_user + return render_403 unless link_provider_allowed?(oauth['provider']) + log_audit_event(current_user, with: oauth['provider']) identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth) - identity_linker.link if identity_linker.changed? diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index b0d65f284af..0d2a6145d0e 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -14,7 +14,7 @@ class Profiles::AccountsController < Profiles::ApplicationController return render_404 unless identity - if unlink_allowed?(provider) + if unlink_provider_allowed?(provider) identity.destroy else flash[:alert] = "You are not allowed to unlink your primary login account" diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index ba94196b2f9..83e14275a8b 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -18,21 +18,16 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController two_factor_authentication_reason( global: lambda do flash.now[:alert] = - 'The global settings require you to enable Two-Factor Authentication for your account.' + s_('The global settings require you to enable Two-Factor Authentication for your account.') end, group: lambda do |groups| - group_links = groups.map { |group| view_context.link_to group.full_name, group_path(group) }.to_sentence - - flash.now[:alert] = %{ - The group settings for #{group_links} require you to enable - Two-Factor Authentication for your account. - }.html_safe + flash.now[:alert] = groups_notification(groups) end ) unless two_factor_grace_period_expired? grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = flash.now[:alert] + " You need to do this before #{l(grace_period_deadline)}." + flash.now[:alert] = flash.now[:alert] + s_(" You need to do this before %{grace_period_deadline}.") % { grace_period_deadline: l(grace_period_deadline) } end end @@ -49,7 +44,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController render 'create' else - @error = 'Invalid pin code' + @error = s_('Invalid pin code') @qr_code = build_qr_code setup_u2f_registration render 'show' @@ -63,7 +58,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController if @u2f_registration.persisted? session.delete(:challenges) - redirect_to profile_two_factor_auth_path, notice: "Your U2F device was registered!" + redirect_to profile_two_factor_auth_path, notice: s_("Your U2F device was registered!") else @qr_code = build_qr_code setup_u2f_registration @@ -85,7 +80,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def skip if two_factor_grace_period_expired? - redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup' + redirect_to new_profile_two_factor_auth_path, alert: s_('Cannot skip two factor authentication setup') else session[:skip_two_factor] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours redirect_to root_path @@ -126,4 +121,12 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def u2f_registration_params params.require(:u2f_registration).permit(:device_response, :name) end + + def groups_notification(groups) + group_links = groups.map { |group| view_context.link_to group.full_name, group_path(group) }.to_sentence + leave_group_links = groups.map { |group| view_context.link_to (s_("leave %{group_name}") % { group_name: group.full_name }), leave_group_members_path(group), remote: false, method: :delete}.to_sentence + + s_(%{The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.}) + .html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe } + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 2b1d6f49878..b4ee648361c 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -100,8 +100,12 @@ module AuthHelper end # rubocop: enable CodeReuse/ActiveRecord - def unlink_allowed?(provider) - %w(saml cas3).exclude?(provider.to_s) + def unlink_provider_allowed?(provider) + IdentityProviderPolicy.new(current_user, provider).can?(:unlink) + end + + def link_provider_allowed?(provider) + IdentityProviderPolicy.new(current_user, provider).can?(:link) end extend self diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5496aa4908c..f2abb241753 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -169,7 +169,7 @@ module ProjectsHelper translation.html_safe end - def project_list_cache_key(project) + def project_list_cache_key(project, pipeline_status: true) key = [ project.route.cache_key, project.cache_key, @@ -179,10 +179,11 @@ module ProjectsHelper Gitlab::CurrentSettings.cache_key, "cross-project:#{can?(current_user, :read_cross_project)}", max_project_member_access_cache_key(project), + pipeline_status, 'v2.6' ] - key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? + key << pipeline_status_cache_key(project.pipeline_status) if pipeline_status && project.pipeline_status.has_status? key end diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 2d237383e60..1c95abdd9ee 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -4,7 +4,7 @@ class BroadcastMessage < ActiveRecord::Base include CacheMarkdownField include Sortable - cache_markdown_field :message, pipeline: :broadcast_message + cache_markdown_field :message, pipeline: :broadcast_message, whitelisted: true validates :message, presence: true validates :starts_at, presence: true diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index a629db82c19..59f47effff7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -172,6 +172,10 @@ module Ci end state_machine :status do + event :enqueue do + transition [:created, :skipped, :manual, :scheduled] => :preparing, if: :any_unmet_prerequisites? + end + event :actionize do transition created: :manual end @@ -185,8 +189,12 @@ module Ci end event :enqueue_scheduled do + transition scheduled: :preparing, if: ->(build) do + build.scheduled_at&.past? && build.any_unmet_prerequisites? + end + transition scheduled: :pending, if: ->(build) do - build.scheduled_at && build.scheduled_at < Time.now + build.scheduled_at&.past? && !build.any_unmet_prerequisites? end end @@ -204,6 +212,12 @@ module Ci end end + after_transition any => [:preparing] do |build| + build.run_after_commit do + Ci::BuildPrepareWorker.perform_async(id) + end + end + after_transition any => [:pending] do |build| build.run_after_commit do BuildQueueWorker.perform_async(id) @@ -355,6 +369,16 @@ module Ci !retried? end + def any_unmet_prerequisites? + return false unless Feature.enabled?(:ci_preparing_state, default_enabled: true) + + prerequisites.present? + end + + def prerequisites + Gitlab::Ci::Build::Prerequisite::Factory.new(self).unmet + end + def expanded_environment_name return unless has_environment? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index adffdc0355e..ae74f569415 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -82,10 +82,14 @@ module Ci state_machine :status, initial: :created do event :enqueue do - transition [:created, :skipped, :scheduled] => :pending + transition [:created, :preparing, :skipped, :scheduled] => :pending transition [:success, :failed, :canceled] => :running end + event :prepare do + transition any - [:preparing] => :preparing + end + event :run do transition any - [:running] => :running end @@ -118,7 +122,7 @@ module Ci # Do not add any operations to this state_machine # Create a separate worker for each new operation - before_transition [:created, :pending] => :running do |pipeline| + before_transition [:created, :preparing, :pending] => :running do |pipeline| pipeline.started_at = Time.now end @@ -141,7 +145,7 @@ module Ci end end - after_transition [:created, :pending] => :running do |pipeline| + after_transition [:created, :preparing, :pending] => :running do |pipeline| pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } end @@ -149,7 +153,7 @@ module Ci pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) } end - after_transition [:created, :pending, :running] => :success do |pipeline| + after_transition [:created, :preparing, :pending, :running] => :success do |pipeline| pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) } end @@ -597,6 +601,7 @@ module Ci retry_optimistic_lock(self) do case latest_builds_status.to_s when 'created' then nil + when 'preparing' then prepare when 'pending' then enqueue when 'running' then run when 'success' then succeed diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 0389945191e..098f5189517 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -39,10 +39,14 @@ module Ci state_machine :status, initial: :created do event :enqueue do - transition created: :pending + transition [:created, :preparing] => :pending transition [:success, :failed, :canceled, :skipped] => :running end + event :prepare do + transition any - [:preparing] => :preparing + end + event :run do transition any - [:running] => :running end @@ -76,6 +80,7 @@ module Ci retry_optimistic_lock(self) do case statuses.latest.status when 'created' then nil + when 'preparing' then prepare when 'pending' then enqueue when 'running' then run when 'success' then succeed diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index fb2221b601f..7786b48429c 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -110,7 +110,7 @@ module Clusters # short time later def terminals(environment) with_reactive_cache do |data| - pods = filter_by_label(data[:pods], app: environment.slug) + pods = filter_by_project_environment(data[:pods], project.full_path_slug, environment.slug) terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }.compact terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } end diff --git a/app/models/commit_collection.rb b/app/models/commit_collection.rb index a9a2e9c81eb..52524456439 100644 --- a/app/models/commit_collection.rb +++ b/app/models/commit_collection.rb @@ -28,10 +28,43 @@ class CommitCollection def without_merge_commits strong_memoize(:without_merge_commits) do - commits.reject(&:merge_commit?) + # `#enrich!` the collection to ensure all commits contain + # the necessary parent data + enrich!.commits.reject(&:merge_commit?) end end + def unenriched + commits.reject(&:gitaly_commit?) + end + + def fully_enriched? + unenriched.empty? + end + + # Batch load any commits that are not backed by full gitaly data, and + # replace them in the collection. + def enrich! + # A project is needed in order to fetch data from gitaly. Projects + # can be absent from commits in certain rare situations (like when + # viewing a MR of a deleted fork). In these cases, assume that the + # enriched data is not needed. + return self if project.blank? || fully_enriched? + + # Batch load full Commits from the repository + # and map to a Hash of id => Commit + replacements = Hash[unenriched.map do |c| + [c.id, Commit.lazy(project, c.id)] + end.compact] + + # Replace the commits, keeping the same order + @commits = @commits.map do |c| + replacements.fetch(c.id, c) + end + + self + end + # Sets the pipeline status for every commit. # # Setting this status ahead of time removes the need for running a query for diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 7f6562b63e5..5f66a661324 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -66,7 +66,10 @@ class CommitStatus < ActiveRecord::Base end event :enqueue do - transition [:created, :skipped, :manual, :scheduled] => :pending + # A CommitStatus will never have prerequisites, but this event + # is shared by Ci::Build, which cannot progress unless prerequisites + # are satisfied. + transition [:created, :preparing, :skipped, :manual, :scheduled] => :pending, unless: :any_unmet_prerequisites? end event :run do @@ -74,26 +77,26 @@ class CommitStatus < ActiveRecord::Base end event :skip do - transition [:created, :pending] => :skipped + transition [:created, :preparing, :pending] => :skipped end event :drop do - transition [:created, :pending, :running, :scheduled] => :failed + transition [:created, :preparing, :pending, :running, :scheduled] => :failed end event :success do - transition [:created, :pending, :running] => :success + transition [:created, :preparing, :pending, :running] => :success end event :cancel do - transition [:created, :pending, :running, :manual, :scheduled] => :canceled + transition [:created, :preparing, :pending, :running, :manual, :scheduled] => :canceled end - before_transition [:created, :skipped, :manual, :scheduled] => :pending do |commit_status| + before_transition [:created, :preparing, :skipped, :manual, :scheduled] => :pending do |commit_status| commit_status.queued_at = Time.now end - before_transition [:created, :pending] => :running do |commit_status| + before_transition [:created, :preparing, :pending] => :running do |commit_status| commit_status.started_at = Time.now end @@ -180,6 +183,10 @@ class CommitStatus < ActiveRecord::Base false end + def any_unmet_prerequisites? + false + end + def auto_canceled? canceled? && auto_canceled_by_id? end diff --git a/app/models/commit_status_enums.rb b/app/models/commit_status_enums.rb index 152105d9429..45e08fa18fe 100644 --- a/app/models/commit_status_enums.rb +++ b/app/models/commit_status_enums.rb @@ -14,7 +14,8 @@ module CommitStatusEnums runner_unsupported: 6, stale_schedule: 7, job_execution_timeout: 8, - archived_failure: 9 + archived_failure: 9, + unmet_prerequisites: 10 } end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 1a8570b80c3..15d8d58b9b5 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -7,6 +7,7 @@ # cache_markdown_field :foo # cache_markdown_field :bar # cache_markdown_field :baz, pipeline: :single_line +# cache_markdown_field :baz, whitelisted: true # # Corresponding foo_html, bar_html and baz_html fields should exist. module CacheMarkdownField @@ -37,7 +38,15 @@ module CacheMarkdownField end def html_fields - markdown_fields.map {|field| html_field(field) } + markdown_fields.map { |field| html_field(field) } + end + + def html_fields_whitelisted + markdown_fields.each_with_object([]) do |field, fields| + if @data[field].fetch(:whitelisted, false) + fields << html_field(field) + end + end end end @@ -149,13 +158,18 @@ module CacheMarkdownField alias_method :attributes_before_markdown_cache, :attributes def attributes attrs = attributes_before_markdown_cache + html_fields = cached_markdown_fields.html_fields + whitelisted = cached_markdown_fields.html_fields_whitelisted + exclude_fields = html_fields - whitelisted - attrs.delete('cached_markdown_version') - - cached_markdown_fields.html_fields.each do |field| + exclude_fields.each do |field| attrs.delete(field) end + if whitelisted.empty? + attrs.delete('cached_markdown_version') + end + attrs end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 0d2be4c61ab..8882f48c281 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -5,14 +5,14 @@ module HasStatus DEFAULT_STATUS = 'created'.freeze BLOCKED_STATUS = %w[manual scheduled].freeze - AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual scheduled].freeze + AVAILABLE_STATUSES = %w[created preparing pending running success failed canceled skipped manual scheduled].freeze STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze - ACTIVE_STATUSES = %w[pending running].freeze + ACTIVE_STATUSES = %w[preparing pending running].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze - ORDERED_STATUSES = %w[failed pending running manual scheduled canceled success skipped created].freeze + ORDERED_STATUSES = %w[failed preparing pending running manual scheduled canceled success skipped created].freeze STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, failed: 4, canceled: 5, skipped: 6, manual: 7, - scheduled: 8 }.freeze + scheduled: 8, preparing: 9 }.freeze UnknownStatusError = Class.new(StandardError) @@ -26,6 +26,7 @@ module HasStatus success = scope_relevant.success.select('count(*)').to_sql manual = scope_relevant.manual.select('count(*)').to_sql scheduled = scope_relevant.scheduled.select('count(*)').to_sql + preparing = scope_relevant.preparing.select('count(*)').to_sql pending = scope_relevant.pending.select('count(*)').to_sql running = scope_relevant.running.select('count(*)').to_sql skipped = scope_relevant.skipped.select('count(*)').to_sql @@ -37,12 +38,14 @@ module HasStatus WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{builds})=(#{success}) THEN 'success' WHEN (#{builds})=(#{created}) THEN 'created' + WHEN (#{builds})=(#{preparing}) THEN 'preparing' WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'success' WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled' WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})>0 THEN 'running' WHEN (#{manual})>0 THEN 'manual' WHEN (#{scheduled})>0 THEN 'scheduled' + WHEN (#{preparing})>0 THEN 'preparing' WHEN (#{created})>0 THEN 'running' ELSE 'failed' END)" @@ -70,6 +73,7 @@ module HasStatus state_machine :status, initial: :created do state :created, value: 'created' + state :preparing, value: 'preparing' state :pending, value: 'pending' state :running, value: 'running' state :failed, value: 'failed' @@ -81,6 +85,7 @@ module HasStatus end scope :created, -> { where(status: 'created') } + scope :preparing, -> { where(status: 'preparing') } scope :relevant, -> { where(status: AVAILABLE_STATUSES - ['created']) } scope :running, -> { where(status: 'running') } scope :pending, -> { where(status: 'pending') } @@ -90,14 +95,14 @@ module HasStatus scope :skipped, -> { where(status: 'skipped') } scope :manual, -> { where(status: 'manual') } scope :scheduled, -> { where(status: 'scheduled') } - scope :alive, -> { where(status: [:created, :pending, :running]) } + scope :alive, -> { where(status: [:created, :preparing, :pending, :running]) } scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :cancelable, -> do - where(status: [:running, :pending, :created, :scheduled]) + where(status: [:running, :preparing, :pending, :created, :scheduled]) end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 811e623b7f7..428edfd88de 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -78,6 +78,10 @@ class Deployment < ActiveRecord::Base Commit.truncate_sha(sha) end + def cluster + project.deployment_platform(environment: environment.name)&.cluster + end + def last? self == environment.last_deployment end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 4cf3a7f3d84..f650dbd3726 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -131,8 +131,8 @@ class KubernetesService < DeploymentService # short time later def terminals(environment) with_reactive_cache do |data| - pods = filter_by_label(data[:pods], app: environment.slug) - terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) } + pods = filter_by_project_environment(data[:pods], project.full_path_slug, environment.slug) + terminals = pods.flat_map { |pod| terminals_for_pod(api_url, actual_namespace, pod) }.compact terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } end end diff --git a/app/policies/identity_provider_policy.rb b/app/policies/identity_provider_policy.rb new file mode 100644 index 00000000000..d34cdd5bdd4 --- /dev/null +++ b/app/policies/identity_provider_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class IdentityProviderPolicy < BasePolicy + desc "Provider is SAML or CAS3" + condition(:protected_provider, scope: :subject, score: 0) { %w(saml cas3).include?(@subject.to_s) } + + rule { anonymous }.prevent_all + + rule { default }.policy do + enable :unlink + enable :link + end + + rule { protected_provider }.prevent(:unlink) +end diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index 0cd77da6303..28a25c8b7a3 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -11,7 +11,8 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated runner_unsupported: 'Your runner is outdated, please upgrade your runner', stale_schedule: 'Delayed job could not be executed by some reason, please try again', job_execution_timeout: 'The script exceeded the maximum execution time set for the job', - archived_failure: 'The job is archived and cannot be run' + archived_failure: 'The job is archived and cannot be run', + unmet_prerequisites: 'The job failed to complete prerequisite tasks' }.freeze private_constant :CALLOUT_FAILURE_MESSAGES diff --git a/app/services/ci/prepare_build_service.rb b/app/services/ci/prepare_build_service.rb new file mode 100644 index 00000000000..32f11438b79 --- /dev/null +++ b/app/services/ci/prepare_build_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Ci + class PrepareBuildService + attr_reader :build + + def initialize(build) + @build = build + end + + def execute + prerequisites.each(&:complete!) + + unless build.enqueue + build.drop!(:unmet_prerequisites) + end + end + + private + + def prerequisites + build.prerequisites + end + end +end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 6756299cf43..2a1d2c2aeab 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -22,9 +22,10 @@ %h3.text-center Users: = approximate_count_with_delimiters(@counts, User) - = render_if_exists 'admin/dashboard/users_statistics' %hr - = link_to 'New user', new_admin_user_path, class: "btn btn-success" + .btn-group.d-flex{ role: 'group' } + = link_to 'New user', new_admin_user_path, class: "btn btn-success" + = render_if_exists 'admin/dashboard/users_statistics' .col-sm-4 .info-well.dark-well .well-segment.well-centered diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml new file mode 100644 index 00000000000..fb4da08e129 --- /dev/null +++ b/app/views/profiles/_email_settings.html.haml @@ -0,0 +1,16 @@ +- form = local_assigns.fetch(:form) +- readonly = @user.read_only_attribute?(:email) +- email_change_disabled = local_assigns.fetch(:email_change_disabled, nil) +- read_only_help_text = readonly ? s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) } : user_email_help_text(@user) +- help_text = email_change_disabled ? s_("Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO.") % { group_name: @user.managing_group.name } : read_only_help_text + += form.text_field :email, required: true, class: 'input-lg', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled += form.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), + { help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") }, + control_class: 'select2 input-lg', disabled: email_change_disabled +- commit_email_link_url = help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank') +- commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url } +- commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe } += form.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)), + { help: commit_email_docs_link }, + control_class: 'select2 input-lg', disabled: email_change_disabled diff --git a/app/views/profiles/accounts/_providers.html.haml b/app/views/profiles/accounts/_providers.html.haml new file mode 100644 index 00000000000..068f9cc70f7 --- /dev/null +++ b/app/views/profiles/accounts/_providers.html.haml @@ -0,0 +1,21 @@ +%label.label-bold + = s_('Profiles|Connected Accounts') + %p= s_('Profiles|Click on icon to activate signin with one of the following services') + - providers.each do |provider| + - unlink_allowed = unlink_provider_allowed?(provider) + - link_allowed = link_provider_allowed?(provider) + - if unlink_allowed || link_allowed + .provider-btn-group + .provider-btn-image + = provider_image_tag(provider) + - if auth_active?(provider) + - if unlink_allowed + = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do + = s_('Profiles|Disconnect') + - else + %a.provider-btn + = s_('Profiles|Active') + - elsif link_allowed + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do + = s_('Profiles|Connect') + = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index ee2c5a13b8a..e6380817c8f 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -29,24 +29,7 @@ %p = s_('Profiles|Activate signin with one of the following services') .col-lg-8 - %label.label-bold - = s_('Profiles|Connected Accounts') - %p= s_('Profiles|Click on icon to activate signin with one of the following services') - - button_based_providers.each do |provider| - .provider-btn-group - .provider-btn-image - = provider_image_tag(provider) - - if auth_active?(provider) - - if unlink_allowed?(provider) - = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do - = s_('Profiles|Disconnect') - - else - %a.provider-btn - = s_('Profiles|Active') - - else - = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do - = s_('Profiles|Connect') - = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: local_assigns[:group_saml_identities] + = render 'providers', providers: button_based_providers, group_saml_identities: local_assigns[:group_saml_identities] %hr - if current_user.can_change_username? .row.prepend-top-default diff --git a/app/views/profiles/notifications/_email_settings.html.haml b/app/views/profiles/notifications/_email_settings.html.haml new file mode 100644 index 00000000000..34dcf8f5402 --- /dev/null +++ b/app/views/profiles/notifications/_email_settings.html.haml @@ -0,0 +1,6 @@ +- form = local_assigns.fetch(:form) +.form-group + = form.label :notification_email, class: "label-bold" + = form.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2", disabled: local_assigns.fetch(:email_change_disabled, nil) + .help-block + = local_assigns.fetch(:help_text, nil) diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 712eb2a4573..e616e5546b3 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -22,9 +22,7 @@ Global notification settings = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| - .form-group - = f.label :notification_email, class: "label-bold" - = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" + = render_if_exists 'profiles/notifications/email_settings', form: f = label_tag :global_notification_level, "Global notification level", class: "label-bold" %br diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 4d3d92d09c0..1fffea08dae 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -83,18 +83,7 @@ = f.text_field :name, label: 'Full name', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name' }, help: s_("Profiles|Enter your name, so people you know can recognize you") = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } - - if @user.read_only_attribute?(:email) - = f.text_field :email, required: true, class: 'input-lg', readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) } - - else - = f.text_field :email, required: true, class: 'input-lg', value: (@user.email unless @user.temp_oauth_email?), - help: user_email_help_text(@user) - = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), - { help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") }, - control_class: 'select2 input-lg' - - commit_email_docs_link = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank') - = f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)), - { help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } }, - control_class: 'select2 input-lg' + = render_if_exists 'profiles/email_settings', form: f = f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username") = f.text_field :linkedin, class: 'input-md', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename") = f.text_field :twitter, class: 'input-md', placeholder: s_("Profiles|@username") diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index be7a2436d16..2e62039b90a 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -20,7 +20,7 @@ .search-results - if @scope == 'projects' .term - = render 'shared/projects/list', projects: @search_objects + = render 'shared/projects/list', projects: @search_objects, pipeline_status: false - else = render partial: "search/results/#{@scope.singularize}", collection: @search_objects diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index d2b1be29eb9..90fb067e75d 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -10,7 +10,7 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project) - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description -- cache_key = project_list_cache_key(project) +- cache_key = project_list_cache_key(project, pipeline_status: pipeline_status) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date) - css_controls_class = compact_mode ? "" : "flex-lg-row justify-content-lg-between" - avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar' diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index b2d88567e0e..6ebd756d3da 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -71,6 +71,7 @@ - pipeline_hooks:build_hooks - pipeline_hooks:pipeline_hooks - pipeline_processing:build_finished +- pipeline_processing:ci_build_prepare - pipeline_processing:build_queue - pipeline_processing:build_success - pipeline_processing:pipeline_process diff --git a/app/workers/ci/build_prepare_worker.rb b/app/workers/ci/build_prepare_worker.rb new file mode 100644 index 00000000000..1a35a74ae53 --- /dev/null +++ b/app/workers/ci/build_prepare_worker.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Ci + class BuildPrepareWorker + include ApplicationWorker + include PipelineQueue + + queue_namespace :pipeline_processing + + def perform(build_id) + Ci::Build.find_by_id(build_id).try do |build| + Ci::PrepareBuildService.new(build).execute + end + end + end +end diff --git a/app/workers/cluster_configure_worker.rb b/app/workers/cluster_configure_worker.rb index 63e6cc147be..b984dee5b21 100644 --- a/app/workers/cluster_configure_worker.rb +++ b/app/workers/cluster_configure_worker.rb @@ -5,6 +5,8 @@ class ClusterConfigureWorker include ClusterQueue def perform(cluster_id) + return if Feature.enabled?(:ci_preparing_state, default_enabled: true) + Clusters::Cluster.find_by_id(cluster_id).try do |cluster| Clusters::RefreshService.create_or_update_namespaces_for_cluster(cluster) end diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb index 497e57c0d0b..d7bea69a01c 100644 --- a/app/workers/cluster_project_configure_worker.rb +++ b/app/workers/cluster_project_configure_worker.rb @@ -5,6 +5,8 @@ class ClusterProjectConfigureWorker include ClusterQueue def perform(project_id) + return if Feature.enabled?(:ci_preparing_state, default_enabled: true) + project = Project.find(project_id) ::Clusters::RefreshService.create_or_update_namespaces_for_project(project) diff --git a/changelogs/unreleased/57115-just-in-time-k8s-resource-creation.yml b/changelogs/unreleased/57115-just-in-time-k8s-resource-creation.yml new file mode 100644 index 00000000000..2141c75ec72 --- /dev/null +++ b/changelogs/unreleased/57115-just-in-time-k8s-resource-creation.yml @@ -0,0 +1,5 @@ +--- +title: Create Kubernetes resources for projects when their deployment jobs run. +merge_request: 25586 +author: +type: changed diff --git a/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml b/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml new file mode 100644 index 00000000000..3e494847e75 --- /dev/null +++ b/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml @@ -0,0 +1,5 @@ +--- +title: Reduce height of instance system header and footer +merge_request: 25752 +author: +type: changed diff --git a/changelogs/unreleased/58883-fix-fetching-comments.yml b/changelogs/unreleased/58883-fix-fetching-comments.yml new file mode 100644 index 00000000000..14c0f1687f2 --- /dev/null +++ b/changelogs/unreleased/58883-fix-fetching-comments.yml @@ -0,0 +1,5 @@ +--- +title: Fix error shown when loading links to specific comments +merge_request: 26092 +author: +type: fixed diff --git a/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml b/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml new file mode 100644 index 00000000000..febbbce2139 --- /dev/null +++ b/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Improve mobile UI on User Profile page +merge_request: 26240 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/59208-fix-error-500-on-every-page-when-active-broadcast-message-present-after-upgrading-to-11-9-0.yml b/changelogs/unreleased/59208-fix-error-500-on-every-page-when-active-broadcast-message-present-after-upgrading-to-11-9-0.yml new file mode 100644 index 00000000000..3c9feae5a04 --- /dev/null +++ b/changelogs/unreleased/59208-fix-error-500-on-every-page-when-active-broadcast-message-present-after-upgrading-to-11-9-0.yml @@ -0,0 +1,6 @@ +--- +title: Gracefully handles excluded fields from attributes during serialization on + JsonCache +merge_request: 26368 +author: +type: fixed diff --git a/changelogs/unreleased/avoid_es_loading_project_ci_status.yml b/changelogs/unreleased/avoid_es_loading_project_ci_status.yml new file mode 100644 index 00000000000..514909c730d --- /dev/null +++ b/changelogs/unreleased/avoid_es_loading_project_ci_status.yml @@ -0,0 +1,5 @@ +--- +title: Avoid loading pipeline status in project search +merge_request: 26342 +author: +type: performance diff --git a/changelogs/unreleased/do-not-force-2fa.yml b/changelogs/unreleased/do-not-force-2fa.yml new file mode 100644 index 00000000000..f9be40e8f37 --- /dev/null +++ b/changelogs/unreleased/do-not-force-2fa.yml @@ -0,0 +1,6 @@ +--- +title: Add link on two-factor authorization settings page to leave group that enforces + two-factor authorization +merge_request: 25731 +author: +type: changed diff --git a/changelogs/unreleased/k8s_new_deployment_labels.yml b/changelogs/unreleased/k8s_new_deployment_labels.yml new file mode 100644 index 00000000000..e9ef3ee0082 --- /dev/null +++ b/changelogs/unreleased/k8s_new_deployment_labels.yml @@ -0,0 +1,5 @@ +--- +title: Update deploy boards to additionally select on "app.gitlab.com" annotations +merge_request: 25623 +author: +type: changed diff --git a/changelogs/unreleased/security-2826-fix-project-serialization-in-quick-actions.yml b/changelogs/unreleased/security-2826-fix-project-serialization-in-quick-actions.yml new file mode 100644 index 00000000000..272f8a95957 --- /dev/null +++ b/changelogs/unreleased/security-2826-fix-project-serialization-in-quick-actions.yml @@ -0,0 +1,5 @@ +--- +title: Remove project serialization in quick actions response +merge_request: +author: +type: security diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index a1ac4a2a57c..b21bfafc096 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -1,6 +1,7 @@ # GitLab Container Registry administration > **Notes:** +> > - [Introduced][ce-4040] in GitLab 8.8. > - Container Registry manifest `v1` support was added in GitLab 8.9 to support > Docker versions earlier than 1.10. diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index a52bc5c3b02..3daebc4d84b 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -14,6 +14,7 @@ a hosted cloud solution or you can use the one that comes bundled with Omnibus GitLab packages. > **Notes:** +> > - Redis requires authentication for High Availability. See > [Redis Security](https://redis.io/topics/security) documentation for more > information. We recommend using a combination of a Redis password and tight @@ -55,6 +56,7 @@ components below. ### High Availability with Sentinel > **Notes:** +> > - Starting with GitLab `8.11`, you can configure a list of Redis Sentinel > servers that will monitor a group of Redis servers to provide failover support. > - Starting with GitLab `8.14`, the Omnibus GitLab Enterprise Edition package @@ -231,6 +233,7 @@ Pick the one that suits your needs. This is the section where we install and set up the new Redis instances. > **Notes:** +> > - We assume that you have installed GitLab and all HA components from scratch. If you > already have it installed and running, read how to > [switch from a single-machine installation to Redis HA](#switching-from-an-existing-single-machine-installation-to-redis-ha). diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 8522d046a92..e7792106f81 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -1,6 +1,7 @@ # Jobs artifacts administration > **Notes:** +> > - Introduced in GitLab 8.2 and GitLab Runner 0.7.0. > - Starting with GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format changed to `ZIP`. > - Starting with GitLab 8.17, builds are renamed to jobs. @@ -86,6 +87,7 @@ _The artifacts are stored by default in ### Using object storage > **Notes:** +> > - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1762) in > [GitLab Premium](https://about.gitlab.com/pricing/) 9.4. > - Since version 9.5, artifacts are [browsable](../user/project/pipelines/job_artifacts.md#browsing-artifacts), diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 20d7ef9bb74..f2ac155a694 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -1,6 +1,7 @@ # Monitoring GitLab with Prometheus > **Notes:** +> > - Prometheus and the various exporters listed in this page are bundled in the > Omnibus GitLab package. Check each exporter's documentation for the timeline > they got added. For installations from source you will have to install them diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 279ad018aed..288ce376687 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -5,6 +5,7 @@ description: 'Learn how to administer GitLab Pages.' # GitLab Pages administration > **Notes:** +> > - [Introduced][ee-80] in GitLab EE 8.3. > - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. > - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index 40f7c5566ac..25c3d564560 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -86,6 +86,11 @@ by another folder with the next 2 characters. They are both stored in a special ### Hashed object pools +CAUTION: **Beta:** +Hashed objects pools are considered beta, and are not ready for production use. +Follow [gitaly#1548](https://gitlab.com/gitlab-org/gitaly/issues/1548) for +updates. + For deduplication of public forks and their parent repository, objects are pooled in an object pool. These object pools are a third repository where shared objects are stored. diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 8c0c7a36736..708b59a273b 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -18,7 +18,7 @@ below. >**Notes:** For historical reasons, uploads are stored into a base directory, which by default is `uploads/-/system`. It is strongly discouraged to change this configuration option on an existing GitLab installation. -_The uploads are stored by default in `/var/opt/gitlab/gitlab-rails/uploads/-/system`._ +_The uploads are stored by default in `/var/opt/gitlab/gitlab-rails/uploads`._ 1. To change the storage path for example to `/mnt/storage/uploads`, edit `/etc/gitlab/gitlab.rb` and add the following line: diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md index eb974267084..1c2f56581eb 100644 --- a/doc/api/group_milestones.md +++ b/doc/api/group_milestones.md @@ -1,6 +1,5 @@ # Group milestones API -> **Notes:** > [Introduced][ce-12819] in GitLab 9.5. ## List group milestones diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index f02674adfe2..0ccb0517e08 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -121,7 +121,6 @@ Parameters: ## Get user agent details -> **Notes:** > [Introduced][ce-29508] in GitLab 9.4. Available only for admins. diff --git a/doc/api/services.md b/doc/api/services.md index c44f5cc5781..03d0a80aa64 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -505,10 +505,9 @@ GET /projects/:id/services/jira Set JIRA service for a project. -> **Notes:** -> - Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and -> `project_url` are replaced by `project_key`, `url`. If you are using an -> older version, [follow this documentation][old-jira-api]. +> Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and +> `project_url` are replaced by `project_key`, `url`. If you are using an +> older version, [follow this documentation][old-jira-api]. ``` PUT /projects/:id/services/jira diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md index df7fb8a4912..a06fe6961a7 100644 --- a/doc/ci/chatops/README.md +++ b/doc/ci/chatops/README.md @@ -2,9 +2,9 @@ > **Notes:** > -> * [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4466) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.6. [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24780) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.9. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4466) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.6. [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24780) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.9. > -> * ChatOps is currently in alpha, with some important features missing like access control. +> - ChatOps is currently in alpha, with some important features missing like access control. GitLab ChatOps provides a method to interact with CI/CD jobs through chat services like Slack. Many organizations' discussion, collaboration, and troubleshooting is taking place in chat services these days, and having a method to run CI/CD jobs with output posted back to the channel can significantly augment a team's workflow. diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 4f9efb57b8d..9266c4511be 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -389,6 +389,7 @@ If you're running multiple Runners you will have to modify all configuration fil ## Using the GitLab Container Registry > **Notes:** +> > - This feature requires GitLab 8.8 and GitLab Runner 1.2. > - Starting from GitLab 8.12, if you have [2FA] enabled in your account, you need > to pass a [personal access token][pat] instead of your password in order to diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md index 90a8f5917f8..908cf85980e 100644 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md @@ -4,7 +4,7 @@ author_gitlab: blitzgren level: intermediate article_type: tutorial date: 2018-03-07 -last_updated: 2019-03-06 +last_updated: 2019-03-11 --- # DevOps and Game Dev with GitLab CI/CD @@ -21,7 +21,7 @@ and the basics of game development. Our [demo game](http://gitlab-game-demo.s3-website-us-east-1.amazonaws.com/) consists of a simple spaceship traveling in space that shoots by clicking the mouse in a given direction. -Creating a strong CI/CD pipeline at the beginning of developing another game, [Dark Nova](http://darknova.io/about), +Creating a strong CI/CD pipeline at the beginning of developing another game, [Dark Nova](http://darknova.io/), was essential for the fast pace the team worked at. This tutorial will build upon my [previous introductory article](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) and go through the following steps: @@ -38,8 +38,8 @@ This will also provide boilerplate code for starting a browser-based game with the following components: - Written in [Typescript](https://www.typescriptlang.org/) and [PhaserJs](https://phaser.io) -- Building, running, and testing with [Gulp](http://gulpjs.com/) -- Unit tests with [Chai](http://chaijs.com/) and [Mocha](https://mochajs.org/) +- Building, running, and testing with [Gulp](https://gulpjs.com) +- Unit tests with [Chai](https://www.chaijs.com) and [Mocha](https://mochajs.org/) - CI/CD with GitLab - Hosting the codebase on GitLab.com - Hosting the game on AWS @@ -318,7 +318,7 @@ allowing us to run our tests by every push. Our entire `.gitlab-ci.yml` file should now look like this: ```yml -image: node:6 +image: node:10 build: stage: build @@ -413,7 +413,7 @@ use root security credentials. Proper IAM credential management is beyond the sc article, but AWS will remind you that using root credentials is unadvised and against their best practices, as they should. Feel free to follow best practices and use a custom IAM user's credentials, which will be the same two credentials (Key ID and Secret). It's a good idea to -fully understand [IAM Best Practices in AWS](http://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html). We need to add these credentials to GitLab: +fully understand [IAM Best Practices in AWS](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html). We need to add these credentials to GitLab: 1. Log into your AWS account and go to the [Security Credentials page](https://console.aws.amazon.com/iam/home#/security_credential) 1. Click the **Access Keys** section and **Create New Access Key**. Create the key and keep the id and secret around, you'll need them later @@ -455,7 +455,7 @@ Be sure to update the region and S3 URL in that last script command to fit your Our final configuration file `.gitlab-ci.yml` looks like: ```yml -image: node:6 +image: node:10 build: stage: build @@ -503,7 +503,7 @@ deploy: ## Conclusion Within the [demo repository](https://gitlab.com/blitzgren/gitlab-game-demo) you can also find a handful of boilerplate code to get -[Typescript](https://www.typescriptlang.org/), [Mocha](https://mochajs.org/), [Gulp](http://gulpjs.com/) and [Phaser](https://phaser.io) all playing +[Typescript](https://www.typescriptlang.org/), [Mocha](https://mochajs.org/), [Gulp](https://gulpjs.com/) and [Phaser](https://phaser.io) all playing together nicely with GitLab CI/CD, which is the result of lessons learned while making [Dark Nova](http://darknova.io/). Using a combination of free and open source software, we have a full CI/CD pipeline, a game foundation, and unit tests, all running and deployed at every push to master - with shockingly little code. @@ -522,6 +522,6 @@ Here are some ideas to further investigate that can speed up or improve your pip - [Yarn](https://yarnpkg.com) instead of npm - Set up a custom [Docker](../../../ci/docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) image that can preload dependencies and tools (like AWS CLI) -- Forward a [custom domain](http://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to your game's S3 static website +- Forward a [custom domain](https:/docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to your game's S3 static website - Combine jobs if you find it unnecessary for a small project - Avoid the queues and set up your own [custom GitLab CI/CD runner](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/) diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 3634e648219..71c9637e72c 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -225,14 +225,12 @@ See this [section][vue-test]. ### Running frontend tests -`rake karma` runs the frontend-only (JavaScript) tests. -It consists of two subtasks: +For running the frontend tests, you need the following commands: -- `rake karma:fixtures` (re-)generates fixtures -- `rake karma:tests` actually executes the tests +- `rake karma:fixtures` (re-)generates fixtures. +- `yarn test` executes the tests. -As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`) -is sufficient (and saves you some time). +As long as the fixtures don't change, `yarn test` is sufficient (and saves you some time). ### Live testing and focused testing diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index c78cfc41678..574ba961cb0 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -409,6 +409,8 @@ an access key from the Google console first: 1. Select "Interoperability" and create an access key 1. Make note of the "Access Key" and "Secret" and replace them in the configurations below +1. In the buckets advanced settings ensure the Access Control option "Set object-level + and bucket-level permissions" is selected 1. Make sure you already have a bucket created For Omnibus GitLab packages: diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 43aa4163ab1..fb3f9711711 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -1022,6 +1022,10 @@ planned for a subsequent release. buildpack](#custom-buildpacks). - Auto Test may fail because of a mismatch between testing frameworks. In this case, you may need to customize your `.gitlab-ci.yml` with your test commands. +- Auto Deploy may fail if it is unable to create a Kubernetes namespace and + service account for your project. See the + [troubleshooting failed deployments](../../user/project/clusters/index.md#troubleshooting-failed-deployment-jobs) + section to debug why these resources were not created. ### Disable the banner instance wide diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index a15e1a59921..cf0dc51ea23 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -19,13 +19,14 @@ comment at any time, and anyone with [Maintainer access level][permissions] or higher can also edit a comment made by someone else. You can also reply to a comment notification email to reply to the comment if -[Reply by email] is configured for your GitLab instance. Replying to a standard comment +[Reply by email] is configured for your GitLab instance. Replying to a standard comment creates another standard comment. Replying to a discussion comment creates a reply in the discussion thread. Email replies support [Markdown] and [quick actions], just as if you replied from the web. ## Resolvable comments and discussions > **Notes:** +> > - The main feature was [introduced][ce-5022] in GitLab 8.11. > - Resolvable discussions can be added only to merge request diffs. @@ -357,7 +358,7 @@ Clicking on the **Reply to comment** button will bring the reply area into focus ![Reply to comment feature](img/reply_to_comment.gif) -Relying to a non-discussion comment will convert the non-discussion comment to a +Relying to a non-discussion comment will convert the non-discussion comment to a threaded discussion once the reply is submitted. This conversion is considered an edit to the original comment, so a note about when it was last edited will appear underneath it. diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 1fe8017adbc..3bcfd30079d 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -63,9 +63,8 @@ together in a single list view. ## Create a new group -> **Notes:** -> - For a list of words that are not allowed to be used as group names see the -> [reserved names](../reserved_names.md). +> For a list of words that are not allowed to be used as group names see the +> [reserved names](../reserved_names.md). You can create a group in GitLab from: diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 8e5fe4b0fb9..b74bd81d467 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -59,6 +59,7 @@ of recovery codes. ### Enable 2FA via U2F device > **Notes:** +> > - GitLab officially only supports [Yubikey] U2F devices. > - Support for U2F devices was added in GitLab 8.8. diff --git a/doc/user/project/bulk_editing.md b/doc/user/project/bulk_editing.md index fead99c5e88..d0c7daf4692 100644 --- a/doc/user/project/bulk_editing.md +++ b/doc/user/project/bulk_editing.md @@ -1,6 +1,7 @@ # Bulk editing issues and merge requests > **Notes:** +> > - A permission level of `Reporter` or higher is required in order to manage > issues. > - A permission level of `Developer` or higher is required in order to manage diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index b05e8777b40..44b9e5cccb5 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -556,26 +556,27 @@ NOTE: **NOTE:** Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main service account of the cluster integration. -### Troubleshooting missing `KUBECONFIG` or `KUBE_TOKEN` +### Troubleshooting failed deployment jobs -GitLab will create a new service account specifically for your CI builds. The -new service account is created when the cluster is added to the project. -Sometimes there may be errors that cause the service account creation to fail. +GitLab will create a namespace and service account specifically for your +deployment jobs. These resources are created just before the deployment +job starts. Sometimes there may be errors that cause their creation to fail. -In such instances, your build will not be passed the `KUBECONFIG` or -`KUBE_TOKEN` variables and, if you are using Auto DevOps, your Auto DevOps -pipelines will no longer trigger a `production` deploy build. You will need to -check the [logs](../../../administration/logs.md) to debug why the service -account creation failed. +In such instances, your job will fail with the message: + +```The job failed to complete prerequisite tasks``` + +You will need to check the [logs](../../../administration/logs.md) to debug +why the namespace and service account creation failed. A common reason for failure is that the token you gave GitLab did not have [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) privileges as GitLab expects. -Another common problem for why these variables are not being passed to your -builds is that they must have a matching +Another common problem is caused by a missing `KUBECONFIG` or `KUBE_TOKEN`. +To be passed to your job, it must have a matching [`environment:name`](../../../ci/environments.md#defining-environments). If -your build has no `environment:name` set, it will not be passed the Kubernetes +your job has no `environment:name` set, it will not be passed the Kubernetes credentials. ## Monitoring your Kubernetes cluster **[ULTIMATE]** diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index dec6eac2508..83b268db967 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -1,7 +1,8 @@ # GitLab Container Registry > **Notes:** -> [Introduced][ce-4040] in GitLab 8.8. +> +> - [Introduced][ce-4040] in GitLab 8.8. > - Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker > versions earlier than 1.10. > - This document is about the user guide. To learn how to enable GitLab Container @@ -10,7 +11,7 @@ > - Starting from GitLab 8.12, if you have 2FA enabled in your account, you need > to pass a [personal access token][pat] instead of your password in order to > login to GitLab's Container Registry. -> - Multiple level image names support was added in GitLab 9.1 +> - Multiple level image names support was added in GitLab 9.1. With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. @@ -41,6 +42,7 @@ to enable it. ## Build and push images > **Notes:** +> > - Moving or renaming existing container registry repositories is not supported > once you have pushed images because the images are signed, and the > signature includes the repository name. diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 754711f5919..a90167b9767 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -56,6 +56,7 @@ When connecting to **JIRA Cloud**, which supports authentication via API token, ### Configuring GitLab > **Notes:** +> > - The currently supported Jira versions are `v6.x` and `v7.x.`. GitLab 7.8 or > higher is required. > - GitLab 8.14 introduced a new way to integrate with Jira which greatly simplified @@ -142,6 +143,7 @@ the same goal: where `PROJECT-1` is the issue ID of the Jira project. > **Notes:** +> > - Only commits and merges into the project's default branch (usually **master**) will > close an issue in Jira. You can change your projects default branch under > [project settings](img/jira_project_settings.png). diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md index 90500fd9c21..70bd1e60594 100644 --- a/doc/user/project/merge_requests/versions.md +++ b/doc/user/project/merge_requests/versions.md @@ -1,6 +1,7 @@ # Merge requests versions > **Notes:** +> > - [Introduced][ce-5467] in GitLab 8.12. > - Comments are disabled while viewing outdated merge versions or comparing to > versions other than base. diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index d41b65f7985..6c3fa5eb463 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -205,6 +205,7 @@ With the update permission model we also extended the support for accessing Container Registries for private projects. > **Notes:** +> > - GitLab Runner versions prior to 1.8 don't incorporate the introduced changes > for permissions. This makes the `image:` directive to not work with private > projects automatically and it needs to be configured manually on Runner's host diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index f67ef1e6a46..ccb0300e23e 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -1,6 +1,7 @@ # Exploring GitLab Pages > **Notes:** +> > - This feature was [introduced][ee-80] in GitLab EE 8.3. > - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. > - GitLab Pages [was ported][ce-14605] to Community Edition in GitLab 8.17. diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index 5271c76fc24..a3f40c20192 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -1,6 +1,7 @@ # Introduction to job artifacts > **Notes:** +> > - Since GitLab 8.2 and GitLab Runner 0.7.0, job artifacts that are created by > GitLab Runner are uploaded to GitLab and are downloadable as a single archive > (`tar.gz`) using the GitLab UI. @@ -152,7 +153,7 @@ For example: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/master/browse?job=coverage ``` -There is also a URL to specific files, including html files that +There is also a URL to specific files, including html files that are shown in [GitLab Pages](../../../administration/pages/index.md): ``` @@ -191,9 +192,9 @@ artifacts and the job's trace. 1. Click the trash icon at the top right of the job's trace. 1. Confirm the deletion. -## Retrieve artifacts of private projects when using GitLab CI +## Retrieve artifacts of private projects when using GitLab CI In order to retrieve a job artifact of a different project, you might need to use a private token in order to [authenticate and download](../../../api/jobs.md#get-job-artifacts) the artifacts. [expiry date]: ../../../ci/yaml/README.md#artifactsexpire_in -[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399
\ No newline at end of file +[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399 diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 58a0fbc97cd..07ce4f3f5da 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -1,6 +1,7 @@ # Pipeline schedules > **Notes**: +> > - This feature was introduced in 9.1 as [Trigger Schedule][ce-10533]. > - In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853]. > - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb index 13d67e0f871..c1517222956 100644 --- a/lib/gitlab/auth/ldap/person.rb +++ b/lib/gitlab/auth/ldap/person.rb @@ -69,7 +69,7 @@ module Gitlab end def name - attribute_value(:name).first + attribute_value(:name)&.first end def uid diff --git a/lib/gitlab/badge/pipeline/template.rb b/lib/gitlab/badge/pipeline/template.rb index 64c3dfcd10b..2c5f9654496 100644 --- a/lib/gitlab/badge/pipeline/template.rb +++ b/lib/gitlab/badge/pipeline/template.rb @@ -15,6 +15,7 @@ module Gitlab failed: '#e05d44', running: '#dfb317', pending: '#dfb317', + preparing: '#dfb317', canceled: '#9f9f9f', skipped: '#9f9f9f', unknown: '#9f9f9f' diff --git a/lib/gitlab/ci/build/prerequisite/base.rb b/lib/gitlab/ci/build/prerequisite/base.rb new file mode 100644 index 00000000000..156aa22d95b --- /dev/null +++ b/lib/gitlab/ci/build/prerequisite/base.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + module Prerequisite + class Base + include Utils::StrongMemoize + + attr_reader :build + + def initialize(build) + @build = build + end + + def unmet? + raise NotImplementedError + end + + def complete! + raise NotImplementedError + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/prerequisite/factory.rb b/lib/gitlab/ci/build/prerequisite/factory.rb new file mode 100644 index 00000000000..60cdf7af418 --- /dev/null +++ b/lib/gitlab/ci/build/prerequisite/factory.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + module Prerequisite + class Factory + attr_reader :build + + def self.prerequisites + [KubernetesNamespace] + end + + def initialize(build) + @build = build + end + + def unmet + build_prerequisites.select(&:unmet?) + end + + private + + def build_prerequisites + self.class.prerequisites.map do |prerequisite| + prerequisite.new(build) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb new file mode 100644 index 00000000000..41135ae62bb --- /dev/null +++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + module Prerequisite + class KubernetesNamespace < Base + def unmet? + deployment_cluster.present? && kubernetes_namespace.new_record? + end + + def complete! + return unless unmet? + + create_or_update_namespace + end + + private + + def deployment_cluster + build.deployment&.cluster + end + + def kubernetes_namespace + strong_memoize(:kubernetes_namespace) do + deployment_cluster.find_or_initialize_kubernetes_namespace_for_project(build.project) + end + end + + def create_or_update_namespace + Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new( + cluster: deployment_cluster, + kubernetes_namespace: kubernetes_namespace + ).execute + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/build/factory.rb b/lib/gitlab/ci/status/build/factory.rb index 6e4bfe23f2b..f7d0715e617 100644 --- a/lib/gitlab/ci/status/build/factory.rb +++ b/lib/gitlab/ci/status/build/factory.rb @@ -11,6 +11,7 @@ module Gitlab Status::Build::Manual, Status::Build::Canceled, Status::Build::Created, + Status::Build::Preparing, Status::Build::Pending, Status::Build::Skipped], [Status::Build::Cancelable, diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index d40454df737..76dfe7b7639 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -15,7 +15,8 @@ module Gitlab runner_unsupported: 'unsupported runner', stale_schedule: 'stale schedule', job_execution_timeout: 'job execution timeout', - archived_failure: 'archived failure' + archived_failure: 'archived failure', + unmet_prerequisites: 'unmet prerequisites' }.freeze private_constant :REASONS diff --git a/lib/gitlab/ci/status/build/preparing.rb b/lib/gitlab/ci/status/build/preparing.rb new file mode 100644 index 00000000000..1fddcb05f79 --- /dev/null +++ b/lib/gitlab/ci/status/build/preparing.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + module Build + class Preparing < Status::Extended + ## + # TODO: image is shared with 'pending' + # until we get a dedicated one + # + def illustration + { + image: 'illustrations/job_not_triggered.svg', + size: 'svg-306', + title: _('This job is preparing to start'), + content: _('This job is performing tasks that must complete before it can start') + } + end + + def self.matches?(build, _) + build.preparing? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/preparing.rb b/lib/gitlab/ci/status/preparing.rb new file mode 100644 index 00000000000..62985d0a9f9 --- /dev/null +++ b/lib/gitlab/ci/status/preparing.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Status + class Preparing < Status::Core + def text + s_('CiStatusText|preparing') + end + + def label + s_('CiStatusLabel|preparing') + end + + ## + # TODO: shared with 'created' + # until we get one for 'preparing' + # + def icon + 'status_created' + end + + ## + # TODO: shared with 'created' + # until we get one for 'preparing' + # + def favicon + 'favicon_status_created' + end + end + end + end +end diff --git a/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml index 9c534b2b8e7..8e767b22360 100644 --- a/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml @@ -118,4 +118,3 @@ promoteProduction: - master script: - bundle exec fastlane promote_beta_to_production -
\ No newline at end of file diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index cbe466a1c37..7ec786b6d5d 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -697,6 +697,8 @@ rollout 100%: helm upgrade --install \ --wait \ --set service.enabled="$service_enabled" \ + --set gitlab.app="$CI_PROJECT_PATH_SLUG" \ + --set gitlab.env="$CI_ENVIRONMENT_SLUG" \ --set releaseOverride="$CI_ENVIRONMENT_SLUG" \ --set image.repository="$CI_APPLICATION_REPOSITORY" \ --set image.tag="$CI_APPLICATION_TAG" \ @@ -734,6 +736,8 @@ rollout 100%: helm upgrade --install \ --wait \ --set service.enabled="$service_enabled" \ + --set gitlab.app="$CI_PROJECT_PATH_SLUG" \ + --set gitlab.env="$CI_ENVIRONMENT_SLUG" \ --set releaseOverride="$CI_ENVIRONMENT_SLUG" \ --set image.repository="$CI_APPLICATION_REPOSITORY" \ --set image.tag="$CI_APPLICATION_TAG" \ diff --git a/lib/gitlab/ci/templates/Bash.gitlab-ci.yml b/lib/gitlab/ci/templates/Bash.gitlab-ci.yml index 2d218b2e164..368069844ea 100644 --- a/lib/gitlab/ci/templates/Bash.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Bash.gitlab-ci.yml @@ -7,28 +7,28 @@ before_script: - echo "Before script section" - echo "For example you might run an update here or install a build dependency" - echo "Or perhaps you might print out some debugging details" - + after_script: - echo "After script section" - echo "For example you might do some cleanup here" - + build1: stage: build script: - echo "Do your build here" - + test1: stage: test - script: + script: - echo "Do a test here" - echo "For example run a test suite" - + test2: stage: test - script: + script: - echo "Do another parallel test here" - echo "For example run a lint test" - + deploy1: stage: deploy script: diff --git a/lib/gitlab/ci/templates/C++.gitlab-ci.yml b/lib/gitlab/ci/templates/C++.gitlab-ci.yml index c83c49d8c95..9a8fa9d7091 100644 --- a/lib/gitlab/ci/templates/C++.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/C++.gitlab-ci.yml @@ -7,9 +7,9 @@ build: stage: build # instead of calling g++ directly you can also use some build toolkit like make # install the necessary build tools when needed - # before_script: - # - apt update && apt -y install make autoconf - script: + # before_script: + # - apt update && apt -y install make autoconf + script: - g++ helloworld.cpp -o mybinary artifacts: paths: diff --git a/lib/gitlab/ci/templates/Chef.gitlab-ci.yml b/lib/gitlab/ci/templates/Chef.gitlab-ci.yml index 4d5b6484d6e..33507aa58e4 100644 --- a/lib/gitlab/ci/templates/Chef.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Chef.gitlab-ci.yml @@ -41,7 +41,7 @@ chefspec: # - apt-get -y install rsync # script: # - kitchen verify default-centos-6 --destroy=always -# +# #verify-centos-7: # stage: functional # before_script: diff --git a/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml b/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml index f066285b1ad..0610cb9ccc0 100644 --- a/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Clojure.gitlab-ci.yml @@ -5,9 +5,9 @@ image: clojure:lein-2.7.0 # Make sure you configure the connection as well before_script: - # If you need to install any external applications, like a + # If you need to install any external applications, like a # postgres client, you may want to uncomment the line below: - # + # #- apt-get update -y # # Retrieve project dependencies @@ -17,6 +17,6 @@ before_script: test: script: - # If you need to run any migrations or configure the database, this - # would be the point to do it. + # If you need to run any migrations or configure the database, this + # would be the point to do it. - lein test diff --git a/lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml new file mode 100644 index 00000000000..10b25af904f --- /dev/null +++ b/lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml @@ -0,0 +1,17 @@ +code_quality: + image: docker:stable + allow_failure: true + services: + - docker:stable-dind + variables: + DOCKER_DRIVER: overlay2 + script: + - docker run + --env SOURCE_CODE="$PWD" + --volume "$PWD":/code + --volume /var/run/docker.sock:/var/run/docker.sock + "registry.gitlab.com/gitlab-org/security-products/codequality:11-8-stable" /code + artifacts: + reports: + codequality: gl-code-quality-report.json + expire_in: 1 week diff --git a/lib/gitlab/ci/templates/Django.gitlab-ci.yml b/lib/gitlab/ci/templates/Django.gitlab-ci.yml index 57afcbbe8b5..1d8be6f017e 100644 --- a/lib/gitlab/ci/templates/Django.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Django.gitlab-ci.yml @@ -21,7 +21,7 @@ cache: # This is a basic example for a gem or script which doesn't use # services such as redis or postgres before_script: - - python -V # Print out python version for debugging + - python -V # Print out python version for debugging # Uncomment next line if your Django app needs a JS runtime: # - apt-get update -q && apt-get install nodejs -yqq - pip install -r requirements.txt diff --git a/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml b/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml index 48d98dddfad..cbf4d58bdad 100644 --- a/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Gradle.gitlab-ci.yml @@ -23,7 +23,6 @@ build: - build - .gradle - test: stage: test script: gradle check @@ -33,4 +32,3 @@ test: paths: - build - .gradle - diff --git a/lib/gitlab/ci/templates/Grails.gitlab-ci.yml b/lib/gitlab/ci/templates/Grails.gitlab-ci.yml index 7fc698d50cf..dbc868238f8 100644 --- a/lib/gitlab/ci/templates/Grails.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Grails.gitlab-ci.yml @@ -13,7 +13,7 @@ image: java:8 variables: GRAILS_VERSION: "3.1.9" GRADLE_VERSION: "2.13" - + # We use SDKMan as tool for managing versions before_script: - apt-get update -qq && apt-get install -y -qq unzip @@ -23,10 +23,10 @@ before_script: - sdk install gradle $GRADLE_VERSION < /dev/null - sdk use gradle $GRADLE_VERSION # As it's not a good idea to version gradle.properties feel free to add your -# environments variable here +# environments variable here - echo grailsVersion=$GRAILS_VERSION > gradle.properties - echo gradleWrapperVersion=2.14 >> gradle.properties -# refresh dependencies from your project +# refresh dependencies from your project - ./gradlew --refresh-dependencies # Be aware that if you are using Angular profile, # Bower cannot be run as root if you don't allow it before. @@ -36,5 +36,5 @@ before_script: # This build job does the full grails pipeline # (compile, test, integrationTest, war, assemble). build: - script: - - ./gradlew build
\ No newline at end of file + script: + - ./gradlew build diff --git a/lib/gitlab/ci/templates/Julia.gitlab-ci.yml b/lib/gitlab/ci/templates/Julia.gitlab-ci.yml index 04c21b4725d..2c4683fbfbb 100644 --- a/lib/gitlab/ci/templates/Julia.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Julia.gitlab-ci.yml @@ -30,7 +30,7 @@ test:0.7: image: julia:0.7 <<: *test_definition - + test:1.0: image: julia:1.0 <<: *test_definition diff --git a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml index d0cad285572..e1cd29ecc94 100644 --- a/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Laravel.gitlab-ci.yml @@ -22,33 +22,25 @@ cache: # This is a basic example for a gem or script which doesn't use # services such as redis or postgres before_script: - # Update packages + # Update packages - apt-get update -yqq - # Prep for Node - apt-get install gnupg -yqq - # Upgrade to Node 8 - curl -sL https://deb.nodesource.com/setup_8.x | bash - - # Install dependencies - apt-get install git nodejs libcurl4-gnutls-dev libicu-dev libmcrypt-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libpq-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev -yqq - # Install php extensions - docker-php-ext-install mbstring pdo_mysql curl json intl gd xml zip bz2 opcache - # Install & enable Xdebug for code coverage reports - pecl install xdebug - docker-php-ext-enable xdebug - # Install Composer and project dependencies. - curl -sS https://getcomposer.org/installer | php - php composer.phar install - # Install Node dependencies. # comment this out if you don't have a node dependency - npm install - # Copy over testing configuration. # Don't forget to set the database config in .env.testing correctly # DB_HOST=mysql @@ -56,20 +48,16 @@ before_script: # DB_USERNAME=root # DB_PASSWORD=secret - cp .env.testing .env - # Run npm build # comment this out if you don't have a frontend build # you can change this to to your frontend building script like # npm run build - npm run dev - # Generate an application key. Re-cache. - php artisan key:generate - php artisan config:cache - # Run database migrations. - php artisan migrate - # Run database seed - php artisan db:seed @@ -77,7 +65,6 @@ test: script: # run laravel tests - php vendor/bin/phpunit --coverage-text --colors=never - # run frontend tests # if you have any task for testing frontend # set it in your package.json script diff --git a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml index 492b3d03db2..c9838c7a7ff 100644 --- a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml @@ -66,7 +66,6 @@ verify:jdk8: <<: *verify image: maven:3.3.9-jdk-8 - # For `master` branch run `mvn deploy` automatically. # Here you need to decide whether you want to use JDK7 or 8. # To get this working you need to define a volume while configuring your gitlab-ci-multi-runner. @@ -85,7 +84,6 @@ deploy:jdk8: - target/staging image: maven:3.3.9-jdk-8 - pages: image: busybox:latest stage: deploy diff --git a/lib/gitlab/ci/templates/Mono.gitlab-ci.yml b/lib/gitlab/ci/templates/Mono.gitlab-ci.yml index 3585f99760f..86d62b93313 100644 --- a/lib/gitlab/ci/templates/Mono.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Mono.gitlab-ci.yml @@ -32,11 +32,11 @@ release: # The output path is relative to the position of the csproj-file - msbuild /p:Configuration="Release" /p:Platform="Any CPU" /p:OutputPath="./../../build/release/" "MyProject.sln" - + debug: stage: test script: # The output path is relative to the position of the csproj-file - msbuild /p:Configuration="Debug" /p:Platform="Any CPU" /p:OutputPath="./../../build/debug/" "MyProject.sln" - - mono packages/NUnit.ConsoleRunner.3.6.0/tools/nunit3-console.exe build/debug/MyProject.Test.dll
\ No newline at end of file + - mono packages/NUnit.ConsoleRunner.3.6.0/tools/nunit3-console.exe build/debug/MyProject.Test.dll diff --git a/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml index 7fcc0b436b5..d6de8cab5d1 100644 --- a/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Pages/Brunch.gitlab-ci.yml @@ -5,7 +5,6 @@ pages: cache: paths: - node_modules/ - script: - npm install -g brunch - brunch build --production diff --git a/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml index dd3ef149668..4b58003ee10 100644 --- a/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Pages/Harp.gitlab-ci.yml @@ -5,7 +5,6 @@ pages: cache: paths: - node_modules - script: - npm install -g harp - harp compile ./ public diff --git a/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml index b8cfb0f56f6..f9ddcc6fb0a 100644 --- a/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Pages/Hugo.gitlab-ci.yml @@ -9,7 +9,7 @@ pages: - public only: - master - + test: script: - hugo diff --git a/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml index 7abfaf53e8e..7a485f8d135 100644 --- a/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Pages/JBake.gitlab-ci.yml @@ -13,7 +13,6 @@ image: java:8 variables: JBAKE_VERSION: 2.5.1 - # We use SDKMan as tool for managing versions before_script: - apt-get update -qq && apt-get install -y -qq unzip zip @@ -29,4 +28,4 @@ pages: - jbake . public artifacts: paths: - - public
\ No newline at end of file + - public diff --git a/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml index 0e5fb410a4e..5ca4619e200 100644 --- a/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Pages/Jigsaw.gitlab-ci.yml @@ -11,24 +11,19 @@ cache: - node_modules/ before_script: - # Update packages + # Update packages - apt-get update -yqq - # Install dependencies - apt-get install -yqq gnupg zlib1g-dev libpng-dev - # Install Node 8 - curl -sL https://deb.nodesource.com/setup_8.x | bash - - apt-get install -yqq nodejs - # Install php extensions - docker-php-ext-install zip - - # Install Composer and project dependencies. + # Install Composer and project dependencies - curl -sS https://getcomposer.org/installer | php - - php composer.phar install - - # Install Node dependencies. + - php composer.phar install + # Install Node dependencies - npm install pages: diff --git a/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml b/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml index 50e8b7ccd46..c6ded272150 100644 --- a/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Pages/Metalsmith.gitlab-ci.yml @@ -5,7 +5,6 @@ pages: cache: paths: - node_modules/ - script: - npm install -g metalsmith - npm install diff --git a/lib/gitlab/ci/templates/Python.gitlab-ci.yml b/lib/gitlab/ci/templates/Python.gitlab-ci.yml index 098abe4daf5..3eaed4e91cd 100644 --- a/lib/gitlab/ci/templates/Python.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Python.gitlab-ci.yml @@ -18,7 +18,7 @@ cache: - venv/ before_script: - - python -V # Print out python version for debugging + - python -V # Print out python version for debugging - pip install virtualenv - virtualenv venv - source venv/bin/activate @@ -26,7 +26,7 @@ before_script: test: script: - python setup.py test - - pip install tox flake8 # you can also use tox + - pip install tox flake8 # you can also use tox - tox -e py36,flake8 run: diff --git a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml index 0d12cbc6460..93196dbd475 100644 --- a/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Ruby.gitlab-ci.yml @@ -21,7 +21,7 @@ cache: # This is a basic example for a gem or script which doesn't use # services such as redis or postgres before_script: - - ruby -v # Print out ruby version for debugging + - ruby -v # Print out ruby version for debugging # Uncomment next line if your rails app needs a JS runtime: # - apt-get update -q && apt-get install nodejs -yqq - bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby diff --git a/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml b/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml index 25a32ba0f74..5e128b793d0 100644 --- a/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/dotNET.gitlab-ci.yml @@ -26,7 +26,6 @@ variables: MSI_RELEASE_FOLDER: 'Setup\bin\Release' TEST_FOLDER: 'Tests\bin\Release' DEPLOY_FOLDER: 'P:\Projects\YourApp\Builds' - NUGET_PATH: 'C:\NuGet\nuget.exe' MSBUILD_PATH: 'C:\Program Files (x86)\MSBuild\14.0\Bin\msbuild.exe' NUNIT_PATH: 'C:\Program Files (x86)\NUnit.org\nunit-console\nunit3-console.exe' @@ -84,4 +83,3 @@ deploy_job: dependencies: - build_job - test_job -
\ No newline at end of file diff --git a/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml b/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml index 245e6bec60a..df6ac4d340d 100644 --- a/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/iOS-Fastlane.gitlab-ci.yml @@ -17,7 +17,7 @@ variables: LC_ALL: "en_US.UTF-8" LANG: "en_US.UTF-8" GIT_STRATEGY: clone - + build: stage: build script: diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index e5bbd500e98..8fac3621df9 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -184,11 +184,12 @@ module Gitlab end end - def initialize(repository, raw_commit, head = nil) + def initialize(repository, raw_commit, head = nil, lazy_load_parents: false) raise "Nil as raw commit passed" unless raw_commit @repository = repository @head = head + @lazy_load_parents = lazy_load_parents init_commit(raw_commit) end @@ -225,6 +226,12 @@ module Gitlab author_name != committer_name || author_email != committer_email end + def parent_ids + return @parent_ids unless @lazy_load_parents + + @parent_ids ||= @repository.commit(id).parent_ids + end + def parent_id parent_ids.first end @@ -311,6 +318,10 @@ module Gitlab parent_ids.size > 1 end + def gitaly_commit? + raw_commit.is_a?(Gitaly::GitCommit) + end + def tree_entry(path) return unless path.present? @@ -333,7 +344,7 @@ module Gitlab end def to_gitaly_commit - return raw_commit if raw_commit.is_a?(Gitaly::GitCommit) + return raw_commit if gitaly_commit? message_split = raw_commit.message.split("\n", 2) Gitaly::GitCommit.new( diff --git a/lib/gitlab/json_cache.rb b/lib/gitlab/json_cache.rb index 24daad638f4..e4bc437d787 100644 --- a/lib/gitlab/json_cache.rb +++ b/lib/gitlab/json_cache.rb @@ -80,8 +80,23 @@ module Gitlab # when the new_record? method incorrectly returns false. # # See https://gitlab.com/gitlab-org/gitlab-ee/issues/9903#note_145329964 - attributes = klass.attributes_builder.build_from_database(raw, {}) - klass.allocate.init_with("attributes" => attributes, "new_record" => new_record?(raw, klass)) + klass + .allocate + .init_with( + "attributes" => attributes_for(klass, raw), + "new_record" => new_record?(raw, klass) + ) + end + + def attributes_for(klass, raw) + # We have models that leave out some fields from the JSON export for + # security reasons, e.g. models that include the CacheMarkdownField. + # The ActiveRecord::AttributeSet we build from raw does know about + # these columns so we need manually set them. + missing_attributes = (klass.columns.map(&:name) - raw.keys) + missing_attributes.each { |column| raw[column] = nil } + + klass.attributes_builder.build_from_database(raw, {}) end def new_record?(raw, klass) diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb index a9957a85d48..d46b5e3aee3 100644 --- a/lib/gitlab/kubernetes.rb +++ b/lib/gitlab/kubernetes.rb @@ -24,6 +24,30 @@ module Gitlab end end + # Filters an array of pods (as returned by the kubernetes API) by their annotations + def filter_by_annotation(items, annotations = {}) + items.select do |item| + metadata = item.fetch("metadata", {}) + item_annotations = metadata.fetch("annotations", nil) + next unless item_annotations + + annotations.all? { |k, v| item_annotations[k.to_s] == v } + end + end + + # Filters an array of pods (as returned by the kubernetes API) by their project and environment + def filter_by_project_environment(items, app, env) + pods = filter_by_annotation(items, { + 'app.gitlab.com/app' => app, + 'app.gitlab.com/env' => env + }) + return pods unless pods.empty? + + filter_by_label(items, { + 'app' => env, # deprecated: replaced by app.gitlab.com/env + }) + end + # Converts a pod (as returned by the kubernetes API) into a terminal def terminals_for_pod(api_url, namespace, pod) metadata = pod.fetch("metadata", {}) diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb index 49ec196b103..5e0c9101de5 100644 --- a/lib/sentry/client.rb +++ b/lib/sentry/client.rb @@ -57,7 +57,7 @@ module Sentry raise Client::Error, "Sentry response status code: #{response.code}" end - response.as_json + response end def projects_api_url diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d9b50e5e3b9..0dfcf62b36d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19,6 +19,9 @@ msgstr "" msgid " Status" msgstr "" +msgid " You need to do this before %{grace_period_deadline}." +msgstr "" + msgid " or " msgstr "" @@ -1380,6 +1383,9 @@ msgstr "" msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded." msgstr "" +msgid "Cannot skip two factor authentication setup" +msgstr "" + msgid "Certificate" msgstr "" @@ -1512,6 +1518,9 @@ msgstr "" msgid "CiStatusLabel|pending" msgstr "" +msgid "CiStatusLabel|preparing" +msgstr "" + msgid "CiStatusLabel|skipped" msgstr "" @@ -1545,6 +1554,9 @@ msgstr "" msgid "CiStatusText|pending" msgstr "" +msgid "CiStatusText|preparing" +msgstr "" + msgid "CiStatusText|skipped" msgstr "" @@ -4328,6 +4340,9 @@ msgstr "" msgid "Invalid input, please avoid emojis" msgstr "" +msgid "Invalid pin code" +msgstr "" + msgid "Invitation" msgstr "" @@ -5977,7 +5992,7 @@ msgstr "" msgid "Profiles|This email will be displayed on your public profile" msgstr "" -msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}" +msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}" msgstr "" msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." @@ -7612,6 +7627,12 @@ msgstr "" msgid "The fork relationship has been removed." msgstr "" +msgid "The global settings require you to enable Two-Factor Authentication for your account." +msgstr "" + +msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}." +msgstr "" + msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination." msgstr "" @@ -7864,6 +7885,12 @@ msgstr "" msgid "This job is in pending state and is waiting to be picked by a runner" msgstr "" +msgid "This job is performing tasks that must complete before it can start" +msgstr "" + +msgid "This job is preparing to start" +msgstr "" + msgid "This job is stuck because you don't have any active runners online with any of these tags assigned to them:" msgstr "" @@ -9008,6 +9035,12 @@ msgstr "" msgid "Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left." msgstr "" +msgid "Your U2F device was registered!" +msgstr "" + +msgid "Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO." +msgstr "" + msgid "Your applications (%{size})" msgstr "" @@ -9195,6 +9228,9 @@ msgstr "" msgid "latest version" msgstr "" +msgid "leave %{group_name}" +msgstr "" + msgid "manual" msgstr "" diff --git a/package.json b/package.json index fa980fa173e..d830d83b963 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "prettier-all": "node ./scripts/frontend/prettier.js check-all", "prettier-all-save": "node ./scripts/frontend/prettier.js save-all", "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.* --custom-formatter node_modules/stylelint-error-string-formatter", + "test": "yarn jest && yarn karma", "webpack": "webpack --config config/webpack.config.js", "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, 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 16aca7c0e22..0b92ea29ca4 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 @@ -116,12 +116,6 @@ module QA p.description = 'Project with AutoDevOps' end - project.visit! - - Page::Alert::AutoDevopsAlert.perform do |alert| - expect(alert).to have_text(/.*The Auto DevOps pipeline has been enabled.*/) - end - # Create AutoDevOps repo Resource::Repository::ProjectPush.fabricate! do |push| push.project = project diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index e0da23ca0b8..06c6f49f7cc 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -113,6 +113,33 @@ describe OmniauthCallbacksController, type: :controller do expect(request.env['warden']).to be_authenticated end + context 'when user has no linked provider' do + let(:user) { create(:user) } + + before do + sign_in user + end + + it 'links identity' do + expect do + post provider + user.reload + end.to change { user.identities.count }.by(1) + end + + context 'and is not allowed to link the provider' do + before do + allow_any_instance_of(IdentityProviderPolicy).to receive(:can?).with(:link).and_return(false) + end + + it 'returns 403' do + post provider + + expect(response).to have_gitlab_http_status(403) + end + end + end + shared_context 'sign_up' do let(:user) { double(email: 'new@example.com') } diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 0b0f5117784..deecb7fefe9 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -413,6 +413,37 @@ describe Projects::NotesController do end end end + + context 'when creating a note with quick actions' do + context 'with commands that return changes' do + let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" } + + it 'includes changes in commands_changes ' do + post :create, params: request_params.merge(note: { note: note_text }, format: :json) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['commands_changes']).to include('emoji_award', 'time_estimate', 'spend_time') + expect(json_response['commands_changes']).not_to include('target_project', 'title') + end + end + + context 'with commands that do not return changes' do + let(:issue) { create(:issue, project: project) } + let(:other_project) { create(:project) } + let(:note_text) { "/move #{other_project.full_path}\n/title AAA" } + + before do + other_project.add_developer(user) + end + + it 'does not include changes in commands_changes' do + post :create, params: request_params.merge(note: { note: note_text }, target_type: 'issue', target_id: issue.id, format: :json) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['commands_changes']).not_to include('target_project', 'title') + end + end + end end describe 'PUT update' do diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0b3e67b4987..067391c1179 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -75,6 +75,10 @@ FactoryBot.define do status 'created' end + trait :preparing do + status 'preparing' + end + trait :scheduled do schedulable status 'scheduled' diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index ee5d27355f1..aa5ccbda6cd 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -50,6 +50,14 @@ FactoryBot.define do failure_reason :config_error end + trait :created do + status :created + end + + trait :preparing do + status :preparing + end + trait :blocked do status :manual end diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index a2e5f4862db..1cc3c0e03d8 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -12,7 +12,7 @@ FactoryBot.define do cluster_type { Clusters::Cluster.cluster_types[:project_type] } before(:create) do |cluster, evaluator| - cluster.projects << create(:project, :repository) + cluster.projects << create(:project, :repository) unless cluster.projects.present? end end @@ -20,7 +20,7 @@ FactoryBot.define do cluster_type { Clusters::Cluster.cluster_types[:group_type] } before(:create) do |cluster, evalutor| - cluster.groups << create(:group) + cluster.groups << create(:group) unless cluster.groups.present? end end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 381bf07f6a0..848a31e96c1 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -33,6 +33,10 @@ FactoryBot.define do status 'pending' end + trait :preparing do + status 'preparing' + end + trait :created do status 'created' end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 986f3823275..8eb413bdd8d 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -278,12 +278,7 @@ describe 'GFM autocomplete', :js do end end - # This context has just one example in each contexts in order to improve spec performance. - context 'labels', :quarantine do - let!(:backend) { create(:label, project: project, title: 'backend') } - let!(:bug) { create(:label, project: project, title: 'bug') } - let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') } - + context 'labels' do it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do create(:label, project: project, title: label_xss_title) @@ -298,83 +293,6 @@ describe 'GFM autocomplete', :js do expect(find('.atwho-view-ul').text).to have_content('alert label') end end - - context 'when no labels are assigned' do - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/label ~". - type(note, '/label ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show no labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(not_shown: [backend, bug, feature_proposal]) - end - end - - context 'when some labels are assigned' do - before do - issue.labels << [backend] - end - - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show only unset labels on "/label ~". - type(note, '/label ~') - expect_labels(shown: [bug, feature_proposal], not_shown: [backend]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show only set labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(shown: [backend], not_shown: [bug, feature_proposal]) - end - end - - context 'when all labels are assigned' do - before do - issue.labels << [backend, bug, feature_proposal] - end - - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show no labels on "/label ~". - type(note, '/label ~') - expect_labels(not_shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - end - end end shared_examples 'autocomplete suggestions' do diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 9bc340ed4bb..51508b78649 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -497,12 +497,21 @@ describe 'Issues' do it 'allows user to unselect themselves', :js do issue2 = create(:issue, project: project, author: user) + visit project_issue_path(project, issue2) + def close_dropdown_menu_if_visible + find('.dropdown-menu-toggle', visible: :all).tap do |toggle| + toggle.click if toggle.visible? + end + end + page.within '.assignee' do click_link 'Edit' click_link user.name + close_dropdown_menu_if_visible + page.within '.value .author' do expect(page).to have_content user.name end @@ -510,6 +519,8 @@ describe 'Issues' do click_link 'Edit' click_link user.name + close_dropdown_menu_if_visible + page.within '.value .assign-yourself' do expect(page).to have_content "No assignee" end diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb index dee81898928..4ac4e8f0fcb 100644 --- a/spec/features/projects/badges/pipeline_badge_spec.rb +++ b/spec/features/projects/badges/pipeline_badge_spec.rb @@ -41,6 +41,25 @@ describe 'Pipeline Badge' do end end + context 'when the pipeline is preparing' do + let!(:job) { create(:ci_build, status: 'created', pipeline: pipeline) } + + before do + # Prevent skipping directly to 'pending' + allow(Ci::BuildPrepareWorker).to receive(:perform_async) + allow(job).to receive(:prerequisites).and_return([double]) + end + + it 'displays the preparing badge' do + job.enqueue + + visit pipeline_project_badges_path(project, ref: ref, format: :svg) + + expect(page.status_code).to eq(200) + expect_badge('preparing') + end + end + context 'when the pipeline is running' do it 'shows displays so on the badge' do create(:ci_build, pipeline: pipeline, name: 'second build', status_event: 'run') diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 5d2a99f40b7..9fdf78baa1e 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -24,6 +24,11 @@ describe 'Pipeline', :js do pipeline: pipeline, stage: 'test', name: 'test') end + let!(:build_preparing) do + create(:ci_build, :preparing, + pipeline: pipeline, stage: 'deploy', name: 'prepare') + end + let!(:build_running) do create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') @@ -109,6 +114,24 @@ describe 'Pipeline', :js do end end + context 'when pipeline has preparing builds' do + it 'shows a preparing icon and a cancel action' do + page.within('#ci-badge-prepare') do + expect(page).to have_selector('.js-ci-status-icon-preparing') + expect(page).to have_selector('.js-icon-cancel') + expect(page).to have_content('prepare') + end + end + + it 'cancels the preparing build and shows retry button' do + find('#ci-badge-deploy .ci-action-icon-container').click + + page.within('#ci-badge-deploy') do + expect(page).to have_css('.js-icon-retry') + end + end + end + context 'when pipeline has successful builds' do it 'shows the success icon and a retry action for the successful build' do page.within('#ci-badge-build') do diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 0e151efa9df..7ca3b3d8edd 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -377,6 +377,30 @@ describe 'Pipelines', :js do end context 'for generic statuses' do + context 'when preparing' do + let!(:pipeline) do + create(:ci_empty_pipeline, + status: 'preparing', project: project) + end + + let!(:status) do + create(:generic_commit_status, + :preparing, pipeline: pipeline) + end + + before do + visit_project_pipelines + end + + it 'is cancelable' do + expect(page).to have_selector('.js-pipelines-cancel-button') + end + + it 'shows the pipeline as preparing' do + expect(page).to have_selector('.ci-preparing') + end + end + context 'when running' do let!(:running) do create(:generic_commit_status, diff --git a/spec/features/user_opens_link_to_comment.rb b/spec/features/user_opens_link_to_comment.rb new file mode 100644 index 00000000000..f1e07e55799 --- /dev/null +++ b/spec/features/user_opens_link_to_comment.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'User opens link to comment', :js do + let(:project) { create(:project, :public) } + let(:note) { create(:note_on_issue, project: project) } + + context 'authenticated user' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'switches to all activity and does not show error message' do + create(:user_preference, user: user, issue_notes_filter: UserPreference::NOTES_FILTERS[:only_activity]) + + visit Gitlab::UrlBuilder.build(note) + + expect(page.find('#discussion-filter-dropdown')).to have_content('Show all activity') + expect(page).not_to have_content('Something went wrong while fetching comments') + end + end + + context 'anonymous user' do + it 'does not show error message' do + visit Gitlab::UrlBuilder.build(note) + + expect(page).not_to have_content('Something went wrong while fetching comments') + end + end +end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index ad856bd062e..368a814874f 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -434,16 +434,22 @@ describe 'Login' do context 'within the grace period' do it 'redirects to two-factor configuration page' do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - - gitlab_sign_in(user) - - expect(current_path).to eq profile_two_factor_auth_path - expect(page).to have_content( - 'The group settings for Group 1 and Group 2 require you to enable ' \ - 'Two-Factor Authentication for your account. You need to do this ' \ - 'before ') + Timecop.freeze do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + + gitlab_sign_in(user) + + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content( + 'The group settings for Group 1 and Group 2 require you to enable '\ + 'Two-Factor Authentication for your account. '\ + 'You can leave Group 1 and leave Group 2. '\ + 'You need to do this '\ + 'before '\ + "#{(Time.zone.now + 2.days).strftime("%a, %-d %b %Y %H:%M:%S %z")}" + ) + end end it 'allows skipping two-factor configuration', :js do @@ -500,7 +506,8 @@ describe 'Login' do expect(current_path).to eq profile_two_factor_auth_path expect(page).to have_content( 'The group settings for Group 1 and Group 2 require you to enable ' \ - 'Two-Factor Authentication for your account.' + 'Two-Factor Authentication for your account. '\ + 'You can leave Group 1 and leave Group 2.' ) end end diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 27c679c749b..ed12af925f1 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -6,17 +6,26 @@ import GfmAutoComplete from '~/gfm_auto_complete'; import 'jquery.caret'; import 'at.js'; +import { TEST_HOST } from 'helpers/test_constants'; +import { setTestTimeout } from 'helpers/timeout'; +import { getJSONFixture } from 'helpers/fixtures'; + +setTestTimeout(500); + +const labelsFixture = getJSONFixture('autocomplete_sources/labels.json'); + describe('GfmAutoComplete', () => { const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ fetchData: () => {}, }); let atwhoInstance; - let items; let sorterValue; describe('DefaultOptions.sorter', () => { describe('assets loading', () => { + let items; + beforeEach(() => { jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(true); @@ -61,7 +70,7 @@ describe('GfmAutoComplete', () => { atwhoInstance = { setting: {} }; const query = 'query'; - items = []; + const items = []; const searchKey = 'searchKey'; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey); @@ -250,4 +259,90 @@ describe('GfmAutoComplete', () => { ).toBe('<li><small>grp/proj#5</small> Some Issue</li>'); }); }); + + describe('labels', () => { + const dataSources = { + labels: `${TEST_HOST}/autocomplete_sources/labels`, + }; + + const allLabels = labelsFixture; + const assignedLabels = allLabels.filter(label => label.set); + const unassignedLabels = allLabels.filter(label => !label.set); + + let autocomplete; + let $textarea; + + beforeEach(() => { + autocomplete = new GfmAutoComplete(dataSources); + $textarea = $('<textarea></textarea>'); + autocomplete.setup($textarea, { labels: true }); + }); + + afterEach(() => { + autocomplete.destroy(); + }); + + const triggerDropdown = text => { + $textarea + .trigger('focus') + .val(text) + .caret('pos', -1); + $textarea.trigger('keyup'); + + return new Promise(window.requestAnimationFrame); + }; + + const getDropdownItems = () => { + const dropdown = document.getElementById('at-view-labels'); + const items = dropdown.getElementsByTagName('li'); + return [].map.call(items, item => item.textContent.trim()); + }; + + const expectLabels = ({ input, output }) => + triggerDropdown(input).then(() => { + expect(getDropdownItems()).toEqual(output.map(label => label.title)); + }); + + describe('with no labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = [...unassignedLabels]; + }); + + it.each` + input | output + ${'~'} | ${unassignedLabels} + ${'/label ~'} | ${unassignedLabels} + ${'/relabel ~'} | ${unassignedLabels} + ${'/unlabel ~'} | ${[]} + `('$input shows $output.length labels', expectLabels); + }); + + describe('with some labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = allLabels; + }); + + it.each` + input | output + ${'~'} | ${allLabels} + ${'/label ~'} | ${unassignedLabels} + ${'/relabel ~'} | ${allLabels} + ${'/unlabel ~'} | ${assignedLabels} + `('$input shows $output.length labels', expectLabels); + }); + + describe('with all labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = [...assignedLabels]; + }); + + it.each` + input | output + ${'~'} | ${assignedLabels} + ${'/label ~'} | ${[]} + ${'/relabel ~'} | ${assignedLabels} + ${'/unlabel ~'} | ${assignedLabels} + `('$input shows $output.length labels', expectLabels); + }); + }); }); diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js new file mode 100644 index 00000000000..f96f27c4d80 --- /dev/null +++ b/spec/frontend/helpers/fixtures.js @@ -0,0 +1,24 @@ +/* eslint-disable import/prefer-default-export, global-require, import/no-dynamic-require */ + +import fs from 'fs'; +import path from 'path'; + +// jest-util is part of Jest +// eslint-disable-next-line import/no-extraneous-dependencies +import { ErrorWithStack } from 'jest-util'; + +const fixturesBasePath = path.join(process.cwd(), 'spec', 'javascripts', 'fixtures'); + +export function getJSONFixture(relativePath, ee = false) { + const absolutePath = path.join(fixturesBasePath, ee ? 'ee' : '', relativePath); + if (!fs.existsSync(absolutePath)) { + throw new ErrorWithStack( + `Fixture file ${relativePath} does not exist. + +Did you run bin/rake karma:fixtures?`, + getJSONFixture, + ); + } + + return require(absolutePath); +} diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index f0c2e4768ec..2ba8b3dbf22 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -97,17 +97,37 @@ describe AuthHelper do end end - describe 'unlink_allowed?' do - [:saml, :cas3].each do |provider| - it "returns true if the provider is #{provider}" do - expect(helper.unlink_allowed?(provider)).to be false - end + describe '#link_provider_allowed?' do + let(:policy) { instance_double('IdentityProviderPolicy') } + let(:current_user) { instance_double('User') } + let(:provider) { double } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + allow(IdentityProviderPolicy).to receive(:new).with(current_user, provider).and_return(policy) end - [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq].each do |provider| - it "returns false if the provider is #{provider}" do - expect(helper.unlink_allowed?(provider)).to be true - end + it 'delegates to identity provider policy' do + allow(policy).to receive(:can?).with(:link).and_return('policy_link_result') + + expect(helper.link_provider_allowed?(provider)).to eq 'policy_link_result' + end + end + + describe '#unlink_provider_allowed?' do + let(:policy) { instance_double('IdentityProviderPolicy') } + let(:current_user) { instance_double('User') } + let(:provider) { double } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + allow(IdentityProviderPolicy).to receive(:new).with(current_user, provider).and_return(policy) + end + + it 'delegates to identity provider policy' do + allow(policy).to receive(:can?).with(:unlink).and_return('policy_unlink_result') + + expect(helper.unlink_provider_allowed?(provider)).to eq 'policy_unlink_result' end end end diff --git a/spec/javascripts/fixtures/autocomplete_sources.rb b/spec/javascripts/fixtures/autocomplete_sources.rb new file mode 100644 index 00000000000..c117fb7cd24 --- /dev/null +++ b/spec/javascripts/fixtures/autocomplete_sources.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + set(:admin) { create(:admin) } + set(:group) { create(:group, name: 'frontend-fixtures') } + set(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') } + set(:issue) { create(:issue, project: project) } + + before(:all) do + clean_frontend_fixtures('autocomplete_sources/') + end + + before do + sign_in(admin) + end + + it 'autocomplete_sources/labels.json' do |example| + issue.labels << create(:label, project: project, title: 'bug') + issue.labels << create(:label, project: project, title: 'critical') + + create(:label, project: project, title: 'feature') + create(:label, project: project, title: 'documentation') + + get :labels, + format: :json, + params: { + namespace_id: group.path, + project_id: project.path, + type: issue.class.name, + type_id: issue.id + } + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/badge/pipeline/template_spec.rb index 20fa4f879c3..bcef0b7e120 100644 --- a/spec/lib/gitlab/badge/pipeline/template_spec.rb +++ b/spec/lib/gitlab/badge/pipeline/template_spec.rb @@ -59,6 +59,16 @@ describe Gitlab::Badge::Pipeline::Template do end end + context 'when status is preparing' do + before do + allow(badge).to receive(:status).and_return('preparing') + end + + it 'has expected color' do + expect(template.value_color).to eq '#dfb317' + end + end + context 'when status is unknown' do before do allow(badge).to receive(:status).and_return('unknown') diff --git a/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb new file mode 100644 index 00000000000..5187f99a441 --- /dev/null +++ b/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Prerequisite::Factory do + let(:build) { create(:ci_build) } + + describe '.for_build' do + let(:kubernetes_namespace) do + instance_double( + Gitlab::Ci::Build::Prerequisite::KubernetesNamespace, + unmet?: unmet) + end + + subject { described_class.new(build).unmet } + + before do + expect(Gitlab::Ci::Build::Prerequisite::KubernetesNamespace) + .to receive(:new).with(build).and_return(kubernetes_namespace) + end + + context 'prerequisite is unmet' do + let(:unmet) { true } + + it { is_expected.to eq [kubernetes_namespace] } + end + + context 'prerequisite is met' do + let(:unmet) { false } + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb new file mode 100644 index 00000000000..62dcd80fad7 --- /dev/null +++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do + let(:build) { create(:ci_build) } + + describe '#unmet?' do + subject { described_class.new(build).unmet? } + + context 'build has no deployment' do + before do + expect(build.deployment).to be_nil + end + + it { is_expected.to be_falsey } + end + + context 'build has a deployment' do + let!(:deployment) { create(:deployment, deployable: build) } + + context 'and a cluster to deploy to' do + let(:cluster) { create(:cluster, projects: [build.project]) } + + before do + allow(build.deployment).to receive(:cluster).and_return(cluster) + end + + it { is_expected.to be_truthy } + + context 'and a namespace is already created for this project' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: build.project) } + + it { is_expected.to be_falsey } + end + end + + context 'and no cluster to deploy to' do + before do + expect(deployment.cluster).to be_nil + end + + it { is_expected.to be_falsey } + end + end + end + + describe '#complete!' do + let!(:deployment) { create(:deployment, deployable: build) } + let(:service) { double(execute: true) } + + subject { described_class.new(build).complete! } + + context 'completion is required' do + let(:cluster) { create(:cluster, projects: [build.project]) } + + before do + allow(build.deployment).to receive(:cluster).and_return(cluster) + end + + it 'creates a kubernetes namespace' do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService) + .to receive(:new) + .with(cluster: cluster, kubernetes_namespace: instance_of(Clusters::KubernetesNamespace)) + .and_return(service) + + expect(service).to receive(:execute).once + + subject + end + end + + context 'completion is not required' do + before do + expect(deployment.cluster).to be_nil + end + + it 'does not create a namespace' do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new) + + subject + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/preparing_spec.rb b/spec/lib/gitlab/ci/status/build/preparing_spec.rb new file mode 100644 index 00000000000..4d8945845ba --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/preparing_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Preparing do + subject do + described_class.new(double('subject')) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + subject { described_class.matches?(build, nil) } + + context 'when build is preparing' do + let(:build) { create(:ci_build, :preparing) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not preparing' do + let(:build) { create(:ci_build, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/preparing_spec.rb b/spec/lib/gitlab/ci/status/preparing_spec.rb new file mode 100644 index 00000000000..7211c0e506d --- /dev/null +++ b/spec/lib/gitlab/ci/status/preparing_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::Preparing do + subject do + described_class.new(double('subject'), nil) + end + + describe '#text' do + it { expect(subject.text).to eq 'preparing' } + end + + describe '#label' do + it { expect(subject.label).to eq 'preparing' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'status_created' } + end + + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_created' } + end + + describe '#group' do + it { expect(subject.group).to eq 'preparing' } + end +end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 3fb41a626b2..4a4ac833e39 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -537,6 +537,18 @@ describe Gitlab::Git::Commit, :seed_helper do end end + describe '#gitaly_commit?' do + context 'when the commit data comes from gitaly' do + it { expect(commit.gitaly_commit?).to eq(true) } + end + + context 'when the commit data comes from a Hash' do + let(:commit) { described_class.new(repository, sample_commit_hash) } + + it { expect(commit.gitaly_commit?).to eq(false) } + end + end + describe '#has_zero_stats?' do it { expect(commit.has_zero_stats?).to eq(false) } end diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb index 2cae8ec031a..b82c09af306 100644 --- a/spec/lib/gitlab/json_cache_spec.rb +++ b/spec/lib/gitlab/json_cache_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::JsonCache do let(:namespace) { 'geo' } let(:key) { 'foo' } let(:expanded_key) { "#{namespace}:#{key}:#{Rails.version}" } - let(:broadcast_message) { create(:broadcast_message) } + set(:broadcast_message) { create(:broadcast_message) } subject(:cache) { described_class.new(namespace: namespace, backend: backend) } @@ -146,6 +146,18 @@ describe Gitlab::JsonCache do expect(cache.read(key, BroadcastMessage)).to be_nil end + + it 'gracefully handles excluded fields from attributes during serialization' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.except("message_html").to_json) + + result = cache.read(key, BroadcastMessage) + + BroadcastMessage.cached_markdown_fields.html_fields.each do |field| + expect(result.public_send(field)).to be_nil + end + end end context 'when the cached value is an array' do @@ -321,6 +333,46 @@ describe Gitlab::JsonCache do expect(result).to be_new_record end + + it 'gracefully handles bad cached entry' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return('{') + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to eq 'block result' + end + + it 'gracefully handles an empty hash' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return('{}') + + expect(cache.fetch(key, as: BroadcastMessage)).to be_a(BroadcastMessage) + end + + it 'gracefully handles unknown attributes' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.merge(unknown_attribute: 1).to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to eq 'block result' + end + + it 'gracefully handles excluded fields from attributes during serialization' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.except("message_html").to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + BroadcastMessage.cached_markdown_fields.html_fields.each do |field| + expect(result.public_send(field)).to be_nil + end + end end it "returns the result of the block when 'as' option is nil" do diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb index f326d57e9c6..57b570a9166 100644 --- a/spec/lib/gitlab/kubernetes_spec.rb +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -40,10 +40,40 @@ describe Gitlab::Kubernetes do describe '#filter_by_label' do it 'returns matching labels' do - matching_items = [kube_pod(app: 'foo')] + matching_items = [kube_pod(track: 'foo'), kube_deployment(track: 'foo')] + items = matching_items + [kube_pod, kube_deployment] + + expect(filter_by_label(items, 'track' => 'foo')).to eq(matching_items) + end + end + + describe '#filter_by_annotation' do + it 'returns matching labels' do + matching_items = [kube_pod(environment_slug: 'foo'), kube_deployment(environment_slug: 'foo')] + items = matching_items + [kube_pod, kube_deployment] + + expect(filter_by_annotation(items, 'app.gitlab.com/env' => 'foo')).to eq(matching_items) + end + end + + describe '#filter_by_project_environment' do + let(:matching_pod) { kube_pod(environment_slug: 'production', project_slug: 'my-cool-app') } + + it 'returns matching legacy env label' do + matching_pod['metadata']['annotations'].delete('app.gitlab.com/app') + matching_pod['metadata']['annotations'].delete('app.gitlab.com/env') + matching_pod['metadata']['labels']['app'] = 'production' + matching_items = [matching_pod] + items = matching_items + [kube_pod] + + expect(filter_by_project_environment(items, 'my-cool-app', 'production')).to eq(matching_items) + end + + it 'returns matching env label' do + matching_items = [matching_pod] items = matching_items + [kube_pod] - expect(filter_by_label(items, app: 'foo')).to eq(matching_items) + expect(filter_by_project_environment(items, 'my-cool-app', 'production')).to eq(matching_items) end end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 89839709131..30ca07d5d2c 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -95,6 +95,12 @@ describe BroadcastMessage do end end + describe '#attributes' do + it 'includes message_html field' do + expect(subject.attributes.keys).to include("cached_markdown_version", "message_html") + end + end + describe '#active?' do it 'is truthy when started and not ended' do message = build(:broadcast_message) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 9ca4241d7d8..7500e6ae5b1 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -186,6 +186,37 @@ describe Ci::Build do end end + describe '#enqueue' do + let(:build) { create(:ci_build, :created) } + + subject { build.enqueue } + + before do + allow(build).to receive(:any_unmet_prerequisites?).and_return(has_prerequisites) + allow(Ci::PrepareBuildService).to receive(:perform_async) + end + + context 'build has unmet prerequisites' do + let(:has_prerequisites) { true } + + it 'transitions to preparing' do + subject + + expect(build).to be_preparing + end + end + + context 'build has no prerequisites' do + let(:has_prerequisites) { false } + + it 'transitions to pending' do + subject + + expect(build).to be_pending + end + end + end + describe '#actionize' do context 'when build is a created' do before do @@ -344,6 +375,18 @@ describe Ci::Build do expect(build).to be_pending end + + context 'build has unmet prerequisites' do + before do + allow(build).to receive(:prerequisites).and_return([double]) + end + + it 'transits to preparing' do + subject + + expect(build).to be_preparing + end + end end end @@ -2876,6 +2919,36 @@ describe Ci::Build do end end + describe '#any_unmet_prerequisites?' do + let(:build) { create(:ci_build, :created) } + + subject { build.any_unmet_prerequisites? } + + context 'build has prerequisites' do + before do + allow(build).to receive(:prerequisites).and_return([double]) + end + + it { is_expected.to be_truthy } + + context 'and the ci_preparing_state feature is disabled' do + before do + stub_feature_flags(ci_preparing_state: false) + end + + it { is_expected.to be_falsey } + end + end + + context 'build does not have prerequisites' do + before do + allow(build).to receive(:prerequisites).and_return([]) + end + + it { is_expected.to be_falsey } + end + end + describe '#yaml_variables' do let(:build) { create(:ci_build, pipeline: pipeline, yaml_variables: variables) } @@ -2928,6 +3001,20 @@ describe Ci::Build do end end + describe 'state transition: any => [:preparing]' do + let(:build) { create(:ci_build, :created) } + + before do + allow(build).to receive(:prerequisites).and_return([double]) + end + + it 'queues BuildPrepareWorker' do + expect(Ci::BuildPrepareWorker).to receive(:perform_async).with(build.id) + + build.enqueue + end + end + describe 'state transition: any => [:pending]' do let(:build) { create(:ci_build, :created) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 7eeaa7a18ef..2ac056f63b2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1201,16 +1201,28 @@ describe Ci::Pipeline, :mailer do end describe '#started_at' do - it 'updates on transitioning to running' do - build.run + let(:pipeline) { create(:ci_empty_pipeline, status: from_status) } + + %i[created preparing pending].each do |status| + context "from #{status}" do + let(:from_status) { status } - expect(pipeline.reload.started_at).not_to be_nil + it 'updates on transitioning to running' do + pipeline.run + + expect(pipeline.started_at).not_to be_nil + end + end end - it 'does not update on transitioning to success' do - build.success + context 'from created' do + let(:from_status) { :created } + + it 'does not update on transitioning to success' do + pipeline.succeed - expect(pipeline.reload.started_at).to be_nil + expect(pipeline.started_at).to be_nil + end end end @@ -1229,27 +1241,49 @@ describe Ci::Pipeline, :mailer do end describe 'merge request metrics' do - let(:project) { create(:project, :repository) } - let(:pipeline) { FactoryBot.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } + let(:pipeline) { create(:ci_empty_pipeline, status: from_status) } before do expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id) end context 'when transitioning to running' do - it 'schedules metrics workers' do - pipeline.run + %i[created preparing pending].each do |status| + context "from #{status}" do + let(:from_status) { status } + + it 'schedules metrics workers' do + pipeline.run + end + end end end context 'when transitioning to success' do + let(:from_status) { 'created' } + it 'schedules metrics workers' do pipeline.succeed end end end + describe 'merge on success' do + let(:pipeline) { create(:ci_empty_pipeline, status: from_status) } + + %i[created preparing pending running].each do |status| + context "from #{status}" do + let(:from_status) { status } + + it 'schedules pipeline success worker' do + expect(PipelineSuccessWorker).to receive(:perform_async).with(pipeline.id) + + pipeline.succeed + end + end + end + end + describe 'pipeline caching' do it 'performs ExpirePipelinesCacheWorker' do expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) @@ -1768,6 +1802,18 @@ describe Ci::Pipeline, :mailer do subject { pipeline.reload.status } + context 'on prepare' do + before do + # Prevent skipping directly to 'pending' + allow(build).to receive(:prerequisites).and_return([double]) + allow(Ci::BuildPrepareWorker).to receive(:perform_async) + + build.enqueue + end + + it { is_expected.to eq('preparing') } + end + context 'on queuing' do before do build.enqueue diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index cc93a1b4965..af65530e663 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -375,14 +375,14 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end context 'with valid pods' do - let(:pod) { kube_pod(app: environment.slug) } - let(:pod_with_no_terminal) { kube_pod(app: environment.slug, status: "Pending") } + let(:pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug) } + let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") } let(:terminals) { kube_terminals(service, pod) } before do stub_reactive_cache( service, - pods: [pod, pod, pod_with_no_terminal, kube_pod(app: "should-be-filtered-out")] + pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] ) end diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb index 0f5d03ff458..30c504ebea8 100644 --- a/spec/models/commit_collection_spec.rb +++ b/spec/models/commit_collection_spec.rb @@ -37,12 +37,92 @@ describe CommitCollection do describe '#without_merge_commits' do it 'returns all commits except merge commits' do + merge_commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98") + expect(merge_commit).to receive(:merge_commit?).and_return(true) + collection = described_class.new(project, [ - build(:commit), - build(:commit, :merge_commit) + commit, + merge_commit ]) - expect(collection.without_merge_commits.size).to eq(1) + expect(collection.without_merge_commits).to contain_exactly(commit) + end + end + + describe 'enrichment methods' do + let(:gitaly_commit) { commit } + let(:hash_commit) { Commit.from_hash(gitaly_commit.to_hash, project) } + + describe '#unenriched' do + it 'returns all commits that are not backed by gitaly data' do + collection = described_class.new(project, [gitaly_commit, hash_commit]) + + expect(collection.unenriched).to contain_exactly(hash_commit) + end + end + + describe '#fully_enriched?' do + it 'returns true when all commits are backed by gitaly data' do + collection = described_class.new(project, [gitaly_commit, gitaly_commit]) + + expect(collection.fully_enriched?).to eq(true) + end + + it 'returns false when any commits are not backed by gitaly data' do + collection = described_class.new(project, [gitaly_commit, hash_commit]) + + expect(collection.fully_enriched?).to eq(false) + end + + it 'returns true when the collection is empty' do + collection = described_class.new(project, []) + + expect(collection.fully_enriched?).to eq(true) + end + end + + describe '#enrich!' do + it 'replaces commits in the collection with those backed by gitaly data' do + collection = described_class.new(project, [hash_commit]) + + collection.enrich! + + new_commit = collection.commits.first + expect(new_commit.id).to eq(hash_commit.id) + expect(hash_commit.gitaly_commit?).to eq(false) + expect(new_commit.gitaly_commit?).to eq(true) + end + + it 'maintains the original order of the commits' do + gitaly_commits = [gitaly_commit] * 3 + hash_commits = [hash_commit] * 3 + # Interleave the gitaly and hash commits together + original_commits = gitaly_commits.zip(hash_commits).flatten + collection = described_class.new(project, original_commits) + + collection.enrich! + + original_commits.each_with_index do |original_commit, i| + new_commit = collection.commits[i] + expect(original_commit.id).to eq(new_commit.id) + end + end + + it 'fetches data if there are unenriched commits' do + collection = described_class.new(project, [hash_commit]) + + expect(Commit).to receive(:lazy).exactly(:once) + + collection.enrich! + end + + it 'does not fetch data if all commits are enriched' do + collection = described_class.new(project, [gitaly_commit]) + + expect(Commit).not_to receive(:lazy) + + collection.enrich! + end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 8b7c88805c1..e2b7f5c6ee2 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -49,6 +49,16 @@ describe CommitStatus do commit_status.success! end + + describe 'transitioning to running' do + let(:commit_status) { create(:commit_status, :pending, started_at: nil) } + + it 'records the started at time' do + commit_status.run! + + expect(commit_status.started_at).to be_present + end + end end describe '#started?' do @@ -479,6 +489,12 @@ describe CommitStatus do it { is_expected.to be_script_failure } end + + context 'when failure_reason is unmet_prerequisites' do + let(:reason) { :unmet_prerequisites } + + it { is_expected.to be_unmet_prerequisites } + end end describe 'ensure stage assignment' do @@ -555,6 +571,7 @@ describe CommitStatus do before do allow(Time).to receive(:now).and_return(current_time) + expect(commit_status.any_unmet_prerequisites?).to eq false end shared_examples 'commit status enqueued' do @@ -569,6 +586,12 @@ describe CommitStatus do it_behaves_like 'commit status enqueued' end + context 'when initial state is :preparing' do + let(:commit_status) { create(:commit_status, :preparing) } + + it_behaves_like 'commit status enqueued' + end + context 'when initial state is :skipped' do let(:commit_status) { create(:commit_status, :skipped) } diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 447279f19a8..7d555f15e39 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -23,6 +23,7 @@ describe CacheMarkdownField do include CacheMarkdownField cache_markdown_field :foo cache_markdown_field :baz, pipeline: :single_line + cache_markdown_field :zoo, whitelisted: true def self.add_attr(name) self.attribute_names += [name] @@ -35,7 +36,7 @@ describe CacheMarkdownField do add_attr :cached_markdown_version - [:foo, :foo_html, :bar, :baz, :baz_html].each do |name| + [:foo, :foo_html, :bar, :baz, :baz_html, :zoo, :zoo_html].each do |name| add_attr(name) end @@ -84,8 +85,8 @@ describe CacheMarkdownField do end describe '.attributes' do - it 'excludes cache attributes' do - expect(thing.attributes.keys.sort).to eq(%w[bar baz foo]) + it 'excludes cache attributes that is blacklisted by default' do + expect(thing.attributes.keys.sort).to eq(%w[bar baz cached_markdown_version foo zoo zoo_html]) end end @@ -297,7 +298,12 @@ describe CacheMarkdownField do it 'saves the changes using #update_columns' do expect(thing).to receive(:persisted?).and_return(true) expect(thing).to receive(:update_columns) - .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version) + .with( + "foo_html" => updated_html, + "baz_html" => "", + "zoo_html" => "", + "cached_markdown_version" => cache_version + ) thing.refresh_markdown_cache! end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 6b1038cb8fd..e8b1eba67cc 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -34,6 +34,22 @@ describe HasStatus do it { is_expected.to eq 'running' } end + context 'all preparing' do + let!(:statuses) do + [create(type, status: :preparing), create(type, status: :preparing)] + end + + it { is_expected.to eq 'preparing' } + end + + context 'at least one preparing' do + let!(:statuses) do + [create(type, status: :success), create(type, status: :preparing)] + end + + it { is_expected.to eq 'preparing' } + end + context 'success and failed but allowed to fail' do let!(:statuses) do [create(type, status: :success), @@ -188,7 +204,7 @@ describe HasStatus do end end - %i[created running pending success + %i[created preparing running pending success failed canceled skipped].each do |status| it_behaves_like 'having a job', status end @@ -234,7 +250,7 @@ describe HasStatus do describe '.alive' do subject { CommitStatus.alive } - %i[running pending created].each do |status| + %i[running pending preparing created].each do |status| it_behaves_like 'containing the job', status end @@ -270,7 +286,7 @@ describe HasStatus do describe '.cancelable' do subject { CommitStatus.cancelable } - %i[running pending created scheduled].each do |status| + %i[running pending preparing created scheduled].each do |status| it_behaves_like 'containing the job', status end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index a8d53cfcd7d..5fce9504334 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -356,4 +356,32 @@ describe Deployment do end end end + + describe '#cluster' do + let(:deployment) { create(:deployment) } + let(:project) { deployment.project } + let(:environment) { deployment.environment } + + subject { deployment.cluster } + + before do + expect(project).to receive(:deployment_platform) + .with(environment: environment.name).and_call_original + end + + context 'project has no deployment platform' do + before do + expect(project.clusters).to be_empty + end + + it { is_expected.to be_nil } + end + + context 'project has a deployment platform' do + let!(:cluster) { create(:cluster, projects: [project]) } + let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) } + + it { is_expected.to eq cluster } + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 42c49e330cc..fad73613989 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -84,32 +84,27 @@ describe MergeRequest do describe '#default_squash_commit_message' do let(:project) { subject.project } - - def commit_collection(commit_hashes) - raw_commits = commit_hashes.map { |raw| Commit.from_hash(raw, project) } - - CommitCollection.new(project, raw_commits) - end + let(:is_multiline) { -> (c) { c.description.present? } } + let(:multiline_commits) { subject.commits.select(&is_multiline) } + let(:singleline_commits) { subject.commits.reject(&is_multiline) } it 'returns the oldest multiline commit message' do - commits = commit_collection([ - { message: 'Singleline', parent_ids: [] }, - { message: "Second multiline\nCommit message", parent_ids: [] }, - { message: "First multiline\nCommit message", parent_ids: [] } - ]) - - expect(subject).to receive(:commits).and_return(commits) - - expect(subject.default_squash_commit_message).to eq("First multiline\nCommit message") + expect(subject.default_squash_commit_message).to eq(multiline_commits.last.message) end it 'returns the merge request title if there are no multiline commits' do - commits = commit_collection([ - { message: 'Singleline', parent_ids: [] } - ]) + expect(subject).to receive(:commits).and_return( + CommitCollection.new(project, singleline_commits) + ) + + expect(subject.default_squash_commit_message).to eq(subject.title) + end - expect(subject).to receive(:commits).and_return(commits) + it 'does not return commit messages from multiline merge commits' do + collection = CommitCollection.new(project, multiline_commits).enrich! + expect(collection.commits).to all( receive(:merge_commit?).and_return(true) ) + expect(subject).to receive(:commits).and_return(collection) expect(subject.default_squash_commit_message).to eq(subject.title) end end @@ -1045,7 +1040,7 @@ describe MergeRequest do describe '#commit_authors' do it 'returns all the authors of every commit in the merge request' do - users = subject.commits.map(&:author_email).uniq.map do |email| + users = subject.commits.without_merge_commits.map(&:author_email).uniq.map do |email| create(:user, email: email) end @@ -1059,7 +1054,7 @@ describe MergeRequest do describe '#authors' do it 'returns a list with all the commit authors in the merge request and author' do - users = subject.commits.map(&:author_email).uniq.map do |email| + users = subject.commits.without_merge_commits.map(&:author_email).uniq.map do |email| create(:user, email: email) end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 47f70e6648a..56e587262ef 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -323,13 +323,14 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end context 'with valid pods' do - let(:pod) { kube_pod(app: environment.slug) } + let(:pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug) } + let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") } let(:terminals) { kube_terminals(service, pod) } before do stub_reactive_cache( service, - pods: [pod, pod, kube_pod(app: "should-be-filtered-out")] + pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] ) end @@ -360,14 +361,16 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when kubernetes responds with valid pods' do before do stub_kubeclient_pods + stub_kubeclient_deployments # Used by EE end - it { is_expected.to eq(pods: [kube_pod]) } + it { is_expected.to include(pods: [kube_pod]) } end context 'when kubernetes responds with 500s' do before do stub_kubeclient_pods(status: 500) + stub_kubeclient_deployments(status: 500) # Used by EE end it { expect { subject }.to raise_error(Kubeclient::HttpError) } @@ -376,9 +379,10 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when kubernetes responds with 404s' do before do stub_kubeclient_pods(status: 404) + stub_kubeclient_deployments(status: 404) # Used by EE end - it { is_expected.to eq(pods: []) } + it { is_expected.to include(pods: []) } end end diff --git a/spec/policies/identity_provider_policy_spec.rb b/spec/policies/identity_provider_policy_spec.rb new file mode 100644 index 00000000000..2520469d4e7 --- /dev/null +++ b/spec/policies/identity_provider_policy_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe IdentityProviderPolicy do + subject(:policy) { described_class.new(user, provider) } + let(:user) { User.new } + let(:provider) { :a_provider } + + describe '#rules' do + it { is_expected.to be_allowed(:link) } + it { is_expected.to be_allowed(:unlink) } + + context 'when user is anonymous' do + let(:user) { nil } + + it { is_expected.not_to be_allowed(:link) } + it { is_expected.not_to be_allowed(:unlink) } + end + + %w[saml cas3].each do |provider_name| + context "when provider is #{provider_name}" do + let(:provider) { provider_name } + + it { is_expected.to be_allowed(:link) } + it { is_expected.not_to be_allowed(:unlink) } + end + end + end +end diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index 9bab1f95150..4e42e233b4c 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -331,7 +331,6 @@ describe API::ProjectClusters do it 'should update cluster attributes' do expect(cluster.platform_kubernetes.namespace).to eq('new-namespace') - expect(cluster.kubernetes_namespace.namespace).to eq('new-namespace') end end diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 9087cccb759..3ccedd8dd06 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -918,6 +918,15 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it { expect(job).to be_job_execution_timeout } end + + context 'when failure_reason is unmet_prerequisites' do + before do + update_job(state: 'failed', failure_reason: 'unmet_prerequisites') + job.reload + end + + it { expect(job).to be_unmet_prerequisites } + end end context 'when trace is given' do diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 4dbd79f2fc0..727fd8951f2 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -279,13 +279,18 @@ describe MergeRequestWidgetEntity do end describe 'commits_without_merge_commits' do + def find_matching_commit(short_id) + resource.commits.find { |c| c.short_id == short_id } + end + it 'should not include merge commits' do - # Mock all but the first 5 commits to be merge commits - resource.commits.each_with_index do |commit, i| - expect(commit).to receive(:merge_commit?).at_least(:once).and_return(i > 4) - end + commits_in_widget = subject[:commits_without_merge_commits] - expect(subject[:commits_without_merge_commits].size).to eq(5) + expect(commits_in_widget.length).to be < resource.commits.length + expect(commits_in_widget.length).to eq(resource.commits.without_merge_commits.length) + commits_in_widget.each do |c| + expect(find_matching_commit(c[:short_id]).merge_commit?).to eq(false) + end end end end diff --git a/spec/services/ci/prepare_build_service_spec.rb b/spec/services/ci/prepare_build_service_spec.rb new file mode 100644 index 00000000000..1797f8f964f --- /dev/null +++ b/spec/services/ci/prepare_build_service_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::PrepareBuildService do + describe '#execute' do + let(:build) { create(:ci_build, :preparing) } + + subject { described_class.new(build).execute } + + before do + allow(build).to receive(:prerequisites).and_return(prerequisites) + end + + shared_examples 'build enqueueing' do + it 'enqueues the build' do + expect(build).to receive(:enqueue).once + + subject + end + end + + context 'build has unmet prerequisites' do + let(:prerequisite) { double(complete!: true) } + let(:prerequisites) { [prerequisite] } + + it 'completes each prerequisite' do + expect(prerequisites).to all(receive(:complete!)) + + subject + end + + include_examples 'build enqueueing' + + context 'prerequisites fail to complete' do + before do + allow(build).to receive(:enqueue).and_return(false) + end + + it 'drops the build' do + expect(build).to receive(:drop!).with(:unmet_prerequisites).once + + subject + end + end + end + + context 'build has no prerequisites' do + let(:prerequisites) { [] } + + include_examples 'build enqueueing' + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index d1b110b9806..e8418b09dc2 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -276,6 +276,7 @@ describe Projects::CreateService, '#execute' do before do group.add_owner(user) + stub_feature_flags(ci_preparing_state: false) expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index aae50d5307f..4efd360cb30 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -83,6 +83,7 @@ describe Projects::TransferService do subject { transfer_project(project, user, group) } before do + stub_feature_flags(ci_preparing_state: false) expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index cca11e112c9..ac52acb6570 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -250,16 +250,19 @@ module KubernetesHelpers # This is a partial response, it will have many more elements in reality but # these are the ones we care about at the moment - def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil) + def kube_pod(name: "kube-pod", environment_slug: "production", project_slug: "project-path-slug", status: "Running", track: nil) { "metadata" => { "name" => name, "generate_name" => "generated-name-with-suffix", "creationTimestamp" => "2016-11-25T19:55:19Z", + "annotations" => { + "app.gitlab.com/env" => environment_slug, + "app.gitlab.com/app" => project_slug + }, "labels" => { - "app" => app, "track" => track - } + }.compact }, "spec" => { "containers" => [ @@ -293,13 +296,16 @@ module KubernetesHelpers } end - def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil) + def kube_deployment(name: "kube-deployment", environment_slug: "production", project_slug: "project-path-slug", track: nil) { "metadata" => { "name" => name, "generation" => 4, + "annotations" => { + "app.gitlab.com/env" => environment_slug, + "app.gitlab.com/app" => project_slug + }, "labels" => { - "app" => app, "track" => track }.compact }, diff --git a/spec/workers/ci/build_prepare_worker_spec.rb b/spec/workers/ci/build_prepare_worker_spec.rb new file mode 100644 index 00000000000..9f76696ee66 --- /dev/null +++ b/spec/workers/ci/build_prepare_worker_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::BuildPrepareWorker do + subject { described_class.new.perform(build_id) } + + context 'build exists' do + let(:build) { create(:ci_build) } + let(:build_id) { build.id } + let(:service) { double(execute: true) } + + it 'calls the prepare build service' do + expect(Ci::PrepareBuildService).to receive(:new).with(build).and_return(service) + expect(service).to receive(:execute).once + + subject + end + end + + context 'build does not exist' do + let(:build_id) { -1 } + + it 'does not attempt to prepare the build' do + expect(Ci::PrepareBuildService).not_to receive(:new) + + subject + end + end +end diff --git a/spec/workers/cluster_configure_worker_spec.rb b/spec/workers/cluster_configure_worker_spec.rb index 6918ee3d7d8..83f76809435 100644 --- a/spec/workers/cluster_configure_worker_spec.rb +++ b/spec/workers/cluster_configure_worker_spec.rb @@ -4,6 +4,11 @@ require 'spec_helper' describe ClusterConfigureWorker, '#perform' do let(:worker) { described_class.new } + let(:ci_preparing_state_enabled) { false } + + before do + stub_feature_flags(ci_preparing_state: ci_preparing_state_enabled) + end context 'when group cluster' do let(:cluster) { create(:cluster, :group, :provided_by_gcp) } @@ -66,4 +71,15 @@ describe ClusterConfigureWorker, '#perform' do described_class.new.perform(123) end end + + context 'ci_preparing_state feature is enabled' do + let(:cluster) { create(:cluster) } + let(:ci_preparing_state_enabled) { true } + + it 'does not configure the cluster' do + expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster) + + described_class.new.perform(cluster.id) + end + end end diff --git a/spec/workers/cluster_project_configure_worker_spec.rb b/spec/workers/cluster_project_configure_worker_spec.rb new file mode 100644 index 00000000000..afdea55adf4 --- /dev/null +++ b/spec/workers/cluster_project_configure_worker_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ClusterProjectConfigureWorker, '#perform' do + let(:worker) { described_class.new } + + context 'ci_preparing_state feature is enabled' do + let(:cluster) { create(:cluster) } + + before do + stub_feature_flags(ci_preparing_state: true) + end + + it 'does not configure the cluster' do + expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_project) + + described_class.new.perform(cluster.id) + end + end +end |