diff options
107 files changed, 1944 insertions, 1022 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 137c1281121..359ee08a7ce 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.85.0 +0.87.0 @@ -411,7 +411,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.85.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.87.0', require: 'gitaly' # Locked until https://github.com/google/protobuf/issues/4210 is closed gem 'google-protobuf', '= 3.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index 89b86ae0259..6918f92aa84 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -285,7 +285,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.85.0) + gitaly-proto (0.87.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (5.3.3) @@ -1057,7 +1057,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.85.0) + gitaly-proto (~> 0.87.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 35618398468..1325a268214 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -99,12 +99,6 @@ </p> `; }, - gitlabRunnerDescription() { - return _.escape(s__( - `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs - and send the results back to GitLab.`, - )); - }, prometheusDescription() { return sprintf( _.escape(s__( @@ -256,6 +250,22 @@ > </div> </application-row> + <application-row + id="runner" + :title="applications.runner.title" + title-link="https://docs.gitlab.com/runner/" + :status="applications.runner.status" + :status-reason="applications.runner.statusReason" + :request-status="applications.runner.requestStatus" + :request-reason="applications.runner.requestReason" + > + <div slot="description"> + {{ s__(`ClusterIntegration|GitLab Runner connects to this + project's repository and executes CI/CD jobs, + pushing results back and deploying, + applications to production.`) }} + </div> + </application-row> <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index ee49a7be0b2..e6390f0855b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -16,6 +16,7 @@ export default class FilteredSearchDropdownManager { page, isGroup, isGroupAncestor, + isGroupDecendent, filteredSearchTokenKeys, }) { this.container = FilteredSearchContainer.container; @@ -26,6 +27,7 @@ export default class FilteredSearchDropdownManager { this.page = page; this.groupsOnly = isGroup; this.groupAncestor = isGroupAncestor; + this.isGroupDecendent = isGroupDecendent; this.setupMapping(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index c6970d7837f..71b7e80335b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -22,11 +22,13 @@ export default class FilteredSearchManager { page, isGroup = false, isGroupAncestor = false, + isGroupDecendent = false, filteredSearchTokenKeys = FilteredSearchTokenKeys, stateFiltersSelector = '.issues-state-filters', }) { this.isGroup = isGroup; this.isGroupAncestor = isGroupAncestor; + this.isGroupDecendent = isGroupDecendent; this.states = ['opened', 'closed', 'merged', 'all']; this.page = page; diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index a19bb882410..600024c21c3 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -1,5 +1,6 @@ import _ from 'underscore'; -import AjaxCache from '../lib/utils/ajax_cache'; +import AjaxCache from '~/lib/utils/ajax_cache'; +import { objectToQueryString } from '~/lib/utils/common_utils'; import Flash from '../flash'; import FilteredSearchContainer from './container'; import UsersCache from '../lib/utils/users_cache'; @@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens { }; } + /** + * Returns a computed API endpoint + * and query string composed of values from endpointQueryParams + * @param {String} endpoint + * @param {String} endpointQueryParams + */ + static getEndpointWithQueryParams(endpoint, endpointQueryParams) { + if (!endpointQueryParams) { + return endpoint; + } + + const queryString = objectToQueryString(JSON.parse(endpointQueryParams)); + return `${endpoint}?${queryString}`; + } + static unselectTokens() { const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected'); [].forEach.call(otherTokens, t => t.classList.remove('selected')); @@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens { static updateLabelTokenColor(tokenValueContainer, tokenValue) { const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search'); const baseEndpoint = filteredSearchInput.dataset.baseEndpoint; - const labelsEndpoint = `${baseEndpoint}/labels.json`; + const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams( + `${baseEndpoint}/labels.json`, + filteredSearchInput.dataset.endpointQueryParams, + ); return AjaxCache.retrieve(labelsEndpoint) .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index e741789fbb6..ed90db317df 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -302,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => { }, {}); }; +/** + * Converts object with key-value pairs + * into query-param string + * + * @param {Object} params + */ +export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&'); + export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname); /** diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 659dc9eaa1f..53b01cca1d3 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -36,8 +36,11 @@ import initBreadcrumbs from './breadcrumb'; import initDispatcher from './dispatcher'; -// eslint-disable-next-line global-require, import/no-commonjs -if (process.env.NODE_ENV !== 'production') require('./test_utils/'); +// inject test utilities if necessary +if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) { + $.fx.off = true; + import(/* webpackMode: "eager" */ './test_utils/'); +} svg4everybody(); diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js index f4cba998fa7..972fdb2b791 100644 --- a/app/assets/javascripts/mr_notes/index.js +++ b/app/assets/javascripts/mr_notes/index.js @@ -3,7 +3,7 @@ import notesApp from '../notes/components/notes_app.vue'; import discussionCounter from '../notes/components/discussion_counter.vue'; import store from '../notes/stores'; -document.addEventListener('DOMContentLoaded', () => { +export default function initMrNotes() { new Vue({ // eslint-disable-line el: '#js-vue-mr-discussions', components: { @@ -38,4 +38,4 @@ document.addEventListener('DOMContentLoaded', () => { return createElement('discussion-counter'); }, }); -}); +} diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js index e3414d9afff..5b2473e0989 100644 --- a/app/assets/javascripts/two_factor_auth.js +++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js @@ -1,4 +1,4 @@ -import U2FRegister from './u2f/register'; +import U2FRegister from '~/u2f/register'; document.addEventListener('DOMContentLoaded', () => { const twoFactorNode = document.querySelector('.js-two-factor-auth'); diff --git a/app/assets/javascripts/pages/projects/environments/terminal/index.js b/app/assets/javascripts/pages/projects/environments/terminal/index.js new file mode 100644 index 00000000000..7129e24cee1 --- /dev/null +++ b/app/assets/javascripts/pages/projects/environments/terminal/index.js @@ -0,0 +1,3 @@ +import initTerminal from '~/terminal/'; + +document.addEventListener('DOMContentLoaded', initTerminal); diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 37503cc1542..500fbd27340 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -11,4 +11,3 @@ export default function () { new ZenMode(); // eslint-disable-line no-new initIssuableSidebar(); } - diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index da27c22f537..28d8761b502 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -30,4 +30,3 @@ export default function () { howToMerge(); initWidget(); } - diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index 3e72f7a6f37..e5b2827b50c 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -1,7 +1,13 @@ +import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils'; +import initMrNotes from '~/mr_notes'; import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initShow from '../init_merge_request_show'; document.addEventListener('DOMContentLoaded', () => { initShow(); initSidebarBundle(); + + if (hasVueMRDiscussionsCookie()) { + initMrNotes(); + } }); diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js new file mode 100644 index 00000000000..35564754ee0 --- /dev/null +++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js @@ -0,0 +1,3 @@ +import initRegistryImages from '~/registry/index'; + +document.addEventListener('DOMContentLoaded', initRegistryImages); diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js index 001128ead59..788d86d1192 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -4,10 +4,14 @@ import ProtectedTagCreate from '~/protected_tags/protected_tag_create'; import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; import initSettingsPanels from '~/settings_panels'; import initDeployKeys from '~/deploy_keys'; +import ProtectedBranchCreate from '~/protected_branches/protected_branch_create'; +import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list'; document.addEventListener('DOMContentLoaded', () => { new ProtectedTagCreate(); new ProtectedTagEditList(); initDeployKeys(); initSettingsPanels(); + new ProtectedBranchCreate(); // eslint-disable-line no-new + new ProtectedBranchEditList(); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js index 57f08701a4f..7fdf4ee0bf3 100644 --- a/app/assets/javascripts/pages/search/init_filtered_search.js +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -5,6 +5,7 @@ export default ({ filteredSearchTokenKeys, isGroup, isGroupAncestor, + isGroupDecendent, stateFiltersSelector, }) => { const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); @@ -13,6 +14,7 @@ export default ({ page, isGroup, isGroupAncestor, + isGroupDecendent, filteredSearchTokenKeys, stateFiltersSelector, }); diff --git a/app/assets/javascripts/protected_branches/index.js b/app/assets/javascripts/protected_branches/index.js deleted file mode 100644 index c9e7af127d2..00000000000 --- a/app/assets/javascripts/protected_branches/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable no-unused-vars */ - -import ProtectedBranchCreate from './protected_branch_create'; -import ProtectedBranchEditList from './protected_branch_edit_list'; - -$(() => { - const protectedBranchCreate = new ProtectedBranchCreate(); - const protectedBranchEditList = new ProtectedBranchEditList(); -}); diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js index d8edff73f72..6fb125192b2 100644 --- a/app/assets/javascripts/registry/index.js +++ b/app/assets/javascripts/registry/index.js @@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -document.addEventListener('DOMContentLoaded', () => new Vue({ +export default () => new Vue({ el: '#js-vue-registry-images', components: { registryApp, @@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, }); }, -})); +}); diff --git a/app/assets/javascripts/terminal/terminal_bundle.js b/app/assets/javascripts/terminal/index.js index 134522ef961..1a75e072c4e 100644 --- a/app/assets/javascripts/terminal/terminal_bundle.js +++ b/app/assets/javascripts/terminal/index.js @@ -6,4 +6,4 @@ import './terminal'; window.Terminal = Terminal; -$(() => new gl.Terminal({ selector: '#terminal' })); +export default () => new gl.Terminal({ selector: '#terminal' }); diff --git a/app/assets/javascripts/test.js b/app/assets/javascripts/test.js deleted file mode 100644 index c4c7918a68f..00000000000 --- a/app/assets/javascripts/test.js +++ /dev/null @@ -1 +0,0 @@ -$.fx.off = true; diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index f7ba305a59f..4114ca6bf7c 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -17,7 +17,7 @@ module IssuableCollections set_pagination return if redirect_out_of_range(@total_pages) - if params[:label_name].present? + if params[:label_name].present? && @project labels_params = { project_id: @project.id, title: params[:label_name] } @labels = LabelsFinder.new(current_user, labels_params).execute end diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index f3a9e591c3e..ac1d97dc54a 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -14,7 +14,14 @@ class Groups::LabelsController < Groups::ApplicationController end format.json do - available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute + available_labels = LabelsFinder.new( + current_user, + group_id: @group.id, + only_group_labels: params[:only_group_labels], + include_ancestor_groups: params[:include_ancestor_groups], + include_descendant_groups: params[:include_descendant_groups] + ).execute + render json: LabelSerializer.new.represent_appearance(available_labels) end end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 1d910e461b1..7b7cb52d7ed 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -14,37 +14,31 @@ class Projects::CommitsController < Projects::ApplicationController @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) - # https://gitlab.com/gitlab-org/gitaly/issues/931 - Gitlab::GitalyClient.allow_n_plus_1_calls do - respond_to do |format| - format.html - format.atom { render layout: 'xml.atom' } + respond_to do |format| + format.html + format.atom { render layout: 'xml.atom' } - format.json do - pager_json( - 'projects/commits/_commits', - @commits.size, - project: @project, - ref: @ref) - end + format.json do + pager_json( + 'projects/commits/_commits', + @commits.size, + project: @project, + ref: @ref) end end end def signatures - # https://gitlab.com/gitlab-org/gitaly/issues/931 - Gitlab::GitalyClient.allow_n_plus_1_calls do - respond_to do |format| - format.json do - render json: { - signatures: @commits.select(&:has_signature?).map do |commit| - { - commit_sha: commit.sha, - html: view_to_html_string('projects/commit/_signature', signature: commit.signature) - } - end - } - end + respond_to do |format| + format.json do + render json: { + signatures: @commits.select(&:has_signature?).map do |commit| + { + commit_sha: commit.sha, + html: view_to_html_string('projects/commit/_signature', signature: commit.signature) + } + end + } end end end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 5c9fce211ec..780c0fdb03e 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -61,12 +61,20 @@ class LabelsFinder < UnionFinder def group_ids strong_memoize(:group_ids) do - group = Group.find(params[:group_id]) - groups = params[:include_ancestor_groups].present? ? group.self_and_ancestors : [group] - groups_user_can_read_labels(groups).map(&:id) + groups_user_can_read_labels(groups_to_include).map(&:id) end end + def groups_to_include + group = Group.find(params[:group_id]) + groups = [group] + + groups += group.ancestors if params[:include_ancestor_groups].present? + groups += group.descendants if params[:include_descendant_groups].present? + + groups + end + def group? params[:group_id].present? end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index a73c573736e..d498a2d6d11 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder .public_or_visible_to_user(current_user) end + # Returns a collection of projects that is either public or visible to the + # logged in user. + # + # A caller must pass in a block to modify individual parts of + # the query, e.g. to apply .with_feature_available_for_user on top of it. + # This is useful for performance as we can stick those additional filters + # at the bottom of e.g. the UNION. + def projects_for_user + return yield(Project.public_to_user) unless current_user + + # If the current_user is allowed to see all projects, + # we can shortcut and just return. + return yield(Project.all) if current_user.full_private_access? + + authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects)) + + levels = Gitlab::VisibilityLevel.levels_for_user(current_user) + visible_projects = yield(Project.where(visibility_level: levels)) + + # We use a UNION here instead of OR clauses since this results in better + # performance. + union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')]) + + Project.from("(#{union.to_sql}) AS #{Project.table_name}") + end + def feature_available_projects # Don't return any project related snippets if the user cannot read cross project return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) - projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part| + projects = projects_for_user do |part| part.with_feature_available_for_user(:snippets, current_user) end.select(:id) diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 193bb48e54d..58de3448577 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -15,7 +15,7 @@ module Clusters end def install_command - Gitlab::Kubernetes::Helm::InstallCommand.new(name, install_helm: true) + Gitlab::Kubernetes::Helm::InitCommand.new(name) end end end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 9f583342c19..27fc3b85465 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -5,6 +5,7 @@ module Clusters include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationStatus + include ::Clusters::Concerns::ApplicationData include AfterCommitQueue default_value_for :ingress_type, :nginx @@ -29,12 +30,12 @@ module Clusters 'stable/nginx-ingress' end - def chart_values_file - "#{Rails.root}/vendor/#{name}/values.yaml" - end - def install_command - Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) + Gitlab::Kubernetes::Helm::InstallCommand.new( + name, + chart: chart, + values: values + ) end def schedule_status_update diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index aa22e9d5d58..89ebd63e605 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -7,6 +7,7 @@ module Clusters include ::Clusters::Concerns::ApplicationCore include ::Clusters::Concerns::ApplicationStatus + include ::Clusters::Concerns::ApplicationData default_value_for :version, VERSION @@ -30,12 +31,12 @@ module Clusters 80 end - def chart_values_file - "#{Rails.root}/vendor/#{name}/values.yaml" - end - def install_command - Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) + Gitlab::Kubernetes::Helm::InstallCommand.new( + name, + chart: chart, + values: values + ) end def proxy_client diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb new file mode 100644 index 00000000000..7adf1663c35 --- /dev/null +++ b/app/models/clusters/applications/runner.rb @@ -0,0 +1,68 @@ +module Clusters + module Applications + class Runner < ActiveRecord::Base + VERSION = '0.1.13'.freeze + + self.table_name = 'clusters_applications_runners' + + include ::Clusters::Concerns::ApplicationCore + include ::Clusters::Concerns::ApplicationStatus + include ::Clusters::Concerns::ApplicationData + + belongs_to :runner, class_name: 'Ci::Runner', foreign_key: :runner_id + delegate :project, to: :cluster + + default_value_for :version, VERSION + + def chart + "#{name}/gitlab-runner" + end + + def repository + 'https://charts.gitlab.io' + end + + def values + content_values.to_yaml + end + + def install_command + Gitlab::Kubernetes::Helm::InstallCommand.new( + name, + chart: chart, + values: values, + repository: repository + ) + end + + private + + def ensure_runner + runner || create_and_assign_runner + end + + def create_and_assign_runner + transaction do + project.runners.create!(name: 'kubernetes-cluster', tag_list: %w(kubernetes cluster)).tap do |runner| + update!(runner_id: runner.id) + end + end + end + + def gitlab_url + Gitlab::Routing.url_helpers.root_url(only_path: false) + end + + def specification + { + "gitlabUrl" => gitlab_url, + "runnerToken" => ensure_runner.token + } + end + + def content_values + specification.merge(YAML.load_file(chart_values_file)) + end + end + end +end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 8678f70f78c..1c0046107d7 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -7,7 +7,8 @@ module Clusters APPLICATIONS = { Applications::Helm.application_name => Applications::Helm, Applications::Ingress.application_name => Applications::Ingress, - Applications::Prometheus.application_name => Applications::Prometheus + Applications::Prometheus.application_name => Applications::Prometheus, + Applications::Runner.application_name => Applications::Runner }.freeze belongs_to :user @@ -23,6 +24,7 @@ module Clusters has_one :application_helm, class_name: 'Clusters::Applications::Helm' has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus' + has_one :application_runner, class_name: 'Clusters::Applications::Runner' accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true @@ -68,7 +70,8 @@ module Clusters [ application_helm || build_application_helm, application_ingress || build_application_ingress, - application_prometheus || build_application_prometheus + application_prometheus || build_application_prometheus, + application_runner || build_application_runner ] end diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb new file mode 100644 index 00000000000..96ac757e99e --- /dev/null +++ b/app/models/clusters/concerns/application_data.rb @@ -0,0 +1,23 @@ +module Clusters + module Concerns + module ApplicationData + extend ActiveSupport::Concern + + included do + def repository + nil + end + + def values + File.read(chart_values_file) + end + + private + + def chart_values_file + "#{Rails.root}/vendor/#{name}/values.yaml" + end + end + end + end +end diff --git a/app/models/commit.rb b/app/models/commit.rb index add5fcf0e79..b9106309142 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -19,6 +19,7 @@ class Commit attr_accessor :project, :author attr_accessor :redacted_description_html attr_accessor :redacted_title_html + attr_reader :gpg_commit DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] @@ -110,6 +111,7 @@ class Commit @raw = raw_commit @project = project @statuses = {} + @gpg_commit = Gitlab::Gpg::Commit.new(self) if project end def id @@ -452,8 +454,4 @@ class Commit def merged_merge_request_no_cache(user) MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit? end - - def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.new(self) - end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 3469d5d795c..9fb5b7efec6 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base end def group_name - name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip + name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip end def failed_but_allowed? diff --git a/app/models/project.rb b/app/models/project.rb index ba278a49688..5b1f8b2658b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -317,42 +317,13 @@ class Project < ActiveRecord::Base # Returns a collection of projects that is either public or visible to the # logged in user. - # - # A caller may pass in a block to modify individual parts of - # the query, e.g. to apply .with_feature_available_for_user on top of it. - # This is useful for performance as we can stick those additional filters - # at the bottom of e.g. the UNION. - # - # Optionally, turning `use_where_in` off leads to returning a - # relation using #from instead of #where. This can perform much better - # but leads to trouble when used in conjunction with AR's #merge method. - def self.public_or_visible_to_user(user = nil, use_where_in: true, &block) - # If we don't get a block passed, use identity to avoid if/else repetitions - block = ->(part) { part } unless block_given? - - return block.call(public_to_user) unless user - - # If the user is allowed to see all projects, - # we can shortcut and just return. - return block.call(all) if user.full_private_access? - - authorized = user - .project_authorizations - .select(1) - .where('project_authorizations.project_id = projects.id') - authorized_projects = block.call(where('EXISTS (?)', authorized)) - - levels = Gitlab::VisibilityLevel.levels_for_user(user) - visible_projects = block.call(where(visibility_level: levels)) - - # We use a UNION here instead of OR clauses since this results in better - # performance. - union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')]) - - if use_where_in - where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + def self.public_or_visible_to_user(user = nil) + if user + where('EXISTS (?) OR projects.visibility_level IN (?)', + user.authorizations_for_projects, + Gitlab::VisibilityLevel.levels_for_user(user)) else - from("(#{union.to_sql}) AS #{table_name}") + public_to_user end end @@ -371,14 +342,11 @@ class Project < ActiveRecord::Base elsif user column = ProjectFeature.quoted_access_level_column(feature) - authorized = user.project_authorizations.select(1) - .where('project_authorizations.project_id = projects.id') - with_project_feature .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", visible, ProjectFeature::PRIVATE, - authorized) + user.authorizations_for_projects) else with_feature_access_level(feature, visible) end diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 818cfb01b14..e2ffdf72201 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -99,7 +99,7 @@ class ChatNotificationService < Service def get_message(object_kind, data) case object_kind when "push", "tag_push" - ChatMessage::PushMessage.new(data) + ChatMessage::PushMessage.new(data) if notify_for_ref?(data) when "issue" ChatMessage::IssueMessage.new(data) unless update?(data) when "merge_request" @@ -145,10 +145,16 @@ class ChatNotificationService < Service end def notify_for_ref?(data) - return true if data[:object_attributes][:tag] + return true if data.dig(:object_attributes, :tag) return true unless notify_only_default_branch? - data[:object_attributes][:ref] == project.default_branch + ref = if data[:ref] + Gitlab::Git.ref_name(data[:ref]) + else + data.dig(:object_attributes, :ref) + end + + ref == project.default_branch end def notify_for_pipeline?(data) diff --git a/app/models/user.rb b/app/models/user.rb index 9547506d33d..9c60adf0c90 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -601,6 +601,15 @@ class User < ActiveRecord::Base authorized_projects(min_access_level).exists?({ id: project.id }) end + # Typically used in conjunction with projects table to get projects + # a user has been given access to. + # + # Example use: + # `Project.where('EXISTS(?)', user.authorizations_for_projects)` + def authorizations_for_projects + project_authorizations.select(1).where('project_authorizations.project_id = projects.id') + end + # Returns the projects this user has reporter (or greater) access to, limited # to at most the given projects. # diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index b89b7a9ff85..68788134b8e 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -666,15 +666,15 @@ .checkbox = f.label :usage_ping_enabled do = f.check_box :usage_ping_enabled, disabled: !can_be_configured - Usage ping enabled - = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") + Enable usage ping .help-block - if can_be_configured - Every week GitLab will report license usage back to GitLab, Inc. - Disable this option if you do not want this to occur. To see the - JSON payload that will be sent, visit the - = succeed '.' do - = link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping') + To help improve GitLab and its user experience, GitLab will + periodically collect usage information. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") + about what information is shared with GitLab Inc. Visit + = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping') + to see the JSON payload sent. - else The usage ping is disabled, and cannot be configured through this form. For more information, see the documentation on diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 0c979109b3f..b981b5fdafa 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -42,7 +42,6 @@ = webpack_bundle_tag "common" = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled - = webpack_bundle_tag "test" if Rails.env.test? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index 329bf16895f..1bd10018b40 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -2,10 +2,6 @@ - add_to_breadcrumbs("Two-Factor Authentication", profile_account_path) - @content_class = "limit-container-width" unless fluid_layout - -- content_for :page_specific_javascripts do - = webpack_bundle_tag('two_factor_auth') - .js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path } .row.prepend-top-default .col-lg-4 diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 179c45a9867..2ee0eafcf1a 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -10,6 +10,7 @@ install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm), install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress), install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus), + install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner), toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 7be4ef39117..6ec4ff56552 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -3,7 +3,6 @@ - content_for :page_specific_javascripts do = stylesheet_link_tag "xterm/xterm" - = webpack_bundle_tag("terminal") %div{ class: container_class } .top-area diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index f2e35ef6e0c..9866cc716ee 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -5,11 +5,6 @@ - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes -- content_for :page_specific_javascripts do - = webpack_bundle_tag('common_vue') - - - if has_vue_discussions_cookie? - = webpack_bundle_tag('mr_notes') .merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } } = render "projects/merge_requests/mr_title" diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml index 127a338e413..2b0a502fe4d 100644 --- a/app/views/projects/protected_branches/_index.html.haml +++ b/app/views/projects/protected_branches/_index.html.haml @@ -1,6 +1,3 @@ -- content_for :page_specific_javascripts do - = webpack_bundle_tag('protected_branches') - - content_for :create_protected_branch do = render 'projects/protected_branches/create_protected_branch' diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 744b88760bc..27e1f9fba3e 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -15,7 +15,6 @@ #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } } = webpack_bundle_tag('common_vue') - = webpack_bundle_tag('registry_list') .row.prepend-top-10 .col-lg-12 diff --git a/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml b/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml new file mode 100644 index 00000000000..74675992105 --- /dev/null +++ b/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml @@ -0,0 +1,5 @@ +--- +title: Allow installation of GitLab Runner with a single click +merge_request: 17134 +author: +type: added diff --git a/changelogs/unreleased/33570-slack-notify-default-branch.yml b/changelogs/unreleased/33570-slack-notify-default-branch.yml new file mode 100644 index 00000000000..5c90ce47729 --- /dev/null +++ b/changelogs/unreleased/33570-slack-notify-default-branch.yml @@ -0,0 +1,5 @@ +--- +title: Fix Slack/Mattermost notifications not respecting `notify_only_default_branch` setting for pushes +merge_request: 17345 +author: +type: fixed diff --git a/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml b/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml new file mode 100644 index 00000000000..c9e23360e3b --- /dev/null +++ b/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml @@ -0,0 +1,5 @@ +--- +title: expose more metrics in merge requests api +merge_request: 16589 +author: haseebeqx +type: added diff --git a/changelogs/unreleased/revert-project-visibility-changes.yml b/changelogs/unreleased/revert-project-visibility-changes.yml new file mode 100644 index 00000000000..df44fdb79b1 --- /dev/null +++ b/changelogs/unreleased/revert-project-visibility-changes.yml @@ -0,0 +1,5 @@ +--- +title: Revert Project.public_or_visible_to_user changes and only apply to snippets +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/zj-version-string-grouping-ci.yml b/changelogs/unreleased/zj-version-string-grouping-ci.yml new file mode 100644 index 00000000000..04ef0f65b1e --- /dev/null +++ b/changelogs/unreleased/zj-version-string-grouping-ci.yml @@ -0,0 +1,5 @@ +--- +title: Allow CI/CD Jobs being grouped on version strings +merge_request: +author: +type: added diff --git a/config/webpack.config.js b/config/webpack.config.js index fd0bfb59e65..cc098c8a3f9 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -43,20 +43,12 @@ function generateEntries() { autoEntriesCount = Object.keys(autoEntries).length; const manualEntries = { - monitoring: './monitoring/monitoring_bundle.js', - mr_notes: './mr_notes/index.js', - protected_branches: './protected_branches', - registry_list: './registry/index.js', - terminal: './terminal/terminal_bundle.js', - two_factor_auth: './two_factor_auth.js', - common: './commons/index.js', common_vue: './vue_shared/vue_resource_interceptor.js', locale: './locale/index.js', main: './main.js', ide: './ide/index.js', raven: './raven/index.js', - test: './test.js', webpack_runtime: './webpack.js', }; diff --git a/db/migrate/20180214155405_create_clusters_applications_runners.rb b/db/migrate/20180214155405_create_clusters_applications_runners.rb new file mode 100644 index 00000000000..fc4c0881338 --- /dev/null +++ b/db/migrate/20180214155405_create_clusters_applications_runners.rb @@ -0,0 +1,32 @@ +class CreateClustersApplicationsRunners < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :clusters_applications_runners do |t| + t.references :cluster, null: false, foreign_key: { on_delete: :cascade } + t.references :runner, references: :ci_runners + t.index :runner_id + t.index :cluster_id, unique: true + t.integer :status, null: false + t.timestamps_with_timezone null: false + t.string :version, null: false + t.text :status_reason + end + + add_concurrent_foreign_key :clusters_applications_runners, :ci_runners, + column: :runner_id, + on_delete: :nullify + end + + def down + if foreign_keys_for(:clusters_applications_runners, :runner_id).any? + remove_foreign_key :clusters_applications_runners, column: :runner_id + end + + drop_table :clusters_applications_runners + end +end diff --git a/db/schema.rb b/db/schema.rb index 773cf8b4d3f..db8bafe9677 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -582,6 +582,19 @@ ActiveRecord::Schema.define(version: 20180301084653) do t.datetime_with_timezone "updated_at", null: false end + create_table "clusters_applications_runners", force: :cascade do |t| + t.integer "cluster_id", null: false + t.integer "runner_id" + t.integer "status", null: false + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.string "version", null: false + t.text "status_reason" + end + + add_index "clusters_applications_runners", ["cluster_id"], name: "index_clusters_applications_runners_on_cluster_id", unique: true, using: :btree + add_index "clusters_applications_runners", ["runner_id"], name: "index_clusters_applications_runners_on_runner_id", using: :btree + create_table "container_repositories", force: :cascade do |t| t.integer "project_id", null: false t.string "name", null: false @@ -1988,6 +2001,8 @@ ActiveRecord::Schema.define(version: 20180301084653) do add_foreign_key "cluster_providers_gcp", "clusters", on_delete: :cascade add_foreign_key "clusters", "users", on_delete: :nullify add_foreign_key "clusters_applications_helm", "clusters", on_delete: :cascade + add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify + add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade add_foreign_key "container_repositories", "projects" add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md new file mode 100644 index 00000000000..6c5a466ced5 --- /dev/null +++ b/doc/administration/incoming_email.md @@ -0,0 +1,331 @@ +# Incoming email + +GitLab has several features based on receiving incoming emails: + +- [Reply by Email](reply_by_email.md): allow GitLab users to comment on issues + and merge requests by replying to notification emails. +- [New issue by email](../user/project/issues/create_new_issue.md#new-issue-via-email): + allow GitLab users to create a new issue by sending an email to a + user-specific email address. +- [New merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email): + allow GitLab users to create a new merge request by sending an email to a + user-specific email address. + +## Requirements + +Handling incoming emails requires an [IMAP]-enabled email account. GitLab +requires one of the following three strategies: + +- Email sub-addressing +- Dedicated email address +- Catch-all mailbox + +Let's walk through each of these options. + +**If your provider or server supports email sub-addressing, we recommend using it. +Most features (other than reply by email) only work with sub-addressing.** + +[IMAP]: https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol + +### Email sub-addressing + +[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is +a feature where any email to `user+some_arbitrary_tag@example.com` will end up +in the mailbox for `user@example.com`, and is supported by providers such as +Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the +[Postfix mail server] which you can run on-premises. + +[Postfix mail server]: reply_by_email_postfix_setup.md + +### Dedicated email address + +This solution is really simple to set up: you just have to create an email +address dedicated to receive your users' replies to GitLab notifications. + +### Catch-all mailbox + +A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will +"catch all" the emails addressed to the domain that do not exist in the mail +server. + +GitLab can be set up to allow users to comment on issues and merge requests by +replying to notification emails. + +## Set it up + +If you want to use Gmail / Google Apps for incoming emails, make sure you have +[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) +and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255) +or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839) +and use [an application password](https://support.google.com/mail/answer/185833). + +To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the +[Postfix setup documentation](reply_by_email_postfix_setup.md). + +### Security Concerns + +**WARNING:** Be careful when choosing the domain used for receiving incoming +email. + +For the sake of example, suppose your top-level company domain is `hooli.com`. +All employees in your company have an email address at that domain via Google +Apps, and your company's private Slack instance requires a valid `@hooli.com` +email address in order to sign up. + +If you also host a public-facing GitLab instance at `hooli.com` and set your +incoming email domain to `hooli.com`, an attacker could abuse the "Create new +issue by email" or +"[Create new merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email)" +features by using a project's unique address as the email when signing up for +Slack, which would send a confirmation email, which would create a new issue or +merge request on the project owned by the attacker, allowing them to click the +confirmation link and validate their account on your company's private Slack +instance. + +We recommend receiving incoming email on a subdomain, such as +`incoming.hooli.com`, and ensuring that you do not employ any services that +authenticate solely based on access to an email domain such as `*.hooli.com.` +Alternatively, use a dedicated domain for GitLab email communications such as +`hooli-gitlab.com`. + +See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366) +for a real-world example of this exploit. + +### Omnibus package installations + +1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the + feature and fill in the details for your specific IMAP server and email account: + + Configuration for Postfix mail server, assumes mailbox + incoming@gitlab.example.com + + ```ruby + gitlab_rails['incoming_email_enabled'] = true + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). + gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + gitlab_rails['incoming_email_email'] = "incoming" + # Email account password + gitlab_rails['incoming_email_password'] = "[REDACTED]" + + # IMAP server host + gitlab_rails['incoming_email_host'] = "gitlab.example.com" + # IMAP server port + gitlab_rails['incoming_email_port'] = 143 + # Whether the IMAP server uses SSL + gitlab_rails['incoming_email_ssl'] = false + # Whether the IMAP server uses StartTLS + gitlab_rails['incoming_email_start_tls'] = false + + # The mailbox where incoming mail will end up. Usually "inbox". + gitlab_rails['incoming_email_mailbox_name'] = "inbox" + # The IDLE command timeout. + gitlab_rails['incoming_email_idle_timeout'] = 60 + ``` + + Configuration for Gmail / Google Apps, assumes mailbox + gitlab-incoming@gmail.com + + ```ruby + gitlab_rails['incoming_email_enabled'] = true + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). + gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" + # Email account password + gitlab_rails['incoming_email_password'] = "[REDACTED]" + + # IMAP server host + gitlab_rails['incoming_email_host'] = "imap.gmail.com" + # IMAP server port + gitlab_rails['incoming_email_port'] = 993 + # Whether the IMAP server uses SSL + gitlab_rails['incoming_email_ssl'] = true + # Whether the IMAP server uses StartTLS + gitlab_rails['incoming_email_start_tls'] = false + + # The mailbox where incoming mail will end up. Usually "inbox". + gitlab_rails['incoming_email_mailbox_name'] = "inbox" + # The IDLE command timeout. + gitlab_rails['incoming_email_idle_timeout'] = 60 + ``` + + Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes + mailbox incoming@exchange.example.com + + ```ruby + gitlab_rails['incoming_email_enabled'] = true + + # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here + gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com" + + # Email account username + # Typically this is the userPrincipalName (UPN) + gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com" + # Email account password + gitlab_rails['incoming_email_password'] = "[REDACTED]" + + # IMAP server host + gitlab_rails['incoming_email_host'] = "exchange.example.com" + # IMAP server port + gitlab_rails['incoming_email_port'] = 993 + # Whether the IMAP server uses SSL + gitlab_rails['incoming_email_ssl'] = true + ``` + +1. Reconfigure GitLab for the changes to take effect: + + ```sh + sudo gitlab-ctl reconfigure + ``` + +1. Verify that everything is configured correctly: + + ```sh + sudo gitlab-rake gitlab:incoming_email:check + ``` + +1. Reply by email should now be working. + +### Installations from source + +1. Go to the GitLab installation directory: + + ```sh + cd /home/git/gitlab + ``` + +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature + and fill in the details for your specific IMAP server and email account: + + ```sh + sudo editor config/gitlab.yml + ``` + + Configuration for Postfix mail server, assumes mailbox + incoming@gitlab.example.com + + ```yaml + incoming_email: + enabled: true + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). + address: "incoming+%{key}@gitlab.example.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + user: "incoming" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "gitlab.example.com" + # IMAP server port + port: 143 + # Whether the IMAP server uses SSL + ssl: false + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" + # The IDLE command timeout. + idle_timeout: 60 + ``` + + Configuration for Gmail / Google Apps, assumes mailbox + gitlab-incoming@gmail.com + + ```yaml + incoming_email: + enabled: true + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). + address: "gitlab-incoming+%{key}@gmail.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + user: "gitlab-incoming@gmail.com" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "imap.gmail.com" + # IMAP server port + port: 993 + # Whether the IMAP server uses SSL + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" + # The IDLE command timeout. + idle_timeout: 60 + ``` + + Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes + mailbox incoming@exchange.example.com + + ```yaml + incoming_email: + enabled: true + + # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here + address: "incoming@exchange.example.com" + + # Email account username + # Typically this is the userPrincipalName (UPN) + user: "incoming@ad-domain.example.com" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "exchange.example.com" + # IMAP server port + port: 993 + # Whether the IMAP server uses SSL + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" + # The IDLE command timeout. + idle_timeout: 60 + ``` + +1. Enable `mail_room` in the init script at `/etc/default/gitlab`: + + ```sh + sudo mkdir -p /etc/default + echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab + ``` + +1. Restart GitLab: + + ```sh + sudo service gitlab restart + ``` + +1. Verify that everything is configured correctly: + + ```sh + sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production + ``` + +1. Reply by email should now be working. diff --git a/doc/administration/index.md b/doc/administration/index.md index 51444651bdb..69efaf75140 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -79,11 +79,19 @@ created in snippets, wikis, and repos. - [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains. - [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS). - [Authentication/Authorization](../topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers. -- [Reply by email](reply_by_email.md): Allow users to comment on issues and merge requests by replying to notification emails. - - [Postfix for Reply by email](reply_by_email_postfix_setup.md): Set up a basic Postfix mail +- [Incoming email](incoming_email.md): Configure incoming emails to allow + users to [reply by email], create [issues by email] and + [merge requests by email], and to enable [Service Desk]. + - [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a + basic Postfix mail server with IMAP authentication on Ubuntu for incoming + emails. server with IMAP authentication on Ubuntu, to be used with Reply by email. - [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time. +[reply by email]: reply_by_email.md +[issues by email]: ../user/project/issues/create_new_issue.md#new-issue-via-email +[merge requests by email]: ../user/project/merge_requests/index.md#create-new-merge-requests-by-email + ## Project settings - [Container Registry](container_registry.md): Configure Container Registry with GitLab. diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 1b42d7979ed..00a2f3d01b8 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -23,7 +23,7 @@ requests from the API are logged to a separate file in `api_json.log`. Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, etc. For example: ```json -{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":{"namespace_id":"gitlab","project_id":"gitlab-ce","id":"1234"},"remote_ip":"18.245.0.1","user_id":1,"username":"admin"} +{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":[{"key":"param_key","value":"param_value"}],"remote_ip":"18.245.0.1","user_id":1,"username":"admin","gitaly_calls":76} ``` In this example, you can see this was a GET request for a specific issue. Notice each line also contains performance data: @@ -31,6 +31,7 @@ In this example, you can see this was a GET request for a specific issue. Notice 1. `duration`: the total time taken to retrieve the request 2. `view`: total time taken inside the Rails views 3. `db`: total time to retrieve data from the database +4. `gitaly_calls`: total number of calls made to Gitaly User clone/fetch activity using http transport appears in this log as `action: git_upload_pack`. diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md index 3a2cced37bf..426245c7aca 100644 --- a/doc/administration/reply_by_email.md +++ b/doc/administration/reply_by_email.md @@ -5,33 +5,7 @@ replying to notification emails. ## Requirement -Reply by email requires an IMAP-enabled email account. GitLab allows you to use -three strategies for this feature: -- using email sub-addressing -- using a dedicated email address -- using a catch-all mailbox - -### Email sub-addressing - -**If your provider or server supports email sub-addressing, we recommend using it. -Some features (e.g. create new issue via email) only work with sub-addressing.** - -[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is -a feature where any email to `user+some_arbitrary_tag@example.com` will end up -in the mailbox for `user@example.com`, and is supported by providers such as -Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix -mail server which you can run on-premises. - -### Dedicated email address - -This solution is really simple to set up: you just have to create an email -address dedicated to receive your users' replies to GitLab notifications. - -### Catch-all mailbox - -A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will -"catch all" the emails addressed to the domain that do not exist in the mail -server. +Make sure [incoming email](incoming_email.md) is setup. ## How it works? @@ -65,329 +39,3 @@ the entity the notification was about (issue, merge request, commit...). For more details about the `Message-ID`, `In-Reply-To`, and `References headers`, please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4). - -## Set it up - -If you want to use Gmail / Google Apps with Reply by email, make sure you have -[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) -and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255) -or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839) -and use [an application password](https://support.google.com/mail/answer/185833). - -To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the -[Postfix setup documentation](reply_by_email_postfix_setup.md). - -### Security Concerns - -**WARNING:** Be careful when choosing the domain used for receiving incoming -email. - -For the sake of example, suppose your top-level company domain is `hooli.com`. -All employees in your company have an email address at that domain via Google -Apps, and your company's private Slack instance requires a valid `@hooli.com` -email address in order to sign up. - -If you also host a public-facing GitLab instance at `hooli.com` and set your -incoming email domain to `hooli.com`, an attacker could abuse the "Create new -issue by email" or -"[Create new merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email)" -features by using a project's unique address as the email when signing up for -Slack, which would send a confirmation email, which would create a new issue or -merge request on the project owned by the attacker, allowing them to click the -confirmation link and validate their account on your company's private Slack -instance. - -We recommend receiving incoming email on a subdomain, such as -`incoming.hooli.com`, and ensuring that you do not employ any services that -authenticate solely based on access to an email domain such as `*.hooli.com.` -Alternatively, use a dedicated domain for GitLab email communications such as -`hooli-gitlab.com`. - -See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366) -for a real-world example of this exploit. - -### Omnibus package installations - -1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the - feature and fill in the details for your specific IMAP server and email account: - - ```ruby - # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com - gitlab_rails['incoming_email_enabled'] = true - - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" - - # Email account username - # With third party providers, this is usually the full email address. - # With self-hosted email servers, this is usually the user part of the email address. - gitlab_rails['incoming_email_email'] = "incoming" - # Email account password - gitlab_rails['incoming_email_password'] = "[REDACTED]" - - # IMAP server host - gitlab_rails['incoming_email_host'] = "gitlab.example.com" - # IMAP server port - gitlab_rails['incoming_email_port'] = 143 - # Whether the IMAP server uses SSL - gitlab_rails['incoming_email_ssl'] = false - # Whether the IMAP server uses StartTLS - gitlab_rails['incoming_email_start_tls'] = false - - # The mailbox where incoming mail will end up. Usually "inbox". - gitlab_rails['incoming_email_mailbox_name'] = "inbox" - # The IDLE command timeout. - gitlab_rails['incoming_email_idle_timeout'] = 60 - ``` - - ```ruby - # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com - gitlab_rails['incoming_email_enabled'] = true - - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" - - # Email account username - # With third party providers, this is usually the full email address. - # With self-hosted email servers, this is usually the user part of the email address. - gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" - # Email account password - gitlab_rails['incoming_email_password'] = "[REDACTED]" - - # IMAP server host - gitlab_rails['incoming_email_host'] = "imap.gmail.com" - # IMAP server port - gitlab_rails['incoming_email_port'] = 993 - # Whether the IMAP server uses SSL - gitlab_rails['incoming_email_ssl'] = true - # Whether the IMAP server uses StartTLS - gitlab_rails['incoming_email_start_tls'] = false - - # The mailbox where incoming mail will end up. Usually "inbox". - gitlab_rails['incoming_email_mailbox_name'] = "inbox" - # The IDLE command timeout. - gitlab_rails['incoming_email_idle_timeout'] = 60 - ``` - - ```ruby - # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com - gitlab_rails['incoming_email_enabled'] = true - - # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here - gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com" - - # Email account username - # Typically this is the userPrincipalName (UPN) - gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com" - # Email account password - gitlab_rails['incoming_email_password'] = "[REDACTED]" - - # IMAP server host - gitlab_rails['incoming_email_host'] = "exchange.example.com" - # IMAP server port - gitlab_rails['incoming_email_port'] = 993 - # Whether the IMAP server uses SSL - gitlab_rails['incoming_email_ssl'] = true - ``` - -1. Reconfigure GitLab for the changes to take effect: - - ```sh - sudo gitlab-ctl reconfigure - ``` - -1. Verify that everything is configured correctly: - - ```sh - sudo gitlab-rake gitlab:incoming_email:check - ``` - -1. Reply by email should now be working. - -### Installations from source - -1. Go to the GitLab installation directory: - - ```sh - cd /home/git/gitlab - ``` - -1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature - and fill in the details for your specific IMAP server and email account: - - ```sh - sudo editor config/gitlab.yml - ``` - - ```yaml - # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com - incoming_email: - enabled: true - - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - address: "incoming+%{key}@gitlab.example.com" - - # Email account username - # With third party providers, this is usually the full email address. - # With self-hosted email servers, this is usually the user part of the email address. - user: "incoming" - # Email account password - password: "[REDACTED]" - - # IMAP server host - host: "gitlab.example.com" - # IMAP server port - port: 143 - # Whether the IMAP server uses SSL - ssl: false - # Whether the IMAP server uses StartTLS - start_tls: false - - # The mailbox where incoming mail will end up. Usually "inbox". - mailbox: "inbox" - # The IDLE command timeout. - idle_timeout: 60 - ``` - - ```yaml - # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com - incoming_email: - enabled: true - - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - address: "gitlab-incoming+%{key}@gmail.com" - - # Email account username - # With third party providers, this is usually the full email address. - # With self-hosted email servers, this is usually the user part of the email address. - user: "gitlab-incoming@gmail.com" - # Email account password - password: "[REDACTED]" - - # IMAP server host - host: "imap.gmail.com" - # IMAP server port - port: 993 - # Whether the IMAP server uses SSL - ssl: true - # Whether the IMAP server uses StartTLS - start_tls: false - - # The mailbox where incoming mail will end up. Usually "inbox". - mailbox: "inbox" - # The IDLE command timeout. - idle_timeout: 60 - ``` - - ```yaml - # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com - incoming_email: - enabled: true - - # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here - address: "incoming@exchange.example.com" - - # Email account username - # Typically this is the userPrincipalName (UPN) - user: "incoming@ad-domain.example.com" - # Email account password - password: "[REDACTED]" - - # IMAP server host - host: "exchange.example.com" - # IMAP server port - port: 993 - # Whether the IMAP server uses SSL - ssl: true - # Whether the IMAP server uses StartTLS - start_tls: false - - # The mailbox where incoming mail will end up. Usually "inbox". - mailbox: "inbox" - # The IDLE command timeout. - idle_timeout: 60 - ``` - -1. Enable `mail_room` in the init script at `/etc/default/gitlab`: - - ```sh - sudo mkdir -p /etc/default - echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab - ``` - -1. Restart GitLab: - - ```sh - sudo service gitlab restart - ``` - -1. Verify that everything is configured correctly: - - ```sh - sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production - ``` - -1. Reply by email should now be working. - -### Development - -1. Go to the GitLab installation directory. - -1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account: - - ```yaml - # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com - incoming_email: - enabled: true - - # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. - # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). - address: "gitlab-incoming+%{key}@gmail.com" - - # Email account username - # With third party providers, this is usually the full email address. - # With self-hosted email servers, this is usually the user part of the email address. - user: "gitlab-incoming@gmail.com" - # Email account password - password: "[REDACTED]" - - # IMAP server host - host: "imap.gmail.com" - # IMAP server port - port: 993 - # Whether the IMAP server uses SSL - ssl: true - # Whether the IMAP server uses StartTLS - start_tls: false - - # The mailbox where incoming mail will end up. Usually "inbox". - mailbox: "inbox" - # The IDLE command timeout. - idle_timeout: 60 - ``` - - As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`. - -1. Uncomment the `mail_room` line in your `Procfile`: - - ```yaml - mail_room: bundle exec mail_room -q -c config/mail_room.yml - ``` - -1. Restart GitLab: - - ```sh - bundle exec foreman start - ``` - -1. Verify that everything is configured correctly: - - ```sh - bundle exec rake gitlab:incoming_email:check RAILS_ENV=development - ``` - -1. Reply by email should now be working. diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md index a1bb3851951..3e8b78e56d5 100644 --- a/doc/administration/reply_by_email_postfix_setup.md +++ b/doc/administration/reply_by_email_postfix_setup.md @@ -1,7 +1,7 @@ -# Set up Postfix for Reply by email +# Set up Postfix for incoming email This document will take you through the steps of setting up a basic Postfix mail -server with IMAP authentication on Ubuntu, to be used with [Reply by email]. +server with IMAP authentication on Ubuntu, to be used with [incoming email]. The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets. @@ -177,12 +177,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo ```sh sudo apt-get install courier-imap ``` - + And start `imapd`: ```sh imapd start ``` - + 1. The courier-authdaemon isn't started after installation. Without it, imap authentication will fail: ```sh sudo service courier-authdaemon start @@ -329,10 +329,10 @@ Courier, which we will install later to add IMAP authentication, requires mailbo ## Done! -If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./reply_by_email.md) guide to configure GitLab. +If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [incoming email] guide to configure GitLab. --- _This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._ -[reply by email]: reply_by_email.md +[incoming email]: incoming_email.md diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 6ce021cb4bf..cb9b0618767 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -261,20 +261,20 @@ Parameters: "upvotes": 0, "downvotes": 0, "author": { - "id": 1, - "username": "admin", - "email": "admin@example.com", - "name": "Administrator", - "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "state" : "active", + "web_url" : "https://gitlab.example.com/root", + "avatar_url" : null, + "username" : "root", + "id" : 1, + "name" : "Administrator" }, "assignee": { - "id": 1, - "username": "admin", - "email": "admin@example.com", - "name": "Administrator", - "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "state" : "active", + "web_url" : "https://gitlab.example.com/root", + "avatar_url" : null, + "username" : "root", + "id" : 1, + "name" : "Administrator" }, "source_project_id": 2, "target_project_id": 3, @@ -308,6 +308,26 @@ Parameters: "total_time_spent": 0, "human_time_estimate": null, "human_total_time_spent": null + }, + "closed_at": "2018-01-19T14:36:11.086Z", + "latest_build_started_at": null, + "latest_build_finished_at": null, + "first_deployed_to_production_at": null, + "pipeline": { + "id": 8, + "ref": "master", + "sha": "2dc6aa325a317eda67812f05600bdf0fcdc70ab0", + "status": "created" + }, + "merged_by": null, + "merged_at": null, + "closed_by": { + "state" : "active", + "web_url" : "https://gitlab.example.com/root", + "avatar_url" : null, + "username" : "root", + "id" : 1, + "name" : "Administrator" } } ``` diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index ac4a9b0ed27..856d7f264e4 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -121,8 +121,9 @@ The basic requirements is that there are two numbers separated with one of the following (you can even use them interchangeably): - a space -- a backslash (`/`) +- a forward slash (`/`) - a colon (`:`) +- a dot (`.`) >**Note:** More specifically, [it uses][regexp] this regular expression: `\d+[\s:\/\\]+\d+\s*`. diff --git a/doc/development/emails.md b/doc/development/emails.md index 18f47f44cb5..677029b1295 100644 --- a/doc/development/emails.md +++ b/doc/development/emails.md @@ -18,6 +18,68 @@ See the [Rails guides] for more info. [previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews [Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails +## Incoming email + +1. Go to the GitLab installation directory. + +1. Find the `incoming_email` section in `config/gitlab.yml`, enable the + feature and fill in the details for your specific IMAP server and email + account: + + Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com + + ```yaml + incoming_email: + enabled: true + + # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. + # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`). + address: "gitlab-incoming+%{key}@gmail.com" + + # Email account username + # With third party providers, this is usually the full email address. + # With self-hosted email servers, this is usually the user part of the email address. + user: "gitlab-incoming@gmail.com" + # Email account password + password: "[REDACTED]" + + # IMAP server host + host: "imap.gmail.com" + # IMAP server port + port: 993 + # Whether the IMAP server uses SSL + ssl: true + # Whether the IMAP server uses StartTLS + start_tls: false + + # The mailbox where incoming mail will end up. Usually "inbox". + mailbox: "inbox" + # The IDLE command timeout. + idle_timeout: 60 + ``` + + As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`. + +1. Uncomment the `mail_room` line in your `Procfile`: + + ```yaml + mail_room: bundle exec mail_room -q -c config/mail_room.yml + ``` + +1. Restart GitLab: + + ```sh + bundle exec foreman start + ``` + +1. Verify that everything is configured correctly: + + ```sh + bundle exec rake gitlab:incoming_email:check RAILS_ENV=development + ``` + +1. Reply by email should now be working. + --- [Return to Development documentation](README.md) diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index bbe25c2d911..4ac54f96aa2 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -120,6 +120,7 @@ added directly to your configured cluster. Those applications are needed for | [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | | [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. | | [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications | +| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. | ## Getting the external IP address diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md index 9af088374a1..1688edc1ee2 100644 --- a/doc/user/project/issues/create_new_issue.md +++ b/doc/user/project/issues/create_new_issue.md @@ -36,3 +36,25 @@ From an Issue Board, create a new issue by clicking on the plus sign (**+**) on It opens a new issue for that project labeled after its respective list.  + +## New issue via email + +*This feature needs [incoming email](../../../administration/incoming_email.md) +to be configured by a GitLab administrator to be available for CE/EE users, and +it's available on GitLab.com.* + +At the bottom of a project's issue page, click +**Email a new issue to this project**, and you will find an email address +which belongs to you. You could add this address to your contact. + +This is a private email address, generated just for you. +**Keep it to yourself** as anyone who gets ahold of it can create issues or +merge requests as if they were you. You can add this address to your contact +list for easy access. + +Sending an email to this address will create a new issue on your behalf for +this project, where the email subject becomes the issue title, and the email +body becomes the issue description. [Markdown] and [quick actions] are +supported. + + diff --git a/doc/user/project/issues/img/new_issue_from_email.png b/doc/user/project/issues/img/new_issue_from_email.png Binary files differnew file mode 100644 index 00000000000..775ea0cdffb --- /dev/null +++ b/doc/user/project/issues/img/new_issue_from_email.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 16027744164..d3220598933 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -134,6 +134,10 @@ those conflicts in the GitLab UI. ## Create new merge requests by email +*This feature needs [incoming email](../../../administration/incoming_email.md) +to be configured by a GitLab administrator to be available for CE/EE users, and +it's available on GitLab.com.* + You can create a new merge request by sending an email to a user-specific email address. The address can be obtained on the merge requests page by clicking on a **Email a new merge request to this project** button. The subject will be diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c88fcf9472e..0c8ec7dd5f5 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -481,6 +481,10 @@ module API expose :id end + class PipelineBasic < Grape::Entity + expose :id, :sha, :ref, :status + end + class MergeRequestSimple < ProjectEntity expose :title expose :web_url do |merge_request, options| @@ -546,6 +550,42 @@ module API expose :changes_count do |merge_request, _options| merge_request.merge_request_diff.real_size end + + expose :merged_by, using: Entities::UserBasic do |merge_request, _options| + merge_request.metrics&.merged_by + end + + expose :merged_at do |merge_request, _options| + merge_request.metrics&.merged_at + end + + expose :closed_by, using: Entities::UserBasic do |merge_request, _options| + merge_request.metrics&.latest_closed_by + end + + expose :closed_at do |merge_request, _options| + merge_request.metrics&.latest_closed_at + end + + expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + merge_request.metrics&.latest_build_started_at + end + + expose :latest_build_finished_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + merge_request.metrics&.latest_build_finished_at + end + + expose :first_deployed_to_production_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + merge_request.metrics&.first_deployed_to_production_at + end + + expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options| + merge_request.metrics&.pipeline + end + + def build_available?(options) + options[:project]&.feature_available?(:builds, options[:current_user]) + end end class MergeRequestChanges < MergeRequest @@ -909,10 +949,6 @@ module API expose :filename, :size end - class PipelineBasic < Grape::Entity - expose :id, :sha, :ref, :status - end - class JobBasic < Grape::Entity expose :id, :status, :stage, :name, :ref, :tag, :coverage expose :created_at, :started_at, :finished_at diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4ffd4895c7e..16d0f005f21 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -222,7 +222,7 @@ module API get ':id/merge_requests/:merge_request_iid/changes' do merge_request = find_merge_request_with_access(params[:merge_request_iid]) - present merge_request, with: Entities::MergeRequestChanges, current_user: current_user + present merge_request, with: Entities::MergeRequestChanges, current_user: current_user, project: user_project end desc 'Get the merge request pipelines' do diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index ae27a138b7c..594b6a9cbc5 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -250,6 +250,45 @@ module Gitlab end end + def extract_signature_lazily(repository, commit_id) + BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader| + items_by_repo = items.group_by { |i| i[:repository] } + + items_by_repo.each do |repo, items| + commit_ids = items.map { |i| i[:commit_id] } + + signatures = batch_signature_extraction(repository, commit_ids) + + signatures.each do |commit_sha, signature_data| + loader.call({ repository: repository, commit_id: commit_sha }, signature_data) + end + end + end + end + + def batch_signature_extraction(repository, commit_ids) + repository.gitaly_migrate(:extract_commit_signature_in_batch) do |is_enabled| + if is_enabled + gitaly_batch_signature_extraction(repository, commit_ids) + else + rugged_batch_signature_extraction(repository, commit_ids) + end + end + end + + def gitaly_batch_signature_extraction(repository, commit_ids) + repository.gitaly_commit_client.get_commit_signatures(commit_ids) + end + + def rugged_batch_signature_extraction(repository, commit_ids) + commit_ids.each_with_object({}) do |commit_id, signatures| + signature_data = rugged_extract_signature(repository, commit_id) + next unless signature_data + + signatures[commit_id] = signature_data + end + end + def rugged_extract_signature(repository, commit_id) begin Rugged::Commit.extract_signature(repository.rugged, commit_id) diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index d60f57717b5..1ad0bf1d060 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -319,6 +319,23 @@ module Gitlab [signature, signed_text] end + def get_commit_signatures(commit_ids) + request = Gitaly::GetCommitSignaturesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids) + response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request) + + signatures = Hash.new { |h, k| h[k] = [''.b, ''.b] } + current_commit_id = nil + + response.each do |message| + current_commit_id = message.commit_id if message.commit_id.present? + + signatures[current_commit_id].first << message.signature + signatures[current_commit_id].last << message.signed_text + end + + signatures + end + private def call_commit_diff(request_params, options = {}) diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb index 90dd569aaf8..6d2278d0876 100644 --- a/lib/gitlab/gpg/commit.rb +++ b/lib/gitlab/gpg/commit.rb @@ -1,15 +1,29 @@ module Gitlab module Gpg class Commit + include Gitlab::Utils::StrongMemoize + def initialize(commit) @commit = commit repo = commit.project.repository.raw_repository - @signature_text, @signed_text = Gitlab::Git::Commit.extract_signature(repo, commit.sha) + @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id) + end + + def signature_text + strong_memoize(:signature_text) do + @signature_data&.itself && @signature_data[0] + end + end + + def signed_text + strong_memoize(:signed_text) do + @signature_data&.itself && @signature_data[1] + end end def has_signature? - !!(@signature_text && @signed_text) + !!(signature_text && signed_text) end def signature @@ -53,7 +67,7 @@ module Gitlab end def verified_signature - @verified_signature ||= GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature| + @verified_signature ||= GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature| break verified_signature end end diff --git a/lib/gitlab/kubernetes/config_map.rb b/lib/gitlab/kubernetes/config_map.rb new file mode 100644 index 00000000000..95e1054919d --- /dev/null +++ b/lib/gitlab/kubernetes/config_map.rb @@ -0,0 +1,37 @@ +module Gitlab + module Kubernetes + class ConfigMap + def initialize(name, values) + @name = name + @values = values + end + + def generate + resource = ::Kubeclient::Resource.new + resource.metadata = metadata + resource.data = { values: values } + resource + end + + private + + attr_reader :name, :values + + def metadata + { + name: config_map_name, + namespace: namespace, + labels: { name: config_map_name } + } + end + + def config_map_name + "values-content-configuration-#{name}" + end + + def namespace + Gitlab::Kubernetes::Helm::NAMESPACE + end + end + end +end diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb index 737081ddc5b..2edd34109ba 100644 --- a/lib/gitlab/kubernetes/helm/api.rb +++ b/lib/gitlab/kubernetes/helm/api.rb @@ -9,7 +9,8 @@ module Gitlab def install(command) @namespace.ensure_exists! - @kubeclient.create_pod(pod_resource(command)) + create_config_map(command) if command.config_map? + @kubeclient.create_pod(command.pod_resource) end ## @@ -33,8 +34,10 @@ module Gitlab private - def pod_resource(command) - Gitlab::Kubernetes::Helm::Pod.new(command, @namespace.name, @kubeclient).generate + def create_config_map(command) + command.config_map_resource.tap do |config_map_resource| + @kubeclient.create_config_map(config_map_resource) + end end end end diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb new file mode 100644 index 00000000000..6e4df05aa7e --- /dev/null +++ b/lib/gitlab/kubernetes/helm/base_command.rb @@ -0,0 +1,40 @@ +module Gitlab + module Kubernetes + module Helm + class BaseCommand + attr_reader :name + + def initialize(name) + @name = name + end + + def pod_resource + Gitlab::Kubernetes::Helm::Pod.new(self, namespace).generate + end + + def generate_script + <<~HEREDOC + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + HEREDOC + end + + def config_map? + false + end + + def pod_name + "install-#{name}" + end + + private + + def namespace + Gitlab::Kubernetes::Helm::NAMESPACE + end + end + end + end +end diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb new file mode 100644 index 00000000000..a02e64561f6 --- /dev/null +++ b/lib/gitlab/kubernetes/helm/init_command.rb @@ -0,0 +1,19 @@ +module Gitlab + module Kubernetes + module Helm + class InitCommand < BaseCommand + def generate_script + super + [ + init_helm_command + ].join("\n") + end + + private + + def init_helm_command + "helm init >/dev/null" + end + end + end + end +end diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb index bf6981035f4..30af3e97b4a 100644 --- a/lib/gitlab/kubernetes/helm/install_command.rb +++ b/lib/gitlab/kubernetes/helm/install_command.rb @@ -1,54 +1,45 @@ module Gitlab module Kubernetes module Helm - class InstallCommand - attr_reader :name, :install_helm, :chart, :chart_values_file + class InstallCommand < BaseCommand + attr_reader :name, :chart, :repository, :values - def initialize(name, install_helm: false, chart: false, chart_values_file: false) + def initialize(name, chart:, values:, repository: nil) @name = name - @install_helm = install_helm @chart = chart - @chart_values_file = chart_values_file + @values = values + @repository = repository end - def pod_name - "install-#{name}" + def generate_script + super + [ + init_command, + repository_command, + script_command + ].compact.join("\n") end - def generate_script(namespace_name) - [ - install_dps_command, - init_command, - complete_command(namespace_name) - ].join("\n") + def config_map? + true + end + + def config_map_resource + Gitlab::Kubernetes::ConfigMap.new(name, values).generate end private def init_command - if install_helm - 'helm init >/dev/null' - else - 'helm init --client-only >/dev/null' - end + 'helm init --client-only >/dev/null' end - def complete_command(namespace_name) - return unless chart - - if chart_values_file - "helm install #{chart} --name #{name} --namespace #{namespace_name} -f /data/helm/#{name}/config/values.yaml >/dev/null" - else - "helm install #{chart} --name #{name} --namespace #{namespace_name} >/dev/null" - end + def repository_command + "helm repo add #{name} #{repository}" if repository end - def install_dps_command + def script_command <<~HEREDOC - set -eo pipefail - apk add -U ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ + helm install #{chart} --name #{name} --namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} -f /data/helm/#{name}/config/values.yaml >/dev/null HEREDOC end end diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb index ca5e06009fa..1e12299eefd 100644 --- a/lib/gitlab/kubernetes/helm/pod.rb +++ b/lib/gitlab/kubernetes/helm/pod.rb @@ -2,18 +2,17 @@ module Gitlab module Kubernetes module Helm class Pod - def initialize(command, namespace_name, kubeclient) + def initialize(command, namespace_name) @command = command @namespace_name = namespace_name - @kubeclient = kubeclient end def generate spec = { containers: [container_specification], restartPolicy: 'Never' } - if command.chart_values_file - create_config_map + if command.config_map? spec[:volumes] = volumes_specification + spec[:containers][0][:volumeMounts] = volume_mounts_specification end ::Kubeclient::Resource.new(metadata: metadata, spec: spec) @@ -21,18 +20,16 @@ module Gitlab private - attr_reader :command, :namespace_name, :kubeclient + attr_reader :command, :namespace_name, :kubeclient, :config_map def container_specification - container = { + { name: 'helm', image: 'alpine:3.6', env: generate_pod_env(command), command: %w(/bin/sh), args: %w(-c $(COMMAND_SCRIPT)) } - container[:volumeMounts] = volume_mounts_specification if command.chart_values_file - container end def labels @@ -50,13 +47,12 @@ module Gitlab } end - def volume_mounts_specification - [ - { - name: 'configuration-volume', - mountPath: "/data/helm/#{command.name}/config" - } - ] + def generate_pod_env(command) + { + HELM_VERSION: Gitlab::Kubernetes::Helm::HELM_VERSION, + TILLER_NAMESPACE: namespace_name, + COMMAND_SCRIPT: command.generate_script + }.map { |key, value| { name: key, value: value } } end def volumes_specification @@ -71,23 +67,13 @@ module Gitlab ] end - def generate_pod_env(command) - { - HELM_VERSION: Gitlab::Kubernetes::Helm::HELM_VERSION, - TILLER_NAMESPACE: namespace_name, - COMMAND_SCRIPT: command.generate_script(namespace_name) - }.map { |key, value| { name: key, value: value } } - end - - def create_config_map - resource = ::Kubeclient::Resource.new - resource.metadata = { - name: "values-content-configuration-#{command.name}", - namespace: namespace_name, - labels: { name: "values-content-configuration-#{command.name}" } - } - resource.data = { values: File.read(command.chart_values_file) } - kubeclient.create_config_map(resource) + def volume_mounts_specification + [ + { + name: 'configuration-volume', + mountPath: "/data/helm/#{command.name}/config" + } + ] end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 889a03e7859..c9b17f1e826 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-02-20 10:26+0100\n" -"PO-Revision-Date: 2018-02-20 10:26+0100\n" +"POT-Creation-Date: 2018-03-01 18:03+0100\n" +"PO-Revision-Date: 2018-03-01 18:03+0100\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -48,6 +48,9 @@ msgid_plural "%s additional commits have been omitted to prevent performance iss msgstr[0] "" msgstr[1] "" +msgid "%{actionText} & %{openOrClose} %{noteable}" +msgstr "" + msgid "%{commit_author_link} authored %{commit_timeago}" msgstr "" @@ -65,6 +68,9 @@ msgstr "" msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved." msgstr "" +msgid "%{openOrClose} %{noteable}" +msgstr "" + msgid "%{storage_name}: failed storage access attempt on host:" msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:" msgstr[0] "" @@ -123,9 +129,15 @@ msgstr "" msgid "Add Contribution guide" msgstr "" +msgid "Add Kubernetes cluster" +msgstr "" + msgid "Add License" msgstr "" +msgid "Add Readme" +msgstr "" + msgid "Add new directory" msgstr "" @@ -144,7 +156,7 @@ msgstr "" msgid "AdminArea|Stopping jobs failed" msgstr "" -msgid "AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running." +msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running." msgstr "" msgid "AdminHealthPageLink|health page" @@ -207,6 +219,9 @@ msgstr "" msgid "An error occurred while fetching sidebar data" msgstr "" +msgid "An error occurred while fetching the pipeline." +msgstr "" + msgid "An error occurred while getting projects" msgstr "" @@ -225,6 +240,9 @@ msgstr "" msgid "An error occurred while loading the file" msgstr "" +msgid "An error occurred while making the request." +msgstr "" + msgid "An error occurred while rendering KaTeX" msgstr "" @@ -312,6 +330,9 @@ msgstr "" msgid "Authors: %{authors}" msgstr "" +msgid "Auto DevOps enabled" +msgstr "" + msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly." msgstr "" @@ -336,7 +357,13 @@ msgstr "" msgid "AutoDevOps|Learn more in the %{link_to_documentation}" msgstr "" -msgid "AutoDevOps|You can activate %{link_to_settings} for this project." +msgid "AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}." +msgstr "" + +msgid "AutoDevOps|add a Kubernetes cluster" +msgstr "" + +msgid "AutoDevOps|enable Auto DevOps (Beta)" msgstr "" msgid "Available" @@ -351,8 +378,8 @@ msgstr "" msgid "Begin with the selected commit" msgstr "" -msgid "Branch" -msgid_plural "Branches" +msgid "Branch (%{branch_count})" +msgid_plural "Branches (%{branch_count})" msgstr[0] "" msgstr[1] "" @@ -620,6 +647,9 @@ msgstr "" msgid "CircuitBreakerApiLink|circuitbreaker api" msgstr "" +msgid "Click the button below to begin the install process by navigating to the Kubernetes page" +msgstr "" + msgid "Click to expand text" msgstr "" @@ -767,9 +797,6 @@ msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" msgstr "" -msgid "ClusterIntegration|Learn more about Kubernetes" -msgstr "" - msgid "ClusterIntegration|Learn more about environments" msgstr "" @@ -899,6 +926,12 @@ msgstr "" msgid "ClusterIntegration|properly configured" msgstr "" +msgid "Comment and resolve discussion" +msgstr "" + +msgid "Comment and unresolve discussion" +msgstr "" + msgid "Comments" msgstr "" @@ -907,6 +940,11 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit (%{commit_count})" +msgid_plural "Commits (%{commit_count})" +msgstr[0] "" +msgstr[1] "" + msgid "Commit Message" msgstr "" @@ -1051,6 +1089,9 @@ msgstr "" msgid "Copy branch name to clipboard" msgstr "" +msgid "Copy command to clipboard" +msgstr "" + msgid "Copy commit SHA to clipboard" msgstr "" @@ -1233,6 +1274,9 @@ msgstr "" msgid "Emails" msgstr "" +msgid "Enable Auto DevOps" +msgstr "" + msgid "Environments|An error occurred while fetching the environments." msgstr "" @@ -1341,6 +1385,9 @@ msgstr "" msgid "Explore public groups" msgstr "" +msgid "Failed Jobs" +msgstr "" + msgid "Failed to change the owner" msgstr "" @@ -1368,6 +1415,9 @@ msgstr "" msgid "Files" msgstr "" +msgid "Files (%{human_size})" +msgstr "" + msgid "Filter by commit message" msgstr "" @@ -1403,6 +1453,9 @@ msgstr "" msgid "From merge request merge until deploy to production" msgstr "" +msgid "From the Kubernetes cluster details view, install Runner from the applications list" +msgstr "" + msgid "GPG Keys" msgstr "" @@ -1531,9 +1584,15 @@ msgstr "" msgid "Housekeeping successfully started" msgstr "" +msgid "If you already have files you can push them using the %{link_to_cli} below." +msgstr "" + msgid "Import repository" msgstr "" +msgid "Install Runner on Kubernetes" +msgstr "" + msgid "Install a Runner compatible with GitLab CI" msgstr "" @@ -1573,6 +1632,9 @@ msgstr "" msgid "January" msgstr "" +msgid "Jobs" +msgstr "" + msgid "Jul" msgstr "" @@ -1603,6 +1665,9 @@ msgstr "" msgid "Kubernetes cluster was successfully updated." msgstr "" +msgid "Kubernetes configured" +msgstr "" + msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page" msgstr "" @@ -1650,6 +1715,12 @@ msgstr "" msgid "Learn more" msgstr "" +msgid "Learn more about Kubernetes" +msgstr "" + +msgid "Learn more about protected branches" +msgstr "" + msgid "Learn more in the" msgstr "" @@ -1743,6 +1814,12 @@ msgstr "" msgid "MissingSSHKeyWarningLink|add an SSH key" msgstr "" +msgid "Modal|Cancel" +msgstr "" + +msgid "Modal|Close" +msgstr "" + msgid "Monitoring" msgstr "" @@ -1832,9 +1909,6 @@ msgstr "" msgid "No schedules" msgstr "" -msgid "No time spent" -msgstr "" - msgid "None" msgstr "" @@ -1850,6 +1924,9 @@ msgstr "" msgid "Not enough data" msgstr "" +msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}" +msgstr "" + msgid "Notification events" msgstr "" @@ -1943,6 +2020,9 @@ msgstr "" msgid "Options" msgstr "" +msgid "Otherwise it is recommended you start with one of the options below." +msgstr "" + msgid "Overview" msgstr "" @@ -2048,19 +2128,19 @@ msgstr "" msgid "Pipeline|Retry pipeline" msgstr "" -msgid "Pipeline|Retry pipeline #%{id}?" +msgid "Pipeline|Retry pipeline #%{pipelineId}?" msgstr "" msgid "Pipeline|Stop pipeline" msgstr "" -msgid "Pipeline|Stop pipeline #%{id}?" +msgid "Pipeline|Stop pipeline #%{pipelineId}?" msgstr "" -msgid "Pipeline|You’re about to retry pipeline %{id}." +msgid "Pipeline|You’re about to retry pipeline %{pipelineId}." msgstr "" -msgid "Pipeline|You’re about to stop pipeline %{id}." +msgid "Pipeline|You’re about to stop pipeline %{pipelineId}." msgstr "" msgid "Pipeline|all" @@ -2093,6 +2173,9 @@ msgstr "" msgid "Private - The group and its projects can only be viewed by members." msgstr "" +msgid "Private projects can be created in your personal namespace with:" +msgstr "" + msgid "Profile" msgstr "" @@ -2294,6 +2377,12 @@ msgstr "" msgid "Push events" msgstr "" +msgid "Push project from command line" +msgstr "" + +msgid "Push to create a project" +msgstr "" + msgid "Quick actions can be used in the issues description and comment boxes." msgstr "" @@ -2357,6 +2446,9 @@ msgstr "" msgid "Reset runners registration token" msgstr "" +msgid "Resolve discussion" +msgstr "" + msgid "Reveal value" msgid_plural "Reveal values" msgstr[0] "" @@ -2416,6 +2508,9 @@ msgstr "" msgid "Select a timezone" msgstr "" +msgid "Select an existing Kubernetes cluster or create a new one" +msgstr "" + msgid "Select assignee" msgstr "" @@ -2446,15 +2541,18 @@ msgstr "" msgid "Set up Koding" msgstr "" -msgid "Set up auto deploy" -msgstr "" - msgid "SetPasswordToCloneLink|set a password" msgstr "" msgid "Settings" msgstr "" +msgid "Setup a specific Runner automatically" +msgstr "" + +msgid "Show command" +msgstr "" + msgid "Show parent pages" msgstr "" @@ -2478,10 +2576,13 @@ msgstr "" msgid "Something went wrong trying to change the confidentiality of this issue" msgstr "" +msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}" +msgstr "" + msgid "Something went wrong when toggling the button" msgstr "" -msgid "Something went wrong while closing the issue. Please try again later" +msgid "Something went wrong while closing the %{issuable}. Please try again later" msgstr "" msgid "Something went wrong while fetching the projects." @@ -2490,7 +2591,10 @@ msgstr "" msgid "Something went wrong while fetching the registry list." msgstr "" -msgid "Something went wrong while reopening the issue. Please try again later" +msgid "Something went wrong while reopening the %{issuable}. Please try again later" +msgstr "" + +msgid "Something went wrong while resolving this discussion. Please try again." msgstr "" msgid "Something went wrong. Please try again." @@ -2631,8 +2735,8 @@ msgstr "" msgid "System Hooks" msgstr "" -msgid "Tag" -msgid_plural "Tags" +msgid "Tag (%{tag_count})" +msgid_plural "Tags (%{tag_count})" msgstr[0] "" msgstr[1] "" @@ -2759,6 +2863,9 @@ msgstr "" msgid "The repository for this project does not exist." msgstr "" +msgid "The repository for this project is empty" +msgstr "" + msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request." msgstr "" @@ -3021,6 +3128,9 @@ msgstr[1] "" msgid "Time|s" msgstr "" +msgid "Tip:" +msgstr "" + msgid "Todo" msgstr "" @@ -3036,9 +3146,6 @@ msgstr "" msgid "Total Time" msgstr "" -msgid "Total issue time spent" -msgstr "" - msgid "Total test time for all commits/merges" msgstr "" @@ -3063,6 +3170,9 @@ msgstr "" msgid "Unlocked" msgstr "" +msgid "Unresolve discussion" +msgstr "" + msgid "Unstar" msgstr "" @@ -3126,6 +3236,9 @@ msgstr "" msgid "We want to be sure it is you, please confirm you are not a robot." msgstr "" +msgid "Web IDE" +msgstr "" + msgid "Wiki" msgstr "" @@ -3252,9 +3365,15 @@ msgstr "" msgid "You are on a read-only GitLab instance." msgstr "" +msgid "You can also create a project from the command line." +msgstr "" + msgid "You can also star a label to make it a priority label." msgstr "" +msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}" +msgstr "" + msgid "You can move around the graph by using the arrow keys." msgstr "" @@ -3324,6 +3443,9 @@ msgstr "" msgid "branch name" msgstr "" +msgid "command line instructions" +msgstr "" + msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue." msgstr "" diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb index da54aa9054c..185b6b4ce57 100644 --- a/spec/controllers/groups/labels_controller_spec.rb +++ b/spec/controllers/groups/labels_controller_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe Groups::LabelsController do - let(:group) { create(:group) } - let(:user) { create(:user) } + set(:group) { create(:group) } + set(:user) { create(:user) } + set(:project) { create(:project, namespace: group) } before do group.add_owner(user) @@ -10,6 +11,34 @@ describe Groups::LabelsController do sign_in(user) end + describe 'GET #index' do + set(:label_1) { create(:label, project: project, title: 'label_1') } + set(:group_label_1) { create(:group_label, group: group, title: 'group_label_1') } + + it 'returns group and project labels by default' do + get :index, group_id: group, format: :json + + label_ids = json_response.map {|label| label['title']} + expect(label_ids).to match_array([label_1.title, group_label_1.title]) + end + + context 'with ancestor group', :nested_groups do + set(:subgroup) { create(:group, parent: group) } + set(:subgroup_label_1) { create(:group_label, group: subgroup, title: 'subgroup_label_1') } + + before do + subgroup.add_owner(user) + end + + it 'returns ancestor group labels', :nested_groups do + get :index, group_id: subgroup, include_ancestor_groups: true, only_group_labels: true, format: :json + + label_ids = json_response.map {|label| label['title']} + expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title]) + end + end + end + describe 'POST #toggle_subscription' do it 'allows user to toggle subscription on group labels' do label = create(:group_label, group: group) diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index 775fbb3d27b..3deca103578 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -34,5 +34,6 @@ FactoryBot.define do factory :clusters_applications_ingress, class: Clusters::Applications::Ingress factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus + factory :clusters_applications_runner, class: Clusters::Applications::Runner end end diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index dc76efea35b..d434c501110 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -89,6 +89,25 @@ describe LabelsFinder do expect(finder.execute).to eq [private_subgroup_label_1] end end + + context 'when including labels from group descendants', :nested_groups do + it 'returns labels from group and its descendants' do + private_group_1.add_developer(user) + private_subgroup_1.add_developer(user) + + finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true) + + expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1] + end + + it 'ignores labels from groups which user can not read' do + private_subgroup_1.add_developer(user) + + finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true) + + expect(finder.execute).to eq [private_subgroup_label_1] + end + end end context 'filtering by project_id' do diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index dfb4cc1b9b1..d546543d273 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -38,11 +38,9 @@ describe('Applications', () => { expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined(); }); - /* * / it('renders a row for GitLab Runner', () => { expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined(); }); - /* */ }); describe('Ingress application', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index f1da5f81c0f..756a654765b 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -128,6 +128,24 @@ describe('Filtered Search Visual Tokens', () => { }); }); + describe('getEndpointWithQueryParams', () => { + it('returns `endpoint` string as is when second param `endpointQueryParams` is undefined, null or empty string', () => { + const endpoint = 'foo/bar/labels.json'; + expect(subject.getEndpointWithQueryParams(endpoint)).toBe(endpoint); + expect(subject.getEndpointWithQueryParams(endpoint, null)).toBe(endpoint); + expect(subject.getEndpointWithQueryParams(endpoint, '')).toBe(endpoint); + }); + + it('returns `endpoint` string with values of `endpointQueryParams`', () => { + const endpoint = 'foo/bar/labels.json'; + const singleQueryParams = '{"foo":"true"}'; + const multipleQueryParams = '{"foo":"true","bar":"true"}'; + + expect(subject.getEndpointWithQueryParams(endpoint, singleQueryParams)).toBe(`${endpoint}?foo=true`); + expect(subject.getEndpointWithQueryParams(endpoint, multipleQueryParams)).toBe(`${endpoint}?foo=true&bar=true`); + }); + }); + describe('unselectTokens', () => { it('does nothing when there are no tokens', () => { const beforeHTML = tokensContainer.innerHTML; diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 49799c31995..27f06573432 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -166,6 +166,21 @@ describe('common_utils', () => { }); }); + describe('objectToQueryString', () => { + it('returns empty string when `param` is undefined, null or empty string', () => { + expect(commonUtils.objectToQueryString()).toBe(''); + expect(commonUtils.objectToQueryString('')).toBe(''); + }); + + it('returns query string with values of `params`', () => { + const singleQueryParams = { foo: true }; + const multipleQueryParams = { foo: true, bar: true }; + + expect(commonUtils.objectToQueryString(singleQueryParams)).toBe('foo=true'); + expect(commonUtils.objectToQueryString(multipleQueryParams)).toBe('foo=true&bar=true'); + }); + }); + describe('buildUrlWithCurrentLocation', () => { it('should build an url with current location and given parameters', () => { expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname); diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 0b20a6349a2..a05feaac1ca 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -393,81 +393,111 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '.extract_signature' do - subject { described_class.extract_signature(repository, commit_id) } - - shared_examples '.extract_signature' do - context 'when the commit is signed' do - let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } - - it 'returns signature and signed text' do - signature, signed_text = subject - - expected_signature = <<~SIGNATURE - -----BEGIN PGP SIGNATURE----- - Version: GnuPG/MacGPG2 v2.0.22 (Darwin) - Comment: GPGTools - https://gpgtools.org + shared_examples 'extracting commit signature' do + context 'when the commit is signed' do + let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } + + it 'returns signature and signed text' do + signature, signed_text = subject + + expected_signature = <<~SIGNATURE + -----BEGIN PGP SIGNATURE----- + Version: GnuPG/MacGPG2 v2.0.22 (Darwin) + Comment: GPGTools - https://gpgtools.org + + iQEcBAABCgAGBQJTDvaZAAoJEGJ8X1ifRn8XfvYIAMuB0yrbTGo1BnOSoDfyrjb0 + Kw2EyUzvXYL72B63HMdJ+/0tlSDC6zONF3fc+bBD8z+WjQMTbwFNMRbSSy2rKEh+ + mdRybOP3xBIMGgEph0/kmWln39nmFQBsPRbZBWoU10VfI/ieJdEOgOphszgryRar + TyS73dLBGE9y9NIININVaNISet9D9QeXFqc761CGjh4YIghvPpi+YihMWapGka6v + hgKhX+hc5rj+7IEE0CXmlbYR8OYvAbAArc5vJD7UTxAY4Z7/l9d6Ydt9GQ25khfy + ANFgltYzlR6evLFmDjssiP/mx/ZMN91AL0ueJ9nNGv411Mu2CUW+tDCaQf35mdc= + =j51i + -----END PGP SIGNATURE----- + SIGNATURE + + expect(signature).to eq(expected_signature.chomp) + expect(signature).to be_a_binary_string + + expected_signed_text = <<~SIGNED_TEXT + tree 22bfa2fbd217df24731f43ff43a4a0f8db759dae + parent ae73cb07c9eeaf35924a10f713b364d32b2dd34f + author Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + committer Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + + Feature added + + Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> + SIGNED_TEXT + + expect(signed_text).to eq(expected_signed_text) + expect(signed_text).to be_a_binary_string + end + end - iQEcBAABCgAGBQJTDvaZAAoJEGJ8X1ifRn8XfvYIAMuB0yrbTGo1BnOSoDfyrjb0 - Kw2EyUzvXYL72B63HMdJ+/0tlSDC6zONF3fc+bBD8z+WjQMTbwFNMRbSSy2rKEh+ - mdRybOP3xBIMGgEph0/kmWln39nmFQBsPRbZBWoU10VfI/ieJdEOgOphszgryRar - TyS73dLBGE9y9NIININVaNISet9D9QeXFqc761CGjh4YIghvPpi+YihMWapGka6v - hgKhX+hc5rj+7IEE0CXmlbYR8OYvAbAArc5vJD7UTxAY4Z7/l9d6Ydt9GQ25khfy - ANFgltYzlR6evLFmDjssiP/mx/ZMN91AL0ueJ9nNGv411Mu2CUW+tDCaQf35mdc= - =j51i - -----END PGP SIGNATURE----- - SIGNATURE + context 'when the commit has no signature' do + let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } - expect(signature).to eq(expected_signature.chomp) - expect(signature).to be_a_binary_string + it 'returns nil' do + expect(subject).to be_nil + end + end - expected_signed_text = <<~SIGNED_TEXT - tree 22bfa2fbd217df24731f43ff43a4a0f8db759dae - parent ae73cb07c9eeaf35924a10f713b364d32b2dd34f - author Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 - committer Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200 + context 'when the commit cannot be found' do + let(:commit_id) { Gitlab::Git::BLANK_SHA } - Feature added + it 'returns nil' do + expect(subject).to be_nil + end + end - Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> - SIGNED_TEXT + context 'when the commit ID is invalid' do + let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e' } - expect(signed_text).to eq(expected_signed_text) - expect(signed_text).to be_a_binary_string - end + it 'raises ArgumentError' do + expect { subject }.to raise_error(ArgumentError) end + end + end - context 'when the commit has no signature' do - let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' } - - it 'returns nil' do - expect(subject).to be_nil + describe '.extract_signature_lazily' do + shared_examples 'loading signatures in batch once' do + it 'fetches signatures in batch once' do + commit_ids = %w[0b4bc9a49b562e85de7cc9e834518ea6828729b9 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6] + signatures = commit_ids.map do |commit_id| + described_class.extract_signature_lazily(repository, commit_id) end - end - context 'when the commit cannot be found' do - let(:commit_id) { Gitlab::Git::BLANK_SHA } + expect(described_class).to receive(:batch_signature_extraction) + .with(repository, commit_ids) + .once + .and_return({}) - it 'returns nil' do - expect(subject).to be_nil - end + 2.times { signatures.each(&:itself) } end + end - context 'when the commit ID is invalid' do - let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e' } + subject { described_class.extract_signature_lazily(repository, commit_id).itself } - it 'raises ArgumentError' do - expect { subject }.to raise_error(ArgumentError) - end - end + context 'with Gitaly extract_commit_signature_in_batch feature enabled' do + it_behaves_like 'extracting commit signature' + it_behaves_like 'loading signatures in batch once' + end + + context 'with Gitaly extract_commit_signature_in_batch feature disabled', :disable_gitaly do + it_behaves_like 'extracting commit signature' + it_behaves_like 'loading signatures in batch once' end + end + + describe '.extract_signature' do + subject { described_class.extract_signature(repository, commit_id) } context 'with gitaly' do - it_behaves_like '.extract_signature' + it_behaves_like 'extracting commit signature' end - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '.extract_signature' + context 'without gitaly', :disable_gitaly do + it_behaves_like 'extracting commit signature' end end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index 67c62458f0f..8c6d673391b 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -38,7 +38,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -101,7 +101,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -140,7 +140,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -175,7 +175,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -211,7 +211,7 @@ describe Gitlab::Gpg::Commit do end before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ @@ -241,7 +241,7 @@ describe Gitlab::Gpg::Commit do let!(:commit) { create :commit, project: project, sha: commit_sha } before do - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return( [ diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb index c034eccf2a6..6fbffc38444 100644 --- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb +++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do before do allow_any_instance_of(Project).to receive(:commit).and_return(commit) - allow(Gitlab::Git::Commit).to receive(:extract_signature) + allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily) .with(Gitlab::Git::Repository, commit_sha) .and_return(signature) end diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb new file mode 100644 index 00000000000..33dfa461202 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes::ConfigMap do + let(:kubeclient) { double('kubernetes client') } + let(:application) { create(:clusters_applications_prometheus) } + let(:config_map) { described_class.new(application.name, application.values) } + let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } + + let(:metadata) do + { + name: "values-content-configuration-#{application.name}", + namespace: namespace, + labels: { name: "values-content-configuration-#{application.name}" } + } + end + + describe '#generate' do + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: { values: application.values }) } + subject { config_map.generate } + + it 'should build a Kubeclient Resource' do + is_expected.to eq(resource) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 69112fe90b1..740466ea5cb 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -5,14 +5,21 @@ describe Gitlab::Kubernetes::Helm::Api do let(:helm) { described_class.new(client) } let(:gitlab_namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) } - let(:install_helm) { true } - let(:chart) { 'stable/a_chart' } - let(:application_name) { 'app_name' } - let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm: install_helm, chart: chart) } + let(:application) { create(:clusters_applications_prometheus) } + + let(:command) do + Gitlab::Kubernetes::Helm::InstallCommand.new( + application.name, + chart: application.chart, + values: application.values + ) + end + subject { helm } before do allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client).and_return(namespace) + allow(client).to receive(:create_config_map) end describe '#initialize' do @@ -26,6 +33,7 @@ describe Gitlab::Kubernetes::Helm::Api do describe '#install' do before do allow(client).to receive(:create_pod).and_return(nil) + allow(client).to receive(:create_config_map).and_return(nil) allow(namespace).to receive(:ensure_exists!).once end @@ -35,6 +43,16 @@ describe Gitlab::Kubernetes::Helm::Api do subject.install(command) end + + context 'with a ConfigMap' do + let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application.name, application.values).generate } + + it 'creates a ConfigMap on kubeclient' do + expect(client).to receive(:create_config_map).with(resource).once + + subject.install(command) + end + end end describe '#installation_status' do diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb new file mode 100644 index 00000000000..3cfdae794f6 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes::Helm::BaseCommand do + let(:application) { create(:clusters_applications_helm) } + let(:base_command) { described_class.new(application.name) } + + describe '#generate_script' do + let(:helm_version) { Gitlab::Kubernetes::Helm::HELM_VERSION } + let(:command) do + <<~HEREDOC + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{helm_version}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + HEREDOC + end + + subject { base_command.generate_script } + + it 'should return a command that prepares the environment for helm-cli' do + expect(subject).to eq(command) + end + end + + describe '#pod_resource' do + subject { base_command.pod_resource } + + it 'should returns a kubeclient resoure with pod content for application' do + is_expected.to be_an_instance_of ::Kubeclient::Resource + end + end + + describe '#config_map?' do + subject { base_command.config_map? } + + it { is_expected.to be_falsy } + end + + describe '#pod_name' do + subject { base_command.pod_name } + + it { is_expected.to eq('install-helm') } + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb new file mode 100644 index 00000000000..e6920b0a76f --- /dev/null +++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::Kubernetes::Helm::InitCommand do + let(:application) { create(:clusters_applications_helm) } + let(:init_command) { described_class.new(application.name) } + + describe '#generate_script' do + let(:command) do + <<~MSG.chomp + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + helm init >/dev/null + MSG + end + + subject { init_command.generate_script } + + it 'should return the appropriate command' do + is_expected.to eq(command) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 63997a40d52..137b8f718de 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -1,79 +1,56 @@ require 'rails_helper' describe Gitlab::Kubernetes::Helm::InstallCommand do - let(:prometheus) { create(:clusters_applications_prometheus) } - - describe "#initialize" do - context "With all the params" do - subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) } - - it 'should assign all parameters' do - expect(subject.name).to eq(prometheus.name) - expect(subject.install_helm).to be_truthy - expect(subject.chart).to eq(prometheus.chart) - expect(subject.chart_values_file).to eq("#{Rails.root}/vendor/prometheus/values.yaml") - end - end - - context 'when install_helm is not set' do - subject { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: true) } - - it 'should set install_helm as false' do - expect(subject.install_helm).to be_falsy - end - end - - context 'when chart is not set' do - subject { described_class.new(prometheus.name, install_helm: true) } + let(:application) { create(:clusters_applications_prometheus) } + let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } + + let(:install_command) do + described_class.new( + application.name, + chart: application.chart, + values: application.values + ) + end - it 'should set chart as nil' do - expect(subject.chart).to be_falsy - end + describe '#generate_script' do + let(:command) do + <<~MSG + set -eo pipefail + apk add -U ca-certificates openssl >/dev/null + wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null + mv /tmp/linux-amd64/helm /usr/bin/ + helm init --client-only >/dev/null + helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null + MSG end - context 'when chart_values_file is not set' do - subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart) } + subject { install_command.generate_script } - it 'should set chart_values_file as nil' do - expect(subject.chart_values_file).to be_falsy - end + it 'should return appropriate command' do + is_expected.to eq(command) end - end - - describe "#generate_script" do - let(:install_command) { described_class.new(prometheus.name, install_helm: install_helm) } - let(:client) { double('kubernetes client') } - let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) } - subject { install_command.send(:generate_script, namespace.name) } - context 'when install helm is true' do - let(:install_helm) { true } - let(:command) do - <<~MSG - set -eo pipefail - apk add -U ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ - - helm init >/dev/null - MSG + context 'with an application with a repository' do + let(:ci_runner) { create(:ci_runner) } + let(:application) { create(:clusters_applications_runner, runner: ci_runner) } + let(:install_command) do + described_class.new( + application.name, + chart: application.chart, + values: application.values, + repository: application.repository + ) end - it 'should return appropriate command' do - is_expected.to eq(command) - end - end - - context 'when install helm is false' do - let(:install_helm) { false } let(:command) do <<~MSG set -eo pipefail apk add -U ca-certificates openssl >/dev/null wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null mv /tmp/linux-amd64/helm /usr/bin/ - helm init --client-only >/dev/null + helm repo add #{application.name} #{application.repository} + helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null MSG end @@ -81,50 +58,29 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do is_expected.to eq(command) end end + end - context 'when chart is present' do - let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart) } - let(:command) do - <<~MSG.chomp - set -eo pipefail - apk add -U ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ + describe '#config_map?' do + subject { install_command.config_map? } - helm init --client-only >/dev/null - helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} >/dev/null - MSG - end + it { is_expected.to be_truthy } + end - it 'should return appropriate command' do - is_expected.to eq(command) - end + describe '#config_map_resource' do + let(:metadata) do + { + name: "values-content-configuration-#{application.name}", + namespace: namespace, + labels: { name: "values-content-configuration-#{application.name}" } + } end - context 'when chart values file is present' do - let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) } - let(:command) do - <<~MSG.chomp - set -eo pipefail - apk add -U ca-certificates openssl >/dev/null - wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null - mv /tmp/linux-amd64/helm /usr/bin/ + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: { values: application.values }) } - helm init --client-only >/dev/null - helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} -f /data/helm/#{prometheus.name}/config/values.yaml >/dev/null - MSG - end + subject { install_command.config_map_resource } - it 'should return appropriate command' do - is_expected.to eq(command) - end + it 'returns a KubeClient resource with config map content for the application' do + is_expected.to eq(resource) end end - - describe "#pod_name" do - let(:install_command) { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: true) } - subject { install_command.send(:pod_name) } - - it { is_expected.to eq('install-prometheus') } - end end diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index ebb6033f71e..43adc80d576 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -5,13 +5,9 @@ describe Gitlab::Kubernetes::Helm::Pod do let(:cluster) { create(:cluster) } let(:app) { create(:clusters_applications_prometheus, cluster: cluster) } let(:command) { app.install_command } - let(:client) { double('kubernetes client') } - let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) } - subject { described_class.new(command, namespace.name, client) } + let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } - before do - allow(client).to receive(:create_config_map).and_return(nil) - end + subject { described_class.new(command, namespace) } shared_examples 'helm pod' do it 'should generate a Kubeclient::Resource' do @@ -47,7 +43,7 @@ describe Gitlab::Kubernetes::Helm::Pod do end end - context 'with a configuration file' do + context 'with a install command' do it_behaves_like 'helm pod' it 'should include volumes for the container' do @@ -62,14 +58,14 @@ describe Gitlab::Kubernetes::Helm::Pod do end it 'should mount configMap specification in the volume' do - spec = subject.generate.spec - expect(spec.volumes.first.configMap['name']).to eq("values-content-configuration-#{app.name}") - expect(spec.volumes.first.configMap['items'].first['key']).to eq('values') - expect(spec.volumes.first.configMap['items'].first['path']).to eq('values.yaml') + volume = subject.generate.spec.volumes.first + expect(volume.configMap['name']).to eq("values-content-configuration-#{app.name}") + expect(volume.configMap['items'].first['key']).to eq('values') + expect(volume.configMap['items'].first['path']).to eq('values.yaml') end end - context 'without a configuration file' do + context 'with a init command' do let(:app) { create(:clusters_applications_helm, cluster: cluster) } it_behaves_like 'helm pod' diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index eb57abaf6ef..ba7bad617b4 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -1,102 +1,17 @@ require 'rails_helper' describe Clusters::Applications::Helm do - it { is_expected.to belong_to(:cluster) } - it { is_expected.to validate_presence_of(:cluster) } - - describe '#name' do - it 'is .application_name' do - expect(subject.name).to eq(described_class.application_name) - end - - it 'is recorded in Clusters::Cluster::APPLICATIONS' do - expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class) - end - end - - describe '#version' do - it 'defaults to Gitlab::Kubernetes::Helm::HELM_VERSION' do - expect(subject.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION) - end - end - - describe '#status' do - let(:cluster) { create(:cluster) } - - subject { described_class.new(cluster: cluster) } - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - - context 'when platform kubernetes is defined' do - let(:cluster) { create(:cluster, :provided_by_gcp) } - - it 'defaults to :installable' do - expect(subject.status_name).to be(:installable) - end - end - end + include_examples 'cluster application core specs', :clusters_applications_helm describe '#install_command' do - it 'has all the needed information' do - expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true) - end - end - - describe 'status state machine' do - describe '#make_installing' do - subject { create(:clusters_applications_helm, :scheduled) } - - it 'is installing' do - subject.make_installing! - - expect(subject).to be_installing - end - end - - describe '#make_installed' do - subject { create(:clusters_applications_helm, :installing) } - - it 'is installed' do - subject.make_installed - - expect(subject).to be_installed - end - end - - describe '#make_errored' do - subject { create(:clusters_applications_helm, :installing) } - let(:reason) { 'some errors' } - - it 'is errored' do - subject.make_errored(reason) - - expect(subject).to be_errored - expect(subject.status_reason).to eq(reason) - end - end - - describe '#make_scheduled' do - subject { create(:clusters_applications_helm, :installable) } - - it 'is scheduled' do - subject.make_scheduled - - expect(subject).to be_scheduled - end - - describe 'when was errored' do - subject { create(:clusters_applications_helm, :errored) } + let(:helm) { create(:clusters_applications_helm) } - it 'clears #status_reason' do - expect(subject.status_reason).not_to be_nil + subject { helm.install_command } - subject.make_scheduled! + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InitCommand) } - expect(subject.status_reason).to be_nil - end - end + it 'should be initialized with 1 arguments' do + expect(subject.name).to eq('helm') end end end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index a34f4ff2b48..03f5b88a525 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -1,16 +1,16 @@ require 'rails_helper' describe Clusters::Applications::Ingress do - it { is_expected.to belong_to(:cluster) } - it { is_expected.to validate_presence_of(:cluster) } + let(:ingress) { create(:clusters_applications_ingress) } + + include_examples 'cluster application core specs', :clusters_applications_ingress + include_examples 'cluster application status specs', :cluster_application_ingress before do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in) allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) end - include_examples 'cluster application specs', described_class - describe '#make_installed!' do before do application.make_installed! @@ -52,4 +52,27 @@ describe Clusters::Applications::Ingress do end end end + + describe '#install_command' do + subject { ingress.install_command } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + + it 'should be initialized with ingress arguments' do + expect(subject.name).to eq('ingress') + expect(subject.chart).to eq('stable/nginx-ingress') + expect(subject.values).to eq(ingress.values) + end + end + + describe '#values' do + subject { ingress.values } + + it 'should include ingress valid keys' do + is_expected.to include('image') + is_expected.to include('repository') + is_expected.to include('stats') + is_expected.to include('podAnnotations') + end + end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 01037919530..df8a508e021 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -1,10 +1,8 @@ require 'rails_helper' describe Clusters::Applications::Prometheus do - it { is_expected.to belong_to(:cluster) } - it { is_expected.to validate_presence_of(:cluster) } - - include_examples 'cluster application specs', described_class + include_examples 'cluster application core specs', :clusters_applications_prometheus + include_examples 'cluster application status specs', :cluster_application_prometheus describe 'transition to installed' do let(:project) { create(:project) } @@ -24,14 +22,6 @@ describe Clusters::Applications::Prometheus do end end - describe "#chart_values_file" do - subject { create(:clusters_applications_prometheus).chart_values_file } - - it 'should return chart values file path' do - expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml") - end - end - describe '#proxy_client' do context 'cluster is nil' do it 'returns nil' do @@ -85,4 +75,33 @@ describe Clusters::Applications::Prometheus do end end end + + describe '#install_command' do + let(:kubeclient) { double('kubernetes client') } + let(:prometheus) { create(:clusters_applications_prometheus) } + + subject { prometheus.install_command } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + + it 'should be initialized with 3 arguments' do + expect(subject.name).to eq('prometheus') + expect(subject.chart).to eq('stable/prometheus') + expect(subject.values).to eq(prometheus.values) + end + end + + describe '#values' do + let(:prometheus) { create(:clusters_applications_prometheus) } + + subject { prometheus.values } + + it 'should include prometheus valid values' do + is_expected.to include('alertmanager') + is_expected.to include('kubeStateMetrics') + is_expected.to include('nodeExporter') + is_expected.to include('pushgateway') + is_expected.to include('serverFiles') + end + end end diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb new file mode 100644 index 00000000000..612a3c8e413 --- /dev/null +++ b/spec/models/clusters/applications/runner_spec.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +describe Clusters::Applications::Runner do + let(:ci_runner) { create(:ci_runner) } + + include_examples 'cluster application core specs', :clusters_applications_runner + include_examples 'cluster application status specs', :cluster_application_runner + + it { is_expected.to belong_to(:runner) } + + describe '#install_command' do + let(:kubeclient) { double('kubernetes client') } + let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } + + subject { gitlab_runner.install_command } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) } + + it 'should be initialized with 4 arguments' do + expect(subject.name).to eq('runner') + expect(subject.chart).to eq('runner/gitlab-runner') + expect(subject.repository).to eq('https://charts.gitlab.io') + expect(subject.values).to eq(gitlab_runner.values) + end + end + + describe '#values' do + let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } + + subject { gitlab_runner.values } + + it 'should include runner valid values' do + is_expected.to include('concurrent') + is_expected.to include('checkInterval') + is_expected.to include('rbac') + is_expected.to include('runners') + is_expected.to include('resources') + is_expected.to include("runnerToken: #{ci_runner.token}") + is_expected.to include("gitlabUrl: #{Gitlab::Routing.url_helpers.root_url}") + end + + context 'without a runner' do + let(:project) { create(:project) } + let(:cluster) { create(:cluster) } + let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) } + + before do + cluster.projects << project + end + + it 'creates a runner' do + expect do + subject + end.to change { Ci::Runner.count }.by(1) + end + + it 'uses the new runner token' do + expect(subject).to include("runnerToken: #{gitlab_runner.reload.runner.token}") + end + + it 'assigns the new runner to runner' do + subject + gitlab_runner.reload + + expect(gitlab_runner.runner).not_to be_nil + end + end + end +end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 799d7ced116..8f12a0e3085 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -8,6 +8,7 @@ describe Clusters::Cluster do it { is_expected.to have_one(:application_helm) } it { is_expected.to have_one(:application_ingress) } it { is_expected.to have_one(:application_prometheus) } + it { is_expected.to have_one(:application_runner) } it { is_expected.to delegate_method(:status).to(:provider) } it { is_expected.to delegate_method(:status_reason).to(:provider) } it { is_expected.to delegate_method(:status_name).to(:provider) } @@ -196,9 +197,10 @@ describe Clusters::Cluster do let!(:helm) { create(:clusters_applications_helm, cluster: cluster) } let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) } let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) } + let!(:runner) { create(:clusters_applications_runner, cluster: cluster) } it 'returns a list of created applications' do - is_expected.to contain_exactly(helm, ingress, prometheus) + is_expected.to contain_exactly(helm, ingress, prometheus, runner) end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index c536dab2681..b7ed8be69fc 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -368,7 +368,9 @@ describe CommitStatus do 'rspec:windows 0 : / 1' => 'rspec:windows', 'rspec:windows 0 : / 1 name' => 'rspec:windows name', '0 1 name ruby' => 'name ruby', - '0 :/ 1 name ruby' => 'name ruby' + '0 :/ 1 name ruby' => 'name ruby', + 'golang test 1.8' => 'golang test', + '1.9 golang test' => 'golang test' } tests.each do |name, group_name| diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3531de244bd..00b5226d874 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1635,6 +1635,32 @@ describe User do end end + describe '#authorizations_for_projects' do + let!(:user) { create(:user) } + subject { Project.where("EXISTS (?)", user.authorizations_for_projects) } + + it 'includes projects that belong to a user, but no other projects' do + owned = create(:project, :private, namespace: user.namespace) + member = create(:project, :private).tap { |p| p.add_master(user) } + other = create(:project) + + expect(subject).to include(owned) + expect(subject).to include(member) + expect(subject).not_to include(other) + end + + it 'includes projects a user has access to, but no other projects' do + other_user = create(:user) + accessible = create(:project, :private, namespace: other_user.namespace) do |project| + project.add_developer(user) + end + other = create(:project) + + expect(subject).to include(accessible) + expect(subject).not_to include(other) + end + end + describe '#authorized_projects', :delete do context 'with a minimum access level' do it 'includes projects for which the user is an owner' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 658cedd6b5f..e8eb01f6c32 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -9,6 +9,7 @@ describe API::MergeRequests do let(:non_member) { create(:user) } let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } + let(:pipeline) { create(:ci_empty_pipeline) } let(:milestone1) { create(:milestone, title: '0.9', project: project) } let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } @@ -500,6 +501,45 @@ describe API::MergeRequests do expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size) end + context 'merge_request_metrics' do + before do + merge_request.metrics.update!(merged_by: user, + latest_closed_by: user, + latest_closed_at: 1.hour.ago, + merged_at: 2.hours.ago, + pipeline: pipeline, + latest_build_started_at: 3.hours.ago, + latest_build_finished_at: 1.hour.ago, + first_deployed_to_production_at: 3.minutes.ago) + end + + it 'has fields from merge request metrics' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) + + expect(json_response).to include('merged_by', + 'merged_at', + 'closed_by', + 'closed_at', + 'latest_build_started_at', + 'latest_build_finished_at', + 'first_deployed_to_production_at', + 'pipeline') + end + + it 'returns correct values' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.reload.iid}", user) + + expect(json_response['merged_by']['id']).to eq(merge_request.metrics.merged_by_id) + expect(Time.parse json_response['merged_at']).to be_like_time(merge_request.metrics.merged_at) + expect(json_response['closed_by']['id']).to eq(merge_request.metrics.latest_closed_by_id) + expect(Time.parse json_response['closed_at']).to be_like_time(merge_request.metrics.latest_closed_at) + expect(json_response['pipeline']['id']).to eq(merge_request.metrics.pipeline_id) + expect(Time.parse json_response['latest_build_started_at']).to be_like_time(merge_request.metrics.latest_build_started_at) + expect(Time.parse json_response['latest_build_finished_at']).to be_like_time(merge_request.metrics.latest_build_finished_at) + expect(Time.parse json_response['first_deployed_to_production_at']).to be_like_time(merge_request.metrics.first_deployed_to_production_at) + end + end + it "returns a 404 error if merge_request_iid not found" do get api("/projects/#{project.id}/merge_requests/999", user) expect(response).to have_gitlab_http_status(404) diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 3a935d98540..6aed481939e 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -15,8 +15,8 @@ describe MergeRequests::BuildService do let(:target_branch) { 'master' } let(:merge_request) { service.execute } let(:compare) { double(:compare, commits: commits) } - let(:commit_1) { double(:commit_1, safe_message: "Initial commit\n\nCreate the app") } - let(:commit_2) { double(:commit_2, safe_message: 'This is a bad commit message!') } + let(:commit_1) { double(:commit_1, sha: 'f00ba7', safe_message: "Initial commit\n\nCreate the app") } + let(:commit_2) { double(:commit_2, sha: 'f00ba7', safe_message: 'This is a bad commit message!') } let(:commits) { nil } let(:service) do diff --git a/spec/support/cluster_application_spec.rb b/spec/support/cluster_application_spec.rb deleted file mode 100644 index ab77910a050..00000000000 --- a/spec/support/cluster_application_spec.rb +++ /dev/null @@ -1,105 +0,0 @@ -shared_examples 'cluster application specs' do - let(:factory_name) { described_class.to_s.downcase.gsub("::", "_") } - - describe '#name' do - it 'is .application_name' do - expect(subject.name).to eq(described_class.application_name) - end - - it 'is recorded in Clusters::Cluster::APPLICATIONS' do - expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class) - end - end - - describe '#status' do - let(:cluster) { create(:cluster, :provided_by_gcp) } - - subject { described_class.new(cluster: cluster) } - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - - context 'when application helm is scheduled' do - before do - create(factory_name, :scheduled, cluster: cluster) - end - - it 'defaults to :not_installable' do - expect(subject.status_name).to be(:not_installable) - end - end - - context 'when application helm is installed' do - before do - create(:clusters_applications_helm, :installed, cluster: cluster) - end - - it 'defaults to :installable' do - expect(subject.status_name).to be(:installable) - end - end - end - - describe '#install_command' do - it 'has all the needed information' do - expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false) - end - end - - describe 'status state machine' do - describe '#make_installing' do - subject { create(factory_name, :scheduled) } - - it 'is installing' do - subject.make_installing! - - expect(subject).to be_installing - end - end - - describe '#make_installed' do - subject { create(factory_name, :installing) } - - it 'is installed' do - subject.make_installed - - expect(subject).to be_installed - end - end - - describe '#make_errored' do - subject { create(factory_name, :installing) } - let(:reason) { 'some errors' } - - it 'is errored' do - subject.make_errored(reason) - - expect(subject).to be_errored - expect(subject.status_reason).to eq(reason) - end - end - - describe '#make_scheduled' do - subject { create(factory_name, :installable) } - - it 'is scheduled' do - subject.make_scheduled - - expect(subject).to be_scheduled - end - - describe 'when was errored' do - subject { create(factory_name, :errored) } - - it 'clears #status_reason' do - expect(subject.status_reason).not_to be_nil - - subject.make_scheduled! - - expect(subject.status_reason).to be_nil - end - end - end - end -end diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb new file mode 100644 index 00000000000..87d12a784ba --- /dev/null +++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb @@ -0,0 +1,70 @@ +shared_examples 'cluster application core specs' do |application_name| + it { is_expected.to belong_to(:cluster) } + it { is_expected.to validate_presence_of(:cluster) } + + describe '#name' do + it 'is .application_name' do + expect(subject.name).to eq(described_class.application_name) + end + + it 'is recorded in Clusters::Cluster::APPLICATIONS' do + expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class) + end + end + + describe 'status state machine' do + describe '#make_installing' do + subject { create(application_name, :scheduled) } + + it 'is installing' do + subject.make_installing! + + expect(subject).to be_installing + end + end + + describe '#make_installed' do + subject { create(application_name, :installing) } + + it 'is installed' do + subject.make_installed + + expect(subject).to be_installed + end + end + + describe '#make_errored' do + subject { create(application_name, :installing) } + let(:reason) { 'some errors' } + + it 'is errored' do + subject.make_errored(reason) + + expect(subject).to be_errored + expect(subject.status_reason).to eq(reason) + end + end + + describe '#make_scheduled' do + subject { create(application_name, :installable) } + + it 'is scheduled' do + subject.make_scheduled + + expect(subject).to be_scheduled + end + + describe 'when was errored' do + subject { create(application_name, :errored) } + + it 'clears #status_reason' do + expect(subject.status_reason).not_to be_nil + + subject.make_scheduled! + + expect(subject.status_reason).to be_nil + end + end + end + end +end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb new file mode 100644 index 00000000000..765dd32f4ba --- /dev/null +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -0,0 +1,31 @@ +shared_examples 'cluster application status specs' do |application_name| + describe '#status' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + subject { described_class.new(cluster: cluster) } + + it 'sets a default status' do + expect(subject.status_name).to be(:not_installable) + end + + context 'when application helm is scheduled' do + before do + create(:clusters_applications_helm, :scheduled, cluster: cluster) + end + + it 'defaults to :not_installable' do + expect(subject.status_name).to be(:not_installable) + end + end + + context 'when application is scheduled' do + before do + create(:clusters_applications_helm, :installed, cluster: cluster) + end + + it 'sets a default status' do + expect(subject.status_name).to be(:installable) + end + end + end +end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index e827a8da0b7..5e1ce19eafb 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -337,6 +337,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do before do chat_service.notify_only_default_branch = true + WebMock.stub_request(:post, webhook_url) end it 'does not call the Slack/Mattermost API for pipeline events' do @@ -345,6 +346,23 @@ RSpec.shared_examples 'slack or mattermost notifications' do expect(result).to be_falsy end + + it 'does not notify push events if they are not for the default branch' do + ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test" + push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, []) + + chat_service.execute(push_sample_data) + + expect(WebMock).not_to have_requested(:post, webhook_url) + end + + it 'notifies about push events for the default branch' do + push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user) + + chat_service.execute(push_sample_data) + + expect(WebMock).to have_requested(:post, webhook_url).once + end end context 'when disabled' do diff --git a/vendor/runner/values.yaml b/vendor/runner/values.yaml new file mode 100644 index 00000000000..b7e2e24acaf --- /dev/null +++ b/vendor/runner/values.yaml @@ -0,0 +1,25 @@ +## Configure the maximum number of concurrent jobs +## - Documentation: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section +## - Default value: 10 +## - Currently don't support auto-scaling. +concurrent: 4 + +## Defines in seconds how often to check GitLab for a new builds +## - Documentation: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section +## - Default value: 3 +checkInterval: 3 + +## For RBAC support +rbac: + create: false + clusterWideAccess: false + +## Configuration for the Pods that that the runner launches for each new job +## +runners: + image: ubuntu:16.04 + privileged: false + builds: {} + services: {} + helpers: {} +resources: {} |