diff options
author | Shinya Maeda <shinya@gitlab.com> | 2018-05-14 17:20:47 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2018-05-14 17:20:47 +0900 |
commit | 1e76f289fb3f6e125594d2ec0169ecde25e4e335 (patch) | |
tree | 3087903b6f5aab8c74f3f0d76b2908cd5dc169e3 | |
parent | a74184eb5e692ef77fe3be28b1f4a40549c8fcff (diff) | |
parent | 40683268b2b5ad807194387d8345a30195e178c4 (diff) | |
download | gitlab-ce-1e76f289fb3f6e125594d2ec0169ecde25e4e335.tar.gz |
Merge branch 'master' into per-project-pipeline-iid
78 files changed, 1263 insertions, 638 deletions
@@ -6,7 +6,7 @@ end gem_versions = {} gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2' gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0' -gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10' +gem_versions['rails'] = rails5? ? '5.0.7' : '4.2.10' gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9' # --- The end of special code for migrating to Rails 5.0 --- diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 3056b97ccd5..fc6dfd040c2 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -4,43 +4,43 @@ GEM RedCloth (4.3.2) abstract_type (0.0.7) ace-rails-ap (4.1.4) - actioncable (5.0.6) - actionpack (= 5.0.6) + actioncable (5.0.7) + actionpack (= 5.0.7) nio4r (>= 1.2, < 3.0) websocket-driver (~> 0.6.1) - actionmailer (5.0.6) - actionpack (= 5.0.6) - actionview (= 5.0.6) - activejob (= 5.0.6) + actionmailer (5.0.7) + actionpack (= 5.0.7) + actionview (= 5.0.7) + activejob (= 5.0.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.0.6) - actionview (= 5.0.6) - activesupport (= 5.0.6) + actionpack (5.0.7) + actionview (= 5.0.7) + activesupport (= 5.0.7) rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.6) - activesupport (= 5.0.6) + actionview (5.0.7) + activesupport (= 5.0.7) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.0.6) - activesupport (= 5.0.6) + activejob (5.0.7) + activesupport (= 5.0.7) globalid (>= 0.3.6) - activemodel (5.0.6) - activesupport (= 5.0.6) - activerecord (5.0.6) - activemodel (= 5.0.6) - activesupport (= 5.0.6) + activemodel (5.0.7) + activesupport (= 5.0.7) + activerecord (5.0.7) + activemodel (= 5.0.7) + activesupport (= 5.0.7) arel (~> 7.0) activerecord_sane_schema_dumper (1.0) rails (>= 5, < 6) - activesupport (5.0.6) + activesupport (5.0.7) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (~> 0.7) + i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) acts-as-taggable-on (5.0.0) @@ -62,13 +62,13 @@ GEM asciidoctor (1.5.6.1) asciidoctor-plantuml (0.0.8) asciidoctor (~> 1.5) - asset_sync (2.2.0) + asset_sync (2.4.0) activemodel (>= 4.1.0) fog-core mime-types (>= 2.99) unf ast (2.4.0) - atomic (1.1.100) + atomic (1.1.99) attr_encrypted (3.1.0) encryptor (~> 3.0.0) attr_required (1.0.1) @@ -144,12 +144,10 @@ GEM connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.3) + crass (1.0.4) creole (0.5.0) css_parser (1.6.0) addressable - d3_rails (3.5.17) - railties (>= 3.1.0) daemons (1.2.6) database_cleaner (1.5.3) debug_inspector (0.0.3) @@ -292,7 +290,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.97.0) + gitaly-proto (0.99.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -335,9 +333,8 @@ GEM activesupport (>= 4.2.0) gollum-grit_adapter (1.0.1) gitlab-grit (~> 2.7, >= 2.7.1) - gon (6.1.0) + gon (6.2.0) actionpack (>= 3.0) - json multi_json request_store (>= 1.0) google-api-client (0.19.8) @@ -367,8 +364,8 @@ GEM rack (>= 1.3.0) rack-accept virtus (>= 1.0.0) - grape-entity (0.6.1) - activesupport (>= 5.0.0) + grape-entity (0.7.1) + activesupport (>= 4.0) multi_json (>= 1.3.2) grape-route-helpers (2.1.0) activesupport @@ -420,7 +417,7 @@ GEM json (~> 1.8) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (0.9.5) + i18n (1.0.1) concurrent-ruby (~> 1.0) ice_nine (0.11.2) influxdb (0.5.3) @@ -515,7 +512,7 @@ GEM net-ldap (0.16.1) net-ssh (4.2.0) netrc (0.11.0) - nio4r (2.2.0) + nio4r (2.3.1) nokogiri (1.8.2) mini_portile2 (~> 2.3.0) numerizer (0.1.1) @@ -545,9 +542,9 @@ GEM omniauth (~> 1.2) omniauth-facebook (4.0.0) omniauth-oauth2 (~> 1.2) - omniauth-github (1.1.2) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) + omniauth-github (1.3.0) + omniauth (~> 1.5) + omniauth-oauth2 (>= 1.4.0, < 2.0) omniauth-gitlab (1.0.3) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) @@ -633,7 +630,7 @@ GEM parser unparser procto (0.0.3) - prometheus-client-mmap (0.9.1) + prometheus-client-mmap (0.9.2) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -644,7 +641,7 @@ GEM pry (>= 0.10.4) public_suffix (3.0.2) pyu-ruby-sasl (0.0.3.3) - rack (2.0.4) + rack (2.0.5) rack-accept (0.4.5) rack (>= 0.4) rack-attack (4.4.1) @@ -662,17 +659,17 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (5.0.6) - actioncable (= 5.0.6) - actionmailer (= 5.0.6) - actionpack (= 5.0.6) - actionview (= 5.0.6) - activejob (= 5.0.6) - activemodel (= 5.0.6) - activerecord (= 5.0.6) - activesupport (= 5.0.6) + rails (5.0.7) + actioncable (= 5.0.7) + actionmailer (= 5.0.7) + actionpack (= 5.0.7) + actionview (= 5.0.7) + activejob (= 5.0.7) + activemodel (= 5.0.7) + activerecord (= 5.0.7) + activesupport (= 5.0.7) bundler (>= 1.3.0) - railties (= 5.0.6) + railties (= 5.0.7) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.2) actionpack (~> 5.x, >= 5.0.1) @@ -683,21 +680,21 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) rails-i18n (5.1.1) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) - railties (5.0.6) - actionpack (= 5.0.6) - activesupport (= 5.0.6) + railties (5.0.7) + actionpack (= 5.0.7) + activesupport (= 5.0.7) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake raindrops (0.19.0) - rake (12.3.0) + rake (12.3.1) rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) @@ -1001,7 +998,7 @@ DEPENDENCIES asana (~> 0.6.0) asciidoctor (~> 1.5.6) asciidoctor-plantuml (= 0.0.8) - asset_sync (~> 2.2.0) + asset_sync (~> 2.4) attr_encrypted (~> 3.1.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) @@ -1027,12 +1024,11 @@ DEPENDENCIES concurrent-ruby (~> 1.0.5) connection_pool (~> 2.0) creole (~> 0.5.0) - d3_rails (~> 3.5.0) database_cleaner (~> 1.5.0) deckar01-task_list (= 2.0.0) default_value_for (~> 3.0.5) device_detector - devise (~> 4.2) + devise (~> 4.4) devise-two-factor (~> 3.0.0) diffy (~> 3.1.0) doorkeeper (~> 4.3) @@ -1063,7 +1059,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.97.0) + gitaly-proto (~> 0.99.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) @@ -1071,12 +1067,12 @@ DEPENDENCIES gitlab-markup (~> 1.6.2) gitlab-styles (~> 2.3) gitlab_omniauth-ldap (~> 2.0.4) - gon (~> 6.1.0) + gon (~> 6.2) google-api-client (~> 0.19.8) google-protobuf (= 3.5.1) gpgme grape (~> 1.0) - grape-entity (~> 0.6.0) + grape-entity (~> 0.7.1) grape-route-helpers (~> 2.1.0) grape_logging (~> 1.7) grpc (~> 1.11.0) @@ -1117,7 +1113,7 @@ DEPENDENCIES omniauth-azure-oauth2 (~> 0.0.9) omniauth-cas3 (~> 1.1.4) omniauth-facebook (~> 4.0.0) - omniauth-github (~> 1.1.1) + omniauth-github (~> 1.3) omniauth-gitlab (~> 1.0.2) omniauth-google-oauth2 (~> 0.5.3) omniauth-kerberos (~> 0.3.0) @@ -1136,14 +1132,14 @@ DEPENDENCIES peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) premailer-rails (~> 1.9.7) - prometheus-client-mmap (~> 0.9.1) + prometheus-client-mmap (~> 0.9.2) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) rack-cors (~> 1.0.0) rack-oauth2 (~> 1.2.1) rack-proxy (~> 0.6.0) - rails (= 5.0.6) + rails (= 5.0.7) rails-controller-testing rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 5.1) diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index ff7e546fb9c..f8678b602ac 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -140,7 +140,7 @@ export default { this.file.staged && this.file.key.indexOf('unstaged-') === 0 ? head : null, ); - if (this.viewer === viewerTypes.mr) { + if (this.viewer === viewerTypes.mr && this.file.mrChange) { this.editor.attachMergeRequestModel(this.model); } else { this.editor.attachModel(this.model); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index bc79ff4a542..e0b9766fbee 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -44,6 +44,7 @@ export const dataStructure = () => ({ size: 0, parentPath: null, lastOpenedAt: 0, + mrChange: null, }); export const decorateData = entity => { diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue deleted file mode 100644 index 0cdffbde05b..00000000000 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ /dev/null @@ -1,95 +0,0 @@ -<script> - /* eslint-disable no-alert */ - - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import icon from '../../vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - - export default { - directives: { - tooltip, - }, - components: { - loadingIcon, - icon, - }, - props: { - endpoint: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - icon: { - type: String, - required: true, - }, - cssClass: { - type: String, - required: true, - }, - pipelineId: { - type: Number, - required: true, - }, - type: { - type: String, - required: true, - }, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - buttonClass() { - return `btn ${this.cssClass}`; - }, - }, - created() { - // We're using eventHub to listen to the modal here instead of - // using props because it would would make the parent components - // much more complex to keep track of the loading state of each button - eventHub.$on('postAction', this.setLoading); - }, - beforeDestroy() { - eventHub.$off('postAction', this.setLoading); - }, - methods: { - onClick() { - eventHub.$emit('openConfirmationModal', { - pipelineId: this.pipelineId, - endpoint: this.endpoint, - type: this.type, - }); - }, - setLoading(endpoint) { - if (endpoint === this.endpoint) { - this.isLoading = true; - } - }, - }, - }; -</script> - -<template> - <button - v-tooltip - type="button" - @click="onClick" - :class="buttonClass" - :title="title" - :aria-label="title" - data-container="body" - data-placement="top" - :disabled="isLoading"> - <icon - :name="icon" - /> - <loading-icon v-if="isLoading" /> - </button> -</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 714aed1333e..41986b827cd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -1,7 +1,7 @@ <script> - import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; + import Modal from '~/vue_shared/components/gl_modal.vue'; import { s__, sprintf } from '~/locale'; - import pipelinesTableRowComponent from './pipelines_table_row.vue'; + import PipelinesTableRowComponent from './pipelines_table_row.vue'; import eventHub from '../event_hub'; /** @@ -11,8 +11,8 @@ */ export default { components: { - pipelinesTableRowComponent, - DeprecatedModal, + PipelinesTableRowComponent, + Modal, }, props: { pipelines: { @@ -37,30 +37,18 @@ return { pipelineId: '', endpoint: '', - type: '', }; }, computed: { modalTitle() { - return this.type === 'stop' ? - sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { - pipelineId: `'${this.pipelineId}'`, - }, false) : - sprintf(s__('Pipeline|Retry pipeline #%{pipelineId}?'), { - pipelineId: `'${this.pipelineId}'`, - }, false); + return sprintf(s__('Pipeline|Stop pipeline #%{pipelineId}?'), { + pipelineId: `${this.pipelineId}`, + }, false); }, modalText() { - return this.type === 'stop' ? - sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false) : - sprintf(s__('Pipeline|You’re about to retry pipeline %{pipelineId}.'), { - pipelineId: `<strong>#${this.pipelineId}</strong>`, - }, false); - }, - primaryButtonLabel() { - return this.type === 'stop' ? s__('Pipeline|Stop pipeline') : s__('Pipeline|Retry pipeline'); + return sprintf(s__('Pipeline|You’re about to stop pipeline %{pipelineId}.'), { + pipelineId: `<strong>#${this.pipelineId}</strong>`, + }, false); }, }, created() { @@ -73,7 +61,6 @@ setModalData(data) { this.pipelineId = data.pipelineId; this.endpoint = data.endpoint; - this.type = data.type; }, onSubmit() { eventHub.$emit('postAction', this.endpoint); @@ -120,20 +107,16 @@ :auto-devops-help-path="autoDevopsHelpPath" :view-type="viewType" /> - <deprecated-modal + + <modal id="confirmation-modal" - :title="modalTitle" - :text="modalText" - kind="danger" - :primary-button-label="primaryButtonLabel" + :header-title-text="modalTitle" + footer-primary-button-variant="danger" + :footer-primary-button-text="s__('Pipeline|Stop pipeline')" @submit="onSubmit" > - <template - slot="body" - slot-scope="props" - > - <p v-html="props.text"></p> - </template> - </deprecated-modal> + <span v-html="modalText"></span> + </modal> + </div> </template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 4cbd67e0372..498a97851fa 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -1,13 +1,14 @@ <script> - /* eslint-disable no-param-reassign */ - import asyncButtonComponent from './async_button.vue'; - import pipelinesActionsComponent from './pipelines_actions.vue'; - import pipelinesArtifactsComponent from './pipelines_artifacts.vue'; - import ciBadge from '../../vue_shared/components/ci_badge_link.vue'; - import pipelineStage from './stage.vue'; - import pipelineUrl from './pipeline_url.vue'; - import pipelinesTimeago from './time_ago.vue'; - import commitComponent from '../../vue_shared/components/commit.vue'; + import eventHub from '../event_hub'; + import PipelinesActionsComponent from './pipelines_actions.vue'; + import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; + import CiBadge from '../../vue_shared/components/ci_badge_link.vue'; + import PipelineStage from './stage.vue'; + import PipelineUrl from './pipeline_url.vue'; + import PipelinesTimeago from './time_ago.vue'; + import CommitComponent from '../../vue_shared/components/commit.vue'; + import LoadingButton from '../../vue_shared/components/loading_button.vue'; + import Icon from '../../vue_shared/components/icon.vue'; /** * Pipeline table row. @@ -16,14 +17,15 @@ */ export default { components: { - asyncButtonComponent, - pipelinesActionsComponent, - pipelinesArtifactsComponent, - commitComponent, - pipelineStage, - pipelineUrl, - ciBadge, - pipelinesTimeago, + PipelinesActionsComponent, + PipelinesArtifactsComponent, + CommitComponent, + PipelineStage, + PipelineUrl, + CiBadge, + PipelinesTimeago, + LoadingButton, + Icon, }, props: { pipeline: { @@ -44,6 +46,12 @@ required: true, }, }, + data() { + return { + isRetrying: false, + isCancelling: false, + }; + }, computed: { /** * If provided, returns the commit tag. @@ -119,8 +127,10 @@ if (this.pipeline.ref) { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { if (prop === 'path') { + // eslint-disable-next-line no-param-reassign accumulator.ref_url = this.pipeline.ref[prop]; } else { + // eslint-disable-next-line no-param-reassign accumulator[prop] = this.pipeline.ref[prop]; } return accumulator; @@ -216,6 +226,21 @@ return this.viewType === 'child'; }, }, + + methods: { + handleCancelClick() { + this.isCancelling = true; + + eventHub.$emit('openConfirmationModal', { + pipelineId: this.pipeline.id, + endpoint: this.pipeline.cancel_path, + }); + }, + handleRetryClick() { + this.isRetrying = true; + eventHub.$emit('retryPipeline', this.pipeline.retry_path); + }, + }, }; </script> <template> @@ -287,7 +312,8 @@ <div v-if="displayPipelineActions" - class="table-section section-20 table-button-footer pipeline-actions"> + class="table-section section-20 table-button-footer pipeline-actions" + > <div class="btn-group table-action-buttons"> <pipelines-actions-component v-if="pipeline.details.manual_actions.length" @@ -300,29 +326,27 @@ :artifacts="pipeline.details.artifacts" /> - <async-button-component + <loading-button v-if="pipeline.flags.retryable" - :endpoint="pipeline.retry_path" - css-class="js-pipelines-retry-button btn-default btn-retry" - title="Retry" - icon="repeat" - :pipeline-id="pipeline.id" - data-toggle="modal" - data-target="#confirmation-modal" - type="retry" - /> + @click="handleRetryClick" + container-class="js-pipelines-retry-button btn btn-default btn-retry" + :loading="isRetrying" + :disabled="isRetrying" + > + <icon name="repeat" /> + </loading-button> - <async-button-component + <loading-button v-if="pipeline.flags.cancelable" - :endpoint="pipeline.cancel_path" - css-class="js-pipelines-cancel-button btn-remove" - title="Stop" - icon="close" - :pipeline-id="pipeline.id" + @click="handleCancelClick" data-toggle="modal" data-target="#confirmation-modal" - type="stop" - /> + container-class="js-pipelines-cancel-button btn btn-remove" + :loading="isCancelling" + :disabled="isCancelling" + > + <icon name="close" /> + </loading-button> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 6d87f75ae8e..de0faf181e5 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -53,10 +53,12 @@ export default { }); eventHub.$on('postAction', this.postAction); + eventHub.$on('retryPipeline', this.postAction); eventHub.$on('clickedDropdown', this.updateTable); }, beforeDestroy() { eventHub.$off('postAction', this.postAction); + eventHub.$off('retryPipeline', this.postAction); eventHub.$off('clickedDropdown', this.updateTable); }, destroyed() { diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index ee4eb3581f3..a2227b2f554 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -111,7 +111,13 @@ </td> <td> - {{ timeFormated(item.createdAt) }} + <span + v-tooltip + :title="tooltipTitle(item.createdAt)" + data-placement="bottom" + > + {{ timeFormated(item.createdAt) }} + </span> </td> <td class="content"> diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index 97d5cf96bcb..96dfff77859 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -15,7 +15,7 @@ export default class UserCallout { init() { if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { - $('.js-close-callout').on('click', e => this.dismissCallout(e)); + this.userCalloutBody.find('.js-close-callout').on('click', e => this.dismissCallout(e)); } } @@ -23,12 +23,15 @@ export default class UserCallout { const $currentTarget = $(e.currentTarget); if (this.options.setCalloutPerProject) { - Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('projectPath') }); + Cookies.set(this.cookieName, 'true', { + expires: 365, + path: this.userCalloutBody.data('projectPath'), + }); } else { Cookies.set(this.cookieName, 'true', { expires: 365 }); } - if ($currentTarget.hasClass('close')) { + if ($currentTarget.hasClass('close') || $currentTarget.hasClass('js-close')) { this.userCalloutBody.remove(); } } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 7bef2e97349..1fea231c816 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -109,12 +109,12 @@ export default { rel="noopener noreferrer nofollow" class="deploy-link js-deploy-url" > + {{ deployment.external_url_formatted }} <i class="fa fa-external-link" aria-hidden="true" > </i> - {{ deployment.external_url_formatted }} </a> </template> <span diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index e832d94d32f..88c13a1f340 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -70,12 +70,14 @@ /> </transition> <transition name="fade"> - <span - v-if="label" - class="js-loading-button-label" - > - {{ label }} - </span> + <slot> + <span + v-if="label" + class="js-loading-button-label" + > + {{ label }} + </span> + </slot> </transition> </button> </template> diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss index 16293d32dfa..744fd0ff796 100644 --- a/app/assets/stylesheets/framework/terms.scss +++ b/app/assets/stylesheets/framework/terms.scss @@ -17,6 +17,7 @@ display: flex; align-items: center; justify-content: space-between; + line-height: $line-height-base; .title { display: flex; @@ -33,10 +34,14 @@ .navbar-collapse { padding-right: 0; + + .navbar-nav { + margin: 0; + } } - .nav li a { - color: $theme-gray-700; + .nav li { + float: none; } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 2caffec66ac..2843d70c645 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,8 +13,7 @@ class ApplicationController < ActionController::Base before_action :authenticate_sessionless_user! before_action :authenticate_user! - before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms }, - unless: :peek_request? + before_action :enforce_terms!, if: :should_enforce_terms? before_action :validate_user_service_ticket! before_action :check_password_expiration before_action :ldap_security_check @@ -373,4 +372,10 @@ class ApplicationController < ActionController::Base def peek_request? request.path.start_with?('/-/peek') end + + def should_enforce_terms? + return false unless Gitlab::CurrentSettings.current_application_settings.enforce_terms + + !(peek_request? || devise_controller?) + end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 134b0dfc0db..ef3eba80154 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -11,13 +11,20 @@ class Groups::GroupMembersController < Groups::ApplicationController :override def index + can_manage_members = can?(current_user, :admin_group_member, @group) + @sort = params[:sort].presence || sort_value_name @project = @group.projects.find(params[:project_id]) if params[:project_id] @members = GroupMembersFinder.new(@group).execute - @members = @members.non_invite unless can?(current_user, :admin_group, @group) + @members = @members.non_invite unless can_manage_members @members = @members.search(params[:search]) if params[:search].present? @members = @members.sort_by_attribute(@sort) + + if can_manage_members && params[:two_factor].present? + @members = @members.filter_by_2fa(params[:two_factor]) + end + @members = @members.page(params[:page]).per(50) @members = present_members(@members.includes(:user)) diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb index 95c5c3432d5..ab685b9106e 100644 --- a/app/controllers/users/terms_controller.rb +++ b/app/controllers/users/terms_controller.rb @@ -3,6 +3,10 @@ module Users include InternalRedirect skip_before_action :enforce_terms! + skip_before_action :check_password_expiration + skip_before_action :check_two_factor_requirement + skip_before_action :require_email + before_action :terms layout 'terms' diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index e803cd3a8d8..ce9373f5883 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -42,22 +42,11 @@ module UsersHelper items << :sign_out if current_user - # TODO: Remove these conditions when the permissions are prevented in - # https://gitlab.com/gitlab-org/gitlab-ce/issues/45849 - terms_not_enforced = !Gitlab::CurrentSettings - .current_application_settings - .enforce_terms? - required_terms_accepted = terms_not_enforced || current_user.terms_accepted? + return items if current_user&.required_terms_not_accepted? - items << :help if required_terms_accepted - - if can?(current_user, :read_user, current_user) && required_terms_accepted - items << :profile - end - - if can?(current_user, :update_user, current_user) && required_terms_accepted - items << :settings - end + items << :help + items << :profile if can?(current_user, :read_user, current_user) + items << :settings if can?(current_user, :update_user, current_user) items end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index ed8b30dae49..bda69f85a78 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -108,7 +108,13 @@ module Ci end def assign_to(project, current_user = nil) - self.is_shared = false if shared? + if shared? + self.is_shared = false if shared? + self.runner_type = :project_type + elsif group_type? + raise ArgumentError, 'Transitioning a group runner to a project runner is not supported' + end + self.save project.runner_projects.create(runner_id: self.id) end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 915ad6959be..0176a12a131 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -4,7 +4,9 @@ module Routable extend ActiveSupport::Concern included do - has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + # Remove `inverse_of: source` when upgraded to rails 5.2 + # See https://github.com/rails/rails/pull/28808 + has_one :route, as: :source, autosave: true, dependent: :destroy, inverse_of: :source # rubocop:disable Cop/ActiveRecordDependent has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :route, presence: true diff --git a/app/models/member.rb b/app/models/member.rb index eac4a22a03f..68572f2e33a 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -96,6 +96,17 @@ class Member < ActiveRecord::Base joins(:user).merge(User.search(query)) end + def filter_by_2fa(value) + case value + when 'enabled' + left_join_users.merge(User.with_two_factor_indistinct) + when 'disabled' + left_join_users.merge(User.without_two_factor) + else + all + end + end + def sort_by_attribute(method) case method.to_s when 'access_level_asc' then reorder(access_level: :asc) diff --git a/app/models/user.rb b/app/models/user.rb index a9cfd39f604..dfef065f094 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -237,14 +237,18 @@ class User < ActiveRecord::Base scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } - def self.with_two_factor + def self.with_two_factor_indistinct joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") - .where("u2f.id IS NOT NULL OR otp_required_for_login = ?", true).distinct(arel_table[:id]) + .where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true) + end + + def self.with_two_factor + with_two_factor_indistinct.distinct(arel_table[:id]) end def self.without_two_factor joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") - .where("u2f.id IS NULL AND otp_required_for_login = ?", false) + .where("u2f.id IS NULL AND users.otp_required_for_login = ?", false) end # @@ -1193,6 +1197,11 @@ class User < ActiveRecord::Base accepted_term_id.present? end + def required_terms_not_accepted? + Gitlab::CurrentSettings.current_application_settings.enforce_terms? && + !terms_accepted? + end + protected # override, from Devise::Validatable diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 64e550d19d0..1cf5515d9d7 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -1,22 +1,24 @@ class GlobalPolicy < BasePolicy desc "User is blocked" with_options scope: :user, score: 0 - condition(:blocked) { @user.blocked? } + condition(:blocked) { @user&.blocked? } desc "User is an internal user" with_options scope: :user, score: 0 - condition(:internal) { @user.internal? } + condition(:internal) { @user&.internal? } desc "User's access has been locked" with_options scope: :user, score: 0 - condition(:access_locked) { @user.access_locked? } + condition(:access_locked) { @user&.access_locked? } - condition(:can_create_fork, scope: :user) { @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } } + condition(:can_create_fork, scope: :user) { @user && @user.manageable_namespaces.any? { |namespace| @user.can?(:create_projects, namespace) } } + + condition(:required_terms_not_accepted, scope: :user, score: 0) do + @user&.required_terms_not_accepted? + end rule { anonymous }.policy do prevent :log_in - prevent :access_api - prevent :access_git prevent :receive_notifications prevent :use_quick_actions prevent :create_group @@ -38,6 +40,11 @@ class GlobalPolicy < BasePolicy prevent :use_quick_actions end + rule { required_terms_not_accepted }.policy do + prevent :access_api + prevent :access_git + end + rule { can_create_group }.policy do enable :create_group end diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index ad9d5562ded..c8addc49117 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,10 +1,11 @@ - page_title "Members" +- can_manage_members = can?(current_user, :admin_group_member, @group) .project-members-page.prepend-top-default %h4 Members %hr - - if can?(current_user, :admin_group_member, @group) + - if can_manage_members .project-members-new.append-bottom-default %p.clearfix Add new member to @@ -13,20 +14,23 @@ = render 'shared/members/requests', membership_source: @group, requesters: @requesters - .append-bottom-default.clearfix + .clearfix %h5.member.existing-title Existing members - = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") - = render 'shared/members/sort_dropdown' .panel.panel-default - .panel-heading - Members with access to - %strong= @group.name + .panel-heading.flex-project-members-panel + %span.flex-project-title + Members with access to + %strong= @group.name %span.badge= @members.total_count + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") + - if can_manage_members + = render 'shared/members/filter_2fa_dropdown' + = render 'shared/members/sort_dropdown' %ul.content-list.members-list = render partial: 'shared/members/member', collection: @members, as: :member = paginate @members, theme: 'gitlab' diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index bbfbea4ac7a..662db18cf86 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -8,7 +8,7 @@ .top-area = render 'shared/issuable/nav', type: :issues .nav-controls - = link_to params.merge(rss_url_options), class: 'btn' do + = link_to safe_params.merge(rss_url_options), class: 'btn' do = icon('rss') %span.icon-label Subscribe diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml index a30d6e2688c..87f4151f241 100644 --- a/app/views/layouts/terms.html.haml +++ b/app/views/layouts/terms.html.haml @@ -20,10 +20,10 @@ = brand_header_logo - logo_text = brand_header_logo_type - if logo_text.present? - %span.logo-text.hidden-xs.prepend-left-8 + %span.logo-text.prepend-left-8 = logo_text - if header_link?(:user_dropdown) - .navbar-collapse.collapse + .navbar-collapse %ul.nav.navbar-nav %li.header-user.dropdown = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do diff --git a/app/views/shared/members/_filter_2fa_dropdown.html.haml b/app/views/shared/members/_filter_2fa_dropdown.html.haml new file mode 100644 index 00000000000..95c35c56b3c --- /dev/null +++ b/app/views/shared/members/_filter_2fa_dropdown.html.haml @@ -0,0 +1,11 @@ +- filter = params[:two_factor] || 'everyone' +- filter_options = { 'everyone' => 'Everyone', 'enabled' => 'Enabled', 'disabled' => 'Disabled' } +.dropdown.inline.member-filter-2fa-dropdown + = dropdown_toggle('2FA: ' + filter_options[filter], { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable + %li.dropdown-header + Filter by two-factor authentication + - filter_options.each do |value, title| + %li + = link_to filter_group_project_member_path(two_factor: value), class: ("is-active" if filter == value) do + = title diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 1c139827acf..1961ad6d616 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -20,6 +20,10 @@ %label.label.label-danger %strong Blocked + - if user.two_factor_enabled? + %label.label.label-info + 2FA + - if source.instance_of?(Group) && source != @group · = link_to source.full_name, source, class: "member-group-link" diff --git a/changelogs/unreleased/40725-move-mr-external-link-to-right.yml b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml new file mode 100644 index 00000000000..e3ebeb5eb61 --- /dev/null +++ b/changelogs/unreleased/40725-move-mr-external-link-to-right.yml @@ -0,0 +1,5 @@ +--- +title: Moves MR widget external link icon to the right +merge_request: 18828 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/45715-remove-modal-retry.yml b/changelogs/unreleased/45715-remove-modal-retry.yml new file mode 100644 index 00000000000..04f2ff5142e --- /dev/null +++ b/changelogs/unreleased/45715-remove-modal-retry.yml @@ -0,0 +1,5 @@ +--- +title: Remove modalbox confirmation when retrying a pipeline +merge_request: 18879 +author: +type: changed diff --git a/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml b/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml new file mode 100644 index 00000000000..8a7c549e356 --- /dev/null +++ b/changelogs/unreleased/46210-terms-acceptance-dropdown-menu.yml @@ -0,0 +1,5 @@ +--- +title: 46210 Display logo and user dropdown on mobile for terms page and fix styling +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml new file mode 100644 index 00000000000..c0ba984bfdc --- /dev/null +++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-merge-requests-references-feature.yml @@ -0,0 +1,5 @@ +--- +title: 'Replace the `project/merge_requests/references.feature` spinach test with an rspec analog' +merge_request: 18794 +author: '@blackst0ne' +type: other diff --git a/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml new file mode 100644 index 00000000000..49cd04b065b --- /dev/null +++ b/changelogs/unreleased/bvl-restrict-api-git-for-terms.yml @@ -0,0 +1,6 @@ +--- +title: Block access to the API & git for users that did not accept enforced Terms + of Service +merge_request: 18816 +author: +type: other diff --git a/changelogs/unreleased/docs-42067-document-runner-registration-api.yml b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml new file mode 100644 index 00000000000..6b507174044 --- /dev/null +++ b/changelogs/unreleased/docs-42067-document-runner-registration-api.yml @@ -0,0 +1,5 @@ +--- +title: Expand documentation for Runners API +merge_request: 16484 +author: +type: other diff --git a/changelogs/unreleased/dz-add-2fa-filter.yml b/changelogs/unreleased/dz-add-2fa-filter.yml new file mode 100644 index 00000000000..82d501d6604 --- /dev/null +++ b/changelogs/unreleased/dz-add-2fa-filter.yml @@ -0,0 +1,5 @@ +--- +title: Add 2FA filter to the group members page +merge_request: 18483 +author: +type: changed diff --git a/changelogs/unreleased/fix-registry-created-at-tooltip.yml b/changelogs/unreleased/fix-registry-created-at-tooltip.yml new file mode 100644 index 00000000000..911b3b10fd4 --- /dev/null +++ b/changelogs/unreleased/fix-registry-created-at-tooltip.yml @@ -0,0 +1,5 @@ +--- +title: 'Add missing tooltip to creation date on container registry overview' +merge_request: 18767 +author: Lars Greiss +type: fixed diff --git a/changelogs/unreleased/zj-wiki-find-file-opt-out.yml b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml new file mode 100644 index 00000000000..5af53c56017 --- /dev/null +++ b/changelogs/unreleased/zj-wiki-find-file-opt-out.yml @@ -0,0 +1,5 @@ +--- +title: Finding a wiki page is done by Gitaly by default +merge_request: +author: +type: other diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb index d92cdb97766..89aabe530fe 100644 --- a/config/initializers/6_validations.rb +++ b/config/initializers/6_validations.rb @@ -26,17 +26,6 @@ def validate_storages_config Gitlab.config.repositories.storages.each do |name, repository_storage| storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name) - if repository_storage.is_a?(String) - raise "#{name} is not a valid storage, because it has no `path` key. " \ - "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \ - "For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\n" \ - "If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n" - end - - if !repository_storage.is_a?(Gitlab::GitalyClient::StorageSettings) || repository_storage.legacy_disk_path.nil? - storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example") - end - %w(failure_count_threshold failure_reset_time storage_timeout).each do |setting| # Falling back to the defaults is fine! next if repository_storage[setting].nil? diff --git a/doc/api/runners.md b/doc/api/runners.md index f384ac57bfe..3ca07ce9795 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -411,3 +411,86 @@ DELETE /projects/:id/runners/:runner_id ``` curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/9/runners/9" ``` + +## Register a new Runner + +Register a new Runner for the instance. + +``` +POST /runners +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|---------------------| +| `token` | string | yes | Registration token ([Read how to obtain a token](../ci/runners/README.md)) | +| `description`| string | no | Runner's description| +| `info` | hash | no | Runner's metadata | +| `active` | boolean| no | Whether the Runner is active | +| `locked` | boolean| no | Whether the Runner should be locked for current project | +| `run_untagged` | boolean | no | Whether the Runner should handle untagged jobs | +| `tag_list` | Array[String] | no | List of Runner's tags | +| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job | + +``` +curl --request POST "https://gitlab.example.com/api/v4/runners" --form "token=ipzXrMhuyyJPifUt6ANz" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2" +``` + +Response: + +| Status | Description | +|-----------|---------------------------------| +| 201 | Runner was created | + +Example response: + +```json +{ + "id": "12345", + "token": "6337ff461c94fd3fa32ba3b1ff4125" +} +``` + +## Delete a registered Runner + +Deletes a registed Runner. + +``` +DELETE /runners +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|---------------------| +| `token` | string | yes | Runner's authentication token | + +``` +curl --request DELETE "https://gitlab.example.com/api/v4/runners" --form "token=ebb6fc00521627750c8bb750f2490e" +``` + +Response: + +| Status | Description | +|-----------|---------------------------------| +| 204 | Runner was deleted | + +## Verify authentication for a registered Runner + +Validates authentication credentials for a registered Runner. + +``` +POST /runners/verify +``` + +| Attribute | Type | Required | Description | +|-------------|---------|----------|---------------------| +| `token` | string | yes | Runner's authentication token | + +``` +curl --request POST "https://gitlab.example.com/api/v4/runners/verify" --form "token=ebb6fc00521627750c8bb750f2490e" +``` + +Response: + +| Status | Description | +|-----------|---------------------------------| +| 200 | Credentials are valid | +| 403 | Credentials are invalid | diff --git a/doc/api/services.md b/doc/api/services.md index 92f12acbc73..ec632125325 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -968,7 +968,7 @@ Group Chat Software Set Microsoft Teams service for a project. ``` -PUT /projects/:id/services/microsoft_teams +PUT /projects/:id/services/microsoft-teams ``` Parameters: @@ -982,7 +982,7 @@ Parameters: Delete Microsoft Teams service for a project. ``` -DELETE /projects/:id/services/microsoft_teams +DELETE /projects/:id/services/microsoft-teams ``` ### Get Microsoft Teams service settings @@ -990,7 +990,7 @@ DELETE /projects/:id/services/microsoft_teams Get Microsoft Teams service settings for a project. ``` -GET /projects/:id/services/microsoft_teams +GET /projects/:id/services/microsoft-teams ``` ## Mattermost notifications diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index 9c5258c2cdf..98af87455ec 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -129,8 +129,8 @@ You may see a temporary error message `SchedulerPredicates failed due to Persist Add the GitLab Helm repository and initialize Helm: ```bash -helm repo add gitlab https://charts.gitlab.io helm init +helm repo add gitlab https://charts.gitlab.io ``` Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab) you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future. diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature deleted file mode 100644 index 571612261a9..00000000000 --- a/features/project/merge_requests/references.feature +++ /dev/null @@ -1,31 +0,0 @@ -@project_merge_requests -Feature: Project Merge Requests References - Background: - Given I sign in as "John Doe" - And public project "Community" - And "John Doe" owns public project "Community" - And project "Community" has "Community fix" open merge request - And I logout - And I sign in as "Mary Jane" - And private project "Enterprise" - And "Mary Jane" owns private project "Enterprise" - And project "Enterprise" has "Enterprise issue" open issue - And project "Enterprise" has "Enterprise fix" open merge request - And I visit issue page "Enterprise issue" - And I leave a comment referencing issue "Community fix" - And I visit merge request page "Enterprise fix" - And I leave a comment referencing issue "Community fix" - And I logout - - @javascript - Scenario: Viewing the public issue as a "John Doe" - Given I sign in as "John Doe" - When I visit issue page "Community fix" - Then I should see no notes at all - - @javascript - Scenario: Viewing the public issue as "Mary Jane" - Given I sign in as "Mary Jane" - When I visit issue page "Community fix" - And I should see a note linking to "Enterprise fix" merge request - And I should see a note linking to "Enterprise issue" issue diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index 97fac595d8e..27dd391b83d 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -22,22 +22,10 @@ module SharedAuthentication sign_in(@user) end - step 'I sign in as "John Doe"' do - gitlab_sign_in(user_exists("John Doe")) - end - - step 'I sign in as "Mary Jane"' do - gitlab_sign_in(user_exists("Mary Jane")) - end - step 'I should be redirected to sign in page' do expect(current_path).to eq new_user_session_path end - step "I logout" do - gitlab_sign_out - end - step "I logout directly" do gitlab_sign_out end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index 8d9cd3db9d9..cc6fd48935f 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -5,29 +5,6 @@ module SharedIssuable find('.js-issuable-edit', visible: true).click end - step 'project "Community" has "Community fix" open merge request' do - create_issuable_for_project( - project_name: 'Community', - type: :merge_request, - title: 'Community fix' - ) - end - - step 'project "Enterprise" has "Enterprise issue" open issue' do - create_issuable_for_project( - project_name: 'Enterprise', - title: 'Enterprise issue' - ) - end - - step 'project "Enterprise" has "Enterprise fix" open merge request' do - create_issuable_for_project( - project_name: 'Enterprise', - type: :merge_request, - title: 'Enterprise fix' - ) - end - step 'I leave a comment referencing issue "Community issue"' do leave_reference_comment( issuable: Issue.find_by(title: 'Community issue'), @@ -35,44 +12,6 @@ module SharedIssuable ) end - step 'I leave a comment referencing issue "Community fix"' do - leave_reference_comment( - issuable: MergeRequest.find_by(title: 'Community fix'), - from_project_name: 'Enterprise' - ) - end - - step 'I visit issue page "Enterprise issue"' do - issue = Issue.find_by(title: 'Enterprise issue') - visit project_issue_path(issue.project, issue) - end - - step 'I visit merge request page "Enterprise fix"' do - mr = MergeRequest.find_by(title: 'Enterprise fix') - visit project_merge_request_path(mr.target_project, mr) - end - - step 'I visit issue page "Community fix"' do - mr = MergeRequest.find_by(title: 'Community fix') - visit project_merge_request_path(mr.target_project, mr) - end - - step 'I should see a note linking to "Enterprise fix" merge request' do - visible_note( - issuable: MergeRequest.find_by(title: 'Enterprise fix'), - from_project_name: 'Community', - user_name: 'Mary Jane' - ) - end - - step 'I should see a note linking to "Enterprise issue" issue' do - visible_note( - issuable: Issue.find_by(title: 'Enterprise issue'), - from_project_name: 'Community', - user_name: 'Mary Jane' - ) - end - step 'I click link "Edit" for the merge request' do edit_issuable end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index bf1b88c60d7..4a6dee3c7b8 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -18,8 +18,4 @@ module SharedNote expect(find('.js-md-preview')).to have_content('Nothing to preview.') end end - - step 'I should see no notes at all' do - expect(page).not_to have_css('.note') - end end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index a1945cf5f3d..dbfb90fcc48 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -42,10 +42,6 @@ module SharedProject # Visibility level # ---------------------------------------- - step 'private project "Enterprise"' do - create(:project, :private, :repository, name: 'Enterprise') - end - step 'I should see project "Enterprise"' do expect(page).to have_content "Enterprise" end @@ -70,10 +66,6 @@ module SharedProject end end - step 'public project "Community"' do - create(:project, :public, :repository, name: 'Community') - end - step 'I should see project "Community"' do expect(page).to have_content "Community" end @@ -89,13 +81,6 @@ module SharedProject ) end - step '"Mary Jane" owns private project "Enterprise"' do - user_owns_project( - user_name: 'Mary Jane', - project_name: 'Enterprise' - ) - end - step '"John Doe" owns internal project "Internal"' do user_owns_project( user_name: 'John Doe', @@ -104,14 +89,6 @@ module SharedProject ) end - step '"John Doe" owns public project "Community"' do - user_owns_project( - user_name: 'John Doe', - project_name: 'Community', - visibility: :public - ) - end - step 'public empty project "Empty Public Project"' do create :project_empty_repo, :public, name: "Empty Public Project" end diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index c2113551207..c17089759de 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -45,7 +45,9 @@ module API user = find_user_from_sources return unless user - forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) + unless api_access_allowed?(user) + forbidden!(api_access_denied_message(user)) + end user end @@ -72,6 +74,14 @@ module API end end end + + def api_access_allowed?(user) + Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) + end + + def api_access_denied_message(user) + Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message + end end module ClassMethods diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb new file mode 100644 index 00000000000..af310aa12fc --- /dev/null +++ b/lib/gitlab/auth/user_access_denied_reason.rb @@ -0,0 +1,33 @@ +module Gitlab + module Auth + class UserAccessDeniedReason + def initialize(user) + @user = user + end + + def rejection_message + case rejection_type + when :internal + 'This action cannot be performed by internal users' + when :terms_not_accepted + 'You must accept the Terms of Service in order to perform this action. '\ + 'Please access GitLab from a web browser to accept these terms.' + else + 'Your account has been blocked.' + end + end + + private + + def rejection_type + if @user.internal? + :internal + elsif @user.required_terms_not_accepted? + :terms_not_accepted + else + :blocked + end + end + end + end +end diff --git a/lib/gitlab/build_access.rb b/lib/gitlab/build_access.rb new file mode 100644 index 00000000000..08a8f846ca5 --- /dev/null +++ b/lib/gitlab/build_access.rb @@ -0,0 +1,12 @@ +module Gitlab + class BuildAccess < UserAccess + attr_accessor :user, :project + + # This bypasses the `can?(:access_git)`-check we normally do in `UserAccess` + # for CI. That way if a user was able to trigger a pipeline, then the + # build is allowed to clone the project. + def can_access_git? + true + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 5d47f8b2075..29a3a35812c 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -579,11 +579,6 @@ module Gitlab count_commits(from: from, to: to, **options) end - # Counts the amount of commits between `from` and `to`. - def count_commits_between(from, to, options = {}) - count_commits(from: from, to: to, **options) - end - # old_rev and new_rev are commit ID's # the result of this method is an array of Gitlab::Git::RawDiffChange def raw_changes_between(old_rev, new_rev) diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 84a26fe4a6f..d75a5f15c29 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -67,7 +67,8 @@ module Gitlab end def page(title:, version: nil, dir: nil) - @repository.gitaly_migrate(:wiki_find_page) do |is_enabled| + @repository.gitaly_migrate(:wiki_find_page, + status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_find_page(title: title, version: version, dir: dir) else diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 0d1ee73ca1a..db7c29be94b 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -2,8 +2,6 @@ # class return an instance of `GitlabAccessStatus` module Gitlab class GitAccess - include Gitlab::Utils::StrongMemoize - UnauthorizedError = Class.new(StandardError) NotFoundError = Class.new(StandardError) ProjectCreationError = Class.new(StandardError) @@ -17,7 +15,6 @@ module Gitlab deploy_key_upload: 'This deploy key does not have write access to this project.', no_repo: 'A repository for this project does not exist yet.', project_not_found: 'The project you were looking for could not be found.', - account_blocked: 'Your account has been blocked.', command_not_allowed: "The command you're trying to execute is not allowed.", upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.', receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.', @@ -108,8 +105,11 @@ module Gitlab end def check_active_user! - if user && !user_access.allowed? - raise UnauthorizedError, ERROR_MESSAGES[:account_blocked] + return unless user + + unless user_access.allowed? + message = Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message + raise UnauthorizedError, message end end @@ -340,6 +340,8 @@ module Gitlab def user_access @user_access ||= if ci? CiAccess.new + elsif user && request_from_ci_build? + BuildAccess.new(user, project: project) else UserAccess.new(user, project: project) end diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb index 8668caf0c55..9a576e463e3 100644 --- a/lib/gitlab/gitaly_client/storage_settings.rb +++ b/lib/gitlab/gitaly_client/storage_settings.rb @@ -5,6 +5,14 @@ module Gitlab # directly. class StorageSettings DirectPathAccessError = Class.new(StandardError) + InvalidConfigurationError = Class.new(StandardError) + + INVALID_STORAGE_MESSAGE = <<~MSG.freeze + Storage is invalid because it has no `path` key. + + For source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example. + If you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`. + MSG # This class will give easily recognizable NoMethodErrors Deprecated = Class.new @@ -12,7 +20,8 @@ module Gitlab attr_reader :legacy_disk_path def initialize(storage) - raise "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash) + raise InvalidConfigurationError, "expected a Hash, got a #{storage.class.name}" unless storage.is_a?(Hash) + raise InvalidConfigurationError, INVALID_STORAGE_MESSAGE unless storage.has_key?('path') # Support a nil 'path' field because some of the circuit breaker tests use it. @legacy_disk_path = File.expand_path(storage['path'], Rails.root) if storage['path'] diff --git a/lib/gitlab/multi_collection_paginator.rb b/lib/gitlab/multi_collection_paginator.rb index 43921a8c1c0..fd5de73c526 100644 --- a/lib/gitlab/multi_collection_paginator.rb +++ b/lib/gitlab/multi_collection_paginator.rb @@ -5,7 +5,7 @@ module Gitlab def initialize(*collections, per_page: nil) raise ArgumentError.new('Only 2 collections are supported') if collections.size != 2 - @per_page = per_page || Kaminari.config.default_per_page + @per_page = (per_page || Kaminari.config.default_per_page).to_i @first_collection, @second_collection = collections end diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb index 1fa2a19b0af..4888184403c 100644 --- a/lib/gitlab/repo_path.rb +++ b/lib/gitlab/repo_path.rb @@ -4,7 +4,8 @@ module Gitlab def self.parse(repo_path) wiki = false - project_path = strip_storage_path(repo_path.sub(/\.git\z/, ''), fail_on_not_found: false) + project_path = repo_path.sub(/\.git\z/, '').sub(%r{\A/}, '') + project, was_redirected = find_project(project_path) if project_path.end_with?('.wiki') && project.nil? @@ -17,22 +18,6 @@ module Gitlab [project, wiki, redirected_path] end - def self.strip_storage_path(repo_path, fail_on_not_found: true) - result = repo_path - - storage = Gitlab.config.repositories.storages.values.find do |params| - repo_path.start_with?(params.legacy_disk_path) - end - - if storage - result = result.sub(storage.legacy_disk_path, '') - elsif fail_on_not_found - raise NotFoundError.new("No known storage path matches #{repo_path.inspect}") - end - - result.sub(%r{\A/*}, '') - end - def self.find_project(project_path) project = Project.find_by_full_path(project_path, follow_redirects: true) was_redirected = project && project.full_path.casecmp(project_path) != 0 diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 2281cb420d9..a08fcea27a5 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -490,43 +490,43 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do id: job.id end - context 'when job has a trace artifact' do + context "when job has a trace artifact" do let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) } it 'returns a trace' do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq job.job_artifacts_trace.open.read + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq(job.job_artifacts_trace.open.read) end end - context 'when job has a trace file' do + context "when job has a trace file" do let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } - it 'send a trace file' do + it "send a trace file" do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq 'BUILD TRACE' + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq("BUILD TRACE") end end - context 'when job has a trace in database' do + context "when job has a trace in database" do let(:job) { create(:ci_build, pipeline: pipeline) } before do - job.update_column(:trace, 'Sample trace') + job.update_column(:trace, "Sample trace") end - it 'send a trace file' do + it "send a trace file" do response = subject expect(response).to have_gitlab_http_status(:ok) - expect(response.content_type).to eq 'text/plain; charset=utf-8' - expect(response.body).to eq 'Sample trace' + expect(response.headers["Content-Type"]).to eq("text/plain; charset=utf-8") + expect(response.body).to eq("Sample trace") end end diff --git a/spec/features/groups/members/filter_members_spec.rb b/spec/features/groups/members/filter_members_spec.rb new file mode 100644 index 00000000000..5ddb5894624 --- /dev/null +++ b/spec/features/groups/members/filter_members_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +feature 'Groups > Members > Filter members' do + let(:user) { create(:user) } + let(:user_with_2fa) { create(:user, :two_factor_via_otp) } + let(:group) { create(:group) } + + background do + group.add_owner(user) + group.add_master(user_with_2fa) + + sign_in(user) + end + + scenario 'shows all members' do + visit_members_list + + expect(first_member).to include(user.name) + expect(second_member).to include(user_with_2fa.name) + expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Everyone') + end + + scenario 'shows only 2FA members' do + visit_members_list(two_factor: 'enabled') + + expect(first_member).to include(user_with_2fa.name) + expect(members_list.size).to eq(1) + expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Enabled') + end + + scenario 'shows only non 2FA members' do + visit_members_list(two_factor: 'disabled') + + expect(first_member).to include(user.name) + expect(members_list.size).to eq(1) + expect(page).to have_css('.member-filter-2fa-dropdown .dropdown-toggle-text', text: '2FA: Disabled') + end + + def visit_members_list(options = {}) + visit group_group_members_path(group.to_param, options) + end + + def members_list + page.all('ul.content-list > li') + end + + def first_member + members_list.first.text + end + + def second_member + members_list.last.text + end +end diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb index 8af4b157cd8..9613e22bf24 100644 --- a/spec/features/issuables/markdown_references/internal_references_spec.rb +++ b/spec/features/issuables/markdown_references/internal_references_spec.rb @@ -10,6 +10,7 @@ describe "Internal references", :js do let(:public_project_user) { public_project.owner } let(:public_project) { create(:project, :public, :repository) } let(:public_project_issue) { create(:issue, project: public_project) } + let(:public_project_merge_request) { create(:merge_request, source_project: public_project) } context "when referencing to open issue" do context "from private project" do @@ -77,4 +78,63 @@ describe "Internal references", :js do end end end + + context "when referencing to open merge request" do + context "from private project" do + context "from issue" do + before do + sign_in(private_project_user) + + visit(project_issue_path(private_project, private_project_issue)) + + add_note("##{public_project_merge_request.to_reference(private_project)}") + end + + context "when user doesn't have access to private project" do + before do + sign_in(public_project_user) + + visit(project_merge_request_path(public_project, public_project_merge_request)) + end + + it { expect(page).not_to have_css(".note") } + end + end + + context "from merge request" do + before do + sign_in(private_project_user) + + visit(project_merge_request_path(private_project, private_project_merge_request)) + + add_note("##{public_project_merge_request.to_reference(private_project)}") + end + + context "when user doesn't have access to private project" do + before do + sign_in(public_project_user) + + visit(project_merge_request_path(public_project, public_project_merge_request)) + end + + it "doesn't show any references" do + page.within(".merge-request-details") do + expect(page).not_to have_content("#merge-requests .merge-requests-title") + end + end + end + + context "when user has access to private project" do + before do + visit(project_merge_request_path(public_project, public_project_merge_request)) + end + + it "shows references" do + expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}") + .and have_content(private_project_user.name) + end + end + end + end + end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 90e28483c6c..9c165b17704 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -125,7 +125,7 @@ describe 'Pipelines', :js do context 'when canceling' do before do find('.js-pipelines-cancel-button').click - find('.js-primary-button').click + find('.js-modal-primary-action').click wait_for_requests end @@ -156,7 +156,6 @@ describe 'Pipelines', :js do context 'when retrying' do before do find('.js-pipelines-retry-button').click - find('.js-primary-button').click wait_for_requests end @@ -256,7 +255,7 @@ describe 'Pipelines', :js do context 'when canceling' do before do find('.js-pipelines-cancel-button').click - find('.js-primary-button').click + find('.js-modal-primary-action').click end it 'indicates that pipeline was canceled' do diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 94a2b289e64..6f968a2c590 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -437,5 +437,107 @@ feature 'Login' do expect(current_path).to eq(root_path) end + + context 'when 2FA is required for the user' do + before do + group = create(:group, require_two_factor_authentication: true) + group.add_developer(user) + end + + context 'when the user did not enable 2FA' do + it 'asks to set 2FA before asking to accept the terms' do + visit new_user_session_path + + fill_in 'user_login', with: user.email + fill_in 'user_password', with: '12345678' + + click_button 'Sign in' + + expect_to_be_on_terms_page + click_button 'Accept terms' + + expect(current_path).to eq(profile_two_factor_auth_path) + + fill_in 'pin_code', with: user.reload.current_otp + + click_button 'Register with two-factor app' + click_link 'Proceed' + + expect(current_path).to eq(profile_account_path) + end + end + + context 'when the user already enabled 2FA' do + before do + user.update!(otp_required_for_login: true, + otp_secret: User.generate_otp_secret(32)) + end + + it 'asks the user to accept the terms' do + visit new_user_session_path + + fill_in 'user_login', with: user.email + fill_in 'user_password', with: '12345678' + click_button 'Sign in' + + fill_in 'user_otp_attempt', with: user.reload.current_otp + click_button 'Verify code' + + expect_to_be_on_terms_page + click_button 'Accept terms' + + expect(current_path).to eq(root_path) + end + end + end + + context 'when the users password is expired' do + before do + user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC')) + end + + it 'asks the user to accept the terms before setting a new password' do + visit new_user_session_path + + fill_in 'user_login', with: user.email + fill_in 'user_password', with: '12345678' + click_button 'Sign in' + + expect_to_be_on_terms_page + click_button 'Accept terms' + + expect(current_path).to eq(new_profile_password_path) + + fill_in 'user_current_password', with: '12345678' + fill_in 'user_password', with: 'new password' + fill_in 'user_password_confirmation', with: 'new password' + click_button 'Set new password' + + expect(page).to have_content('Password successfully changed') + end + end + + context 'when the user does not have an email configured' do + let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') } + + before do + stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config]) + end + + it 'asks the user to accept the terms before setting an email' do + gitlab_sign_in_via('saml', user, 'my-uid') + + expect_to_be_on_terms_page + click_button 'Accept terms' + + expect(current_path).to eq(profile_path) + + fill_in 'Email', with: 'hello@world.com' + + click_button 'Update profile settings' + + expect(page).to have_content('Profile was successfully updated') + end + end end end diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb index bf6b5fa3d6a..f9469adbfe3 100644 --- a/spec/features/users/terms_spec.rb +++ b/spec/features/users/terms_spec.rb @@ -81,4 +81,22 @@ describe 'Users > Terms' do expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed") end end + + context 'when the terms are enforced' do + before do + enforce_terms + end + + context 'signing out', :js do + it 'allows the user to sign out without a response' do + visit terms_path + + find('.header-user-dropdown-toggle').click + click_link('Sign out') + + expect(page).to have_content('Sign in') + expect(page).to have_content('Register') + end + end + end end diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb index 1dc307ea922..8d9dc092547 100644 --- a/spec/initializers/6_validations_spec.rb +++ b/spec/initializers/6_validations_spec.rb @@ -42,26 +42,6 @@ describe '6_validations' do expect { validate_storages_config }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.') end end - - context 'with incomplete settings' do - before do - mock_storages('foo' => {}) - end - - it 'throws an error suggesting the user to update its settings' do - expect { validate_storages_config }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.') - end - end - - context 'with deprecated settings structure' do - before do - mock_storages('foo' => 'tmp/tests/paths/a/b/c') - end - - it 'throws an error suggesting the user to update its settings' do - expect { validate_storages_config }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nFor source installations, update your config/gitlab.yml Refer to gitlab.yml.example for an updated example.\n\nIf you're using the Gitlab Development Kit, you can update your configuration running `gdk reconfigure`.\n") - end - end end describe 'validate_storages_paths' do diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index 360b6d4dc15..ff500acd849 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -24,7 +24,7 @@ describe('RepoEditor', () => { f.active = true; f.tempFile = true; vm.$store.state.openFiles.push(f); - vm.$store.state.entries[f.path] = f; + Vue.set(vm.$store.state.entries, f.path, f); vm.monaco = true; vm.$mount(); @@ -215,6 +215,30 @@ describe('RepoEditor', () => { expect(vm.editor.attachModel).toHaveBeenCalledWith(vm.model); }); + it('attaches model to merge request editor', () => { + vm.$store.state.viewer = 'mrdiff'; + vm.file.mrChange = true; + spyOn(vm.editor, 'attachMergeRequestModel'); + + Editor.editorInstance.modelManager.dispose(); + + vm.setupEditor(); + + expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model); + }); + + it('does not attach model to merge request editor when not a MR change', () => { + vm.$store.state.viewer = 'mrdiff'; + vm.file.mrChange = false; + spyOn(vm.editor, 'attachMergeRequestModel'); + + Editor.editorInstance.modelManager.dispose(); + + vm.setupEditor(); + + expect(vm.editor.attachMergeRequestModel).not.toHaveBeenCalledWith(vm.model); + }); + it('adds callback methods', () => { spyOn(vm.editor, 'onPositionChange').and.callThrough(); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js deleted file mode 100644 index e0ea3649646..00000000000 --- a/spec/javascripts/pipelines/async_button_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import Vue from 'vue'; -import asyncButtonComp from '~/pipelines/components/async_button.vue'; -import eventHub from '~/pipelines/event_hub'; - -describe('Pipelines Async Button', () => { - let component; - let AsyncButtonComponent; - - beforeEach(() => { - AsyncButtonComponent = Vue.extend(asyncButtonComp); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'repeat', - cssClass: 'bar', - pipelineId: 123, - type: 'explode', - }, - }).$mount(); - }); - - it('should render a button', () => { - expect(component.$el.tagName).toEqual('BUTTON'); - }); - - it('should render svg icon', () => { - expect(component.$el.querySelector('svg')).not.toBeNull(); - }); - - it('should render the provided title', () => { - expect(component.$el.getAttribute('data-original-title')).toContain('Foo'); - expect(component.$el.getAttribute('aria-label')).toContain('Foo'); - }); - - it('should render the provided cssClass', () => { - expect(component.$el.getAttribute('class')).toContain('bar'); - }); - - describe('With confirm dialog', () => { - it('should call the service when confimation is positive', () => { - eventHub.$on('openConfirmationModal', (data) => { - expect(data.pipelineId).toEqual(123); - expect(data.type).toEqual('explode'); - }); - - component = new AsyncButtonComponent({ - propsData: { - endpoint: '/foo', - title: 'Foo', - icon: 'fa fa-foo', - cssClass: 'bar', - pipelineId: 123, - type: 'explode', - }, - }).$mount(); - - component.$el.click(); - }); - }); -}); diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index de744739e42..05ca4cb9044 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import tableRowComp from '~/pipelines/components/pipelines_table_row.vue'; +import eventHub from '~/pipelines/event_hub'; describe('Pipelines Table Row', () => { const jsonFixtureName = 'pipelines/pipelines.json'; @@ -151,13 +152,37 @@ describe('Pipelines Table Row', () => { describe('actions column', () => { beforeEach(() => { - component = buildComponent(pipeline); + const withActions = Object.assign({}, pipeline); + withActions.flags.cancelable = true; + withActions.flags.retryable = true; + withActions.cancel_path = '/cancel'; + withActions.retry_path = '/retry'; + + component = buildComponent(withActions); }); it('should render the provided actions', () => { - expect( - component.$el.querySelectorAll('.table-section:nth-child(6) ul li').length, - ).toEqual(pipeline.details.manual_actions.length); + expect(component.$el.querySelector('.js-pipelines-retry-button')).not.toBeNull(); + expect(component.$el.querySelector('.js-pipelines-cancel-button')).not.toBeNull(); + }); + + it('emits `retryPipeline` event when retry button is clicked and toggles loading', () => { + eventHub.$on('retryPipeline', (endpoint) => { + expect(endpoint).toEqual('/retry'); + }); + + component.$el.querySelector('.js-pipelines-retry-button').click(); + expect(component.isRetrying).toEqual(true); + }); + + it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => { + eventHub.$on('openConfirmationModal', (data) => { + expect(data.endpoint).toEqual('/cancel'); + expect(data.pipelineId).toEqual(pipeline.id); + }); + + component.$el.querySelector('.js-pipelines-cancel-button').click(); + expect(component.isCancelling).toEqual(true); }); }); }); diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb new file mode 100644 index 00000000000..fa209bed74e --- /dev/null +++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Gitlab::Auth::UserAccessDeniedReason do + include TermsHelper + let(:user) { build(:user) } + + let(:reason) { described_class.new(user) } + + describe '#rejection_message' do + subject { reason.rejection_message } + + context 'when a user is blocked' do + before do + user.block! + end + + it { is_expected.to match /blocked/ } + end + + context 'a user did not accept the enforced terms' do + before do + enforce_terms + end + + it { is_expected.to match /You must accept the Terms of Service/ } + end + + context 'when the user is internal' do + let(:user) { User.ghost } + + it { is_expected.to match /This action cannot be performed by internal users/ } + end + end +end diff --git a/spec/lib/gitlab/build_access_spec.rb b/spec/lib/gitlab/build_access_spec.rb new file mode 100644 index 00000000000..08f50bf4fac --- /dev/null +++ b/spec/lib/gitlab/build_access_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::BuildAccess do + let(:user) { create(:user) } + let(:project) { create(:project) } + + describe '#can_do_action' do + subject { described_class.new(user, project: project).can_do_action?(:download_code) } + + context 'when the user can do an action on the project but cannot access git' do + before do + user.block! + project.add_developer(user) + end + + it { is_expected.to be(true) } + end + + context 'when the user cannot do an action on the project' do + it { is_expected.to be(false) } + end + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 6c625596605..317a932d5a6 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe Gitlab::GitAccess do - set(:user) { create(:user) } + include TermsHelper + + let(:user) { create(:user) } let(:actor) { user } let(:project) { create(:project, :repository) } @@ -1040,6 +1042,96 @@ describe Gitlab::GitAccess do end end + context 'terms are enforced' do + before do + enforce_terms + end + + shared_examples 'access after accepting terms' do + let(:actions) do + [-> { pull_access_check }, + -> { push_access_check }] + end + + it 'blocks access when the user did not accept terms', :aggregate_failures do + actions.each do |action| + expect { action.call }.to raise_unauthorized(/You must accept the Terms of Service in order to perform this action/) + end + end + + it 'allows access when the user accepted the terms', :aggregate_failures do + accept_terms(user) + + actions.each do |action| + expect { action.call }.not_to raise_error + end + end + end + + describe 'as an anonymous user to a public project' do + let(:actor) { nil } + let(:project) { create(:project, :public, :repository) } + + it { expect { pull_access_check }.not_to raise_error } + end + + describe 'as a guest to a public project' do + let(:project) { create(:project, :public, :repository) } + + it_behaves_like 'access after accepting terms' do + let(:actions) { [-> { pull_access_check }] } + end + end + + describe 'as a reporter to the project' do + before do + project.add_reporter(user) + end + + it_behaves_like 'access after accepting terms' do + let(:actions) { [-> { pull_access_check }] } + end + end + + describe 'as a developer of the project' do + before do + project.add_developer(user) + end + + it_behaves_like 'access after accepting terms' + end + + describe 'as a master of the project' do + before do + project.add_master(user) + end + + it_behaves_like 'access after accepting terms' + end + + describe 'as an owner of the project' do + let(:project) { create(:project, :repository, namespace: user.namespace) } + + it_behaves_like 'access after accepting terms' + end + + describe 'when a ci build clones the project' do + let(:protocol) { 'http' } + let(:authentication_abilities) { [:build_download_code] } + let(:auth_result_type) { :build } + + before do + project.add_developer(user) + end + + it "doesn't block http pull" do + aggregate_failures do + expect { pull_access_check }.not_to raise_error + end + end + end + end + private def raise_unauthorized(message) diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb new file mode 100644 index 00000000000..c89913ec8e9 --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::GitalyClient::StorageSettings do + describe "#initialize" do + context 'when the storage contains no path' do + it 'raises an error' do + expect do + described_class.new("foo" => {}) + end.to raise_error(described_class::InvalidConfigurationError) + end + end + + context "when the argument isn't a hash" do + it 'raises an error' do + expect do + described_class.new("test") + end.to raise_error("expected a Hash, got a String") + end + end + + context 'when the storage is valid' do + it 'raises no error' do + expect do + described_class.new("path" => Rails.root) + end.not_to raise_error + end + end + end +end diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index f030f371372..13940713dfc 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -45,25 +45,6 @@ describe ::Gitlab::RepoPath do end end - describe '.strip_storage_path' do - before do - allow(Gitlab.config.repositories).to receive(:storages).and_return({ - 'storage1' => Gitlab::GitalyClient::StorageSettings.new('path' => '/foo'), - 'storage2' => Gitlab::GitalyClient::StorageSettings.new('path' => '/bar') - }) - end - - it 'strips the storage path' do - expect(described_class.strip_storage_path('/bar/foo/qux/baz.git')).to eq('foo/qux/baz.git') - end - - it 'raises NotFoundError if no storage matches the path' do - expect { described_class.strip_storage_path('/doesnotexist/foo.git') }.to raise_error( - described_class::NotFoundError - ) - end - end - describe '.find_project' do let(:project) { create(:project) } let(:redirect) { project.route.create_redirect('foo/bar/baz') } diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index eb59ba7cbe9..e2b212f4f4c 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -200,15 +200,29 @@ describe Ci::Runner do describe '#assign_to' do let!(:project) { FactoryBot.create(:project) } - let!(:shared_runner) { FactoryBot.create(:ci_runner, :shared) } - before do - shared_runner.assign_to(project) + subject { runner.assign_to(project) } + + context 'with shared_runner' do + let!(:runner) { FactoryBot.create(:ci_runner, :shared) } + + it 'transitions shared runner to project runner and assigns project' do + subject + expect(runner).to be_specific + expect(runner).to be_project_type + expect(runner.projects).to eq([project]) + expect(runner.only_for?(project)).to be_truthy + end end - it { expect(shared_runner).to be_specific } - it { expect(shared_runner.projects).to eq([project]) } - it { expect(shared_runner.only_for?(project)).to be_truthy } + context 'with group runner' do + let!(:runner) { FactoryBot.create(:ci_runner, runner_type: :group_type) } + + it 'raises an error' do + expect { subject } + .to raise_error(ArgumentError, 'Transitioning a group runner to a project runner is not supported') + end + end end describe '.online' do diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 5f2c723d483..3ef59457c5f 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -55,13 +55,9 @@ describe Clusters::Applications::Runner do context 'without a runner' do let(:project) { create(:project) } - let(:cluster) { create(:cluster) } + let(:cluster) { create(:cluster, projects: [project]) } let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) } - before do - cluster.projects << project - end - it 'creates a runner' do expect do subject diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index add481b8096..ab7f89f9bf4 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -239,17 +239,19 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching it { is_expected.to be_nil } end - context 'when kubernetes responds with valid pods' do + context 'when kubernetes responds with valid pods and deployments' do before do stub_kubeclient_pods + stub_kubeclient_deployments 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) end it { expect { subject }.to raise_error(Kubeclient::HttpError) } @@ -258,9 +260,10 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching context 'when kubernetes responds with 404s' do before do stub_kubeclient_pods(status: 404) + stub_kubeclient_deployments(status: 404) end - it { is_expected.to eq(pods: []) } + it { is_expected.to include(pods: []) } end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3f2eb58f009..ad094b3ed48 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe User do include ProjectForksHelper + include TermsHelper describe 'modules' do subject { described_class } @@ -2728,4 +2729,30 @@ describe User do .to change { RedirectRoute.where(path: 'foo').count }.by(-1) end end + + describe '#required_terms_not_accepted?' do + let(:user) { build(:user) } + subject { user.required_terms_not_accepted? } + + context "when terms are not enforced" do + it { is_expected.to be_falsy } + end + + context "when terms are enforced and accepted by the user" do + before do + enforce_terms + accept_terms(user) + end + + it { is_expected.to be_falsy } + end + + context "when terms are enforced but the user has not accepted" do + before do + enforce_terms + end + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index ec26810e371..873673b50ef 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -90,4 +90,94 @@ describe GlobalPolicy do it { is_expected.to be_allowed(:update_custom_attribute) } end end + + shared_examples 'access allowed when terms accepted' do |ability| + it { is_expected.not_to be_allowed(ability) } + + it "allows #{ability} when the user accepted the terms" do + accept_terms(current_user) + + is_expected.to be_allowed(ability) + end + end + + describe 'API access' do + context 'regular user' do + it { is_expected.to be_allowed(:access_api) } + end + + context 'admin' do + let(:current_user) { create(:admin) } + + it { is_expected.to be_allowed(:access_api) } + end + + context 'anonymous' do + let(:current_user) { nil } + + it { is_expected.to be_allowed(:access_api) } + end + + context 'when terms are enforced' do + before do + enforce_terms + end + + context 'regular user' do + it_behaves_like 'access allowed when terms accepted', :access_api + end + + context 'admin' do + let(:current_user) { create(:admin) } + + it_behaves_like 'access allowed when terms accepted', :access_api + end + + context 'anonymous' do + let(:current_user) { nil } + + it { is_expected.to be_allowed(:access_api) } + end + end + end + + describe 'git access' do + describe 'regular user' do + it { is_expected.to be_allowed(:access_git) } + end + + describe 'admin' do + let(:current_user) { create(:admin) } + + it { is_expected.to be_allowed(:access_git) } + end + + describe 'anonymous' do + let(:current_user) { nil } + + it { is_expected.to be_allowed(:access_git) } + end + + context 'when terms are enforced' do + before do + enforce_terms + end + + context 'regular user' do + it_behaves_like 'access allowed when terms accepted', :access_git + end + + context 'admin' do + let(:current_user) { create(:admin) } + + it_behaves_like 'access allowed when terms accepted', :access_git + end + + context 'anonymous' do + let(:current_user) { nil } + + it { is_expected.to be_allowed(:access_git) } + end + end + end end diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 837389451e8..d3ab44c0d7e 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -6,6 +6,7 @@ describe API::Helpers do include API::APIGuard::HelperMethods include described_class include SentryHelper + include TermsHelper let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -163,6 +164,23 @@ describe API::Helpers do expect { current_user }.to raise_error /403/ end + context 'when terms are enforced' do + before do + enforce_terms + env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token + end + + it 'returns a 403 when a user has not accepted the terms' do + expect { current_user }.to raise_error /You must accept the Terms of Service/ + end + + it 'sets the current user when the user accepted the terms' do + accept_terms(user) + + expect(current_user).to eq(user) + end + end + it "sets current_user" do env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 494db30e8e0..2514dab1714 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -1,6 +1,7 @@ require "spec_helper" describe 'Git HTTP requests' do + include TermsHelper include GitHttpHelpers include WorkhorseHelpers include UserActivitiesHelpers @@ -824,4 +825,56 @@ describe 'Git HTTP requests' do end end end + + context 'when terms are enforced' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + let(:path) { "#{project.full_path}.git" } + let(:env) { { user: user.username, password: user.password } } + + before do + project.add_master(user) + enforce_terms + end + + it 'blocks git access when the user did not accept terms', :aggregate_failures do + clone_get(path, env) do |response| + expect(response).to have_gitlab_http_status(:forbidden) + end + + download(path, env) do |response| + expect(response).to have_gitlab_http_status(:forbidden) + end + + upload(path, env) do |response| + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context 'when the user accepted the terms' do + before do + accept_terms(user) + end + + it 'allows clones' do + clone_get(path, env) do |response| + expect(response).to have_gitlab_http_status(:ok) + end + end + + it_behaves_like 'pulls are allowed' + it_behaves_like 'pushes are allowed' + end + + context 'from CI' do + let(:build) { create(:ci_build, :running) } + let(:env) { { user: 'gitlab-ci-token', password: build.token } } + + before do + build.update!(user: user, project: project) + end + + it_behaves_like 'pulls are allowed' + end + end end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index e46b61b6461..683a64504a1 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -9,8 +9,13 @@ module KubernetesHelpers kube_response(kube_pods_body) end + def kube_deployments_response + kube_response(kube_deployments_body) + end + def stub_kubeclient_discover(api_url) WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) + WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body)) end def stub_kubeclient_pods(response = nil) @@ -20,6 +25,13 @@ module KubernetesHelpers WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response) end + def stub_kubeclient_deployments(response = nil) + stub_kubeclient_discover(service.api_url) + deployments_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{service.actual_namespace}/deployments" + + WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response) + end + def stub_kubeclient_get_secrets(api_url, **options) WebMock.stub_request(:get, api_url + '/api/v1/secrets') .to_return(kube_response(kube_v1_secrets_body(options))) @@ -53,6 +65,18 @@ module KubernetesHelpers "kind" => "APIResourceList", "resources" => [ { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, + { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, + { "name" => "secrets", "namespaced" => true, "kind" => "Secret" } + ] + } + end + + def kube_v1beta1_discovery_body + { + "kind" => "APIResourceList", + "resources" => [ + { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, + { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, { "name" => "secrets", "namespaced" => true, "kind" => "Secret" } ] } @@ -65,14 +89,25 @@ module KubernetesHelpers } end + def kube_deployments_body + { + "kind" => "DeploymentList", + "items" => [kube_deployment] + } + end + # 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") + def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil) { "metadata" => { "name" => name, + "generate_name" => "generated-name-with-suffix", "creationTimestamp" => "2016-11-25T19:55:19Z", - "labels" => { "app" => app } + "labels" => { + "app" => app, + "track" => track + } }, "spec" => { "containers" => [ @@ -80,7 +115,27 @@ module KubernetesHelpers { "name" => "container-1" } ] }, - "status" => { "phase" => "Running" } + "status" => { "phase" => status } + } + end + + def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil) + { + "metadata" => { + "name" => name, + "generation" => 4, + "labels" => { + "app" => app, + "track" => track + }.compact + }, + "spec" => { "replicas" => 3 }, + "status" => { + "observedGeneration" => 4, + "replicas" => 3, + "updatedReplicas" => 3, + "availableReplicas" => 3 + } } end @@ -101,4 +156,12 @@ module KubernetesHelpers terminal end end + + def kube_deployment_rollout_status + ::Gitlab::Kubernetes::RolloutStatus.from_deployments(kube_deployment) + end + + def empty_deployment_rollout_status + ::Gitlab::Kubernetes::RolloutStatus.from_deployments() + end end diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 020031af3cb..a00c6e89a1d 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -12,8 +12,10 @@ # AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project # level, or manually added below. # -# If you want to deploy to staging first, or enable canary deploys, -# uncomment the relevant jobs in the pipeline below. +# Continuous deployment to production is enabled by default. +# If you want to deploy to staging first, or enable incremental rollouts, +# set STAGING_ENABLED or INCREMENTAL_ROLLOUT_ENABLED environment variables. +# If you want to use canary deployments, uncomment the canary job. # # If Auto DevOps fails to detect the proper buildpack, or if you want to # specify a custom buildpack, set a project variable `BUILDPACK_URL` to the @@ -88,14 +90,6 @@ codequality: artifacts: paths: [codeclimate.json] -license_management: - image: registry.gitlab.com/gitlab-org/security-products/license-management:latest - allow_failure: true - script: - - license_management - artifacts: - paths: [gl-license-report.json] - performance: stage: performance image: docker:stable @@ -223,8 +217,8 @@ stop_review: # Staging deploys are disabled by default since # continuous deployment to production is enabled by default # If you prefer to automatically deploy to staging and -# only manually promote to production, enable this job by removing the dot (.), -# and uncomment the `when: manual` line in the `production` job. +# only manually promote to production, enable this job by setting +# STAGING_ENABLED. staging: stage: staging @@ -245,13 +239,9 @@ staging: kubernetes: active variables: - $STAGING_ENABLED - except: - variables: - - $INCREMENTAL_ROLLOUT_ENABLED # Canaries are disabled by default, but if you want them, -# and know what the downsides are, enable this job by removing the dot (.), -# and uncomment the `when: manual` line in the `production` job. +# and know what the downsides are, enable this job by removing the dot (.). .canary: stage: canary @@ -272,11 +262,6 @@ staging: - master kubernetes: active -# This job continuously deploys to production on every push to `master`. -# To make this a manual process, either because you're enabling `staging` -# or `canary` deploys, or you simply want more control over when you deploy -# to production, uncomment the `when: manual` line in the `production` job. - .production: &production_template stage: production script: @@ -310,6 +295,7 @@ production: production_manual: <<: *production_template when: manual + allow_failure: false only: refs: - master @@ -345,6 +331,7 @@ rollout 10%: <<: *rollout_template variables: ROLLOUT_PERCENTAGE: 10 + when: manual only: refs: - master @@ -379,6 +366,7 @@ rollout 50%: rollout 100%: <<: *production_template when: manual + allow_failure: false only: refs: - master @@ -428,14 +416,6 @@ rollout 100%: "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code } - function license_management() { - if echo $GITLAB_FEATURES |grep license_management > /dev/null ; then - /run.sh . - else - echo "License management is not available in your subscription" - fi - } - function sast() { case "$CI_SERVER_VERSION" in *-ee) @@ -562,12 +542,14 @@ rollout 100%: replicas=$(get_replicas "$track" "$percentage") - helm upgrade --reuse-values \ - --wait \ - --set replicaCount="$replicas" \ - --namespace="$KUBE_NAMESPACE" \ - "$name" \ - chart/ + if [[ -n "$(helm ls -q "^$name$")" ]]; then + helm upgrade --reuse-values \ + --wait \ + --set replicaCount="$replicas" \ + --namespace="$KUBE_NAMESPACE" \ + "$name" \ + chart/ + fi } function install_dependencies() { |