diff options
author | Shinya Maeda <shinya@gitlab.com> | 2018-05-07 05:59:43 +0300 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2018-05-07 05:59:43 +0300 |
commit | 1f39fcd1123c1a65798a0a0b3e5f3b2fa43651ac (patch) | |
tree | 8d8a6a5a6a424c3f61332e509b97ab85cf0167b5 /app | |
parent | c1d3b48c96ce44a2ff3e84cb89063a00c67297f5 (diff) | |
parent | 58aa2d7f395be4aee38b5202ef1666879505c737 (diff) |
Merge branch 'master' into live-trace-v2
Diffstat (limited to 'app')
65 files changed, 971 insertions, 270 deletions
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index bb8b3d91e40..90d4e19e90b 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -30,7 +30,7 @@ export default class IssuableForm { } this.initAutosave(); - this.form.on('submit', this.handleSubmit); + this.form.on('submit:success', this.handleSubmit); this.form.on('click', '.btn-cancel', this.resetAutosave); this.initWip(); diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js index 9aa8945e268..b0b077a5e4c 100644 --- a/app/assets/javascripts/pages/projects/pipelines/new/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js @@ -1,6 +1,12 @@ import $ from 'jquery'; import NewBranchForm from '~/new_branch_form'; +import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list'; document.addEventListener('DOMContentLoaded', () => { new NewBranchForm($('.js-new-pipeline-form')); // eslint-disable-line no-new + + setupNativeFormVariableList({ + container: $('.js-ci-variable-list-section'), + formField: 'variables_attributes', + }); }); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 360dcb6afef..9bd35183d8a 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -61,3 +61,4 @@ @import 'framework/stacked_progress_bar'; @import 'framework/ci_variable_list'; @import 'framework/feature_highlight'; +@import 'framework/terms'; diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss new file mode 100644 index 00000000000..dadfaf1c3f9 --- /dev/null +++ b/app/assets/stylesheets/framework/terms.scss @@ -0,0 +1,55 @@ +.terms { + .alert-wrapper { + min-height: $header-height + $gl-padding; + } + + .content { + padding-top: $gl-padding; + } + + .panel { + .panel-heading { + display: -webkit-flex; + display: flex; + align-items: center; + justify-content: space-between; + + .title { + display: flex; + align-items: center; + + .logo-text { + width: 55px; + height: 24px; + display: flex; + flex-direction: column; + justify-content: center; + } + } + + .navbar-collapse { + padding-right: 0; + } + + .nav li a { + color: $theme-gray-700; + } + } + + .panel-content { + padding: $gl-padding; + + *:first-child { + margin-top: 0; + } + + *:last-child { + margin-bottom: 0; + } + } + + .footer-block { + margin: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index e74606e864f..888757c12d8 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -440,6 +440,7 @@ padding-right: 3px; .projects-sidebar { + min-height: 0; display: flex; flex-direction: column; flex: 1; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8ad13a82f89..2caffec66ac 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -13,12 +13,14 @@ class ApplicationController < ActionController::Base before_action :authenticate_sessionless_user! before_action :authenticate_user! + before_action :enforce_terms!, if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms }, + unless: :peek_request? before_action :validate_user_service_ticket! before_action :check_password_expiration before_action :ldap_security_check before_action :sentry_context before_action :default_headers - before_action :add_gon_variables, unless: -> { request.path.start_with?('/-/peek') } + before_action :add_gon_variables, unless: :peek_request? before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? @@ -269,6 +271,27 @@ class ApplicationController < ActionController::Base end end + def enforce_terms! + return unless current_user + return if current_user.terms_accepted? + + if sessionless_user? + render_403 + else + # Redirect to the destination if the request is a get. + # Redirect to the source if it was a post, so the user can re-submit after + # accepting the terms. + redirect_path = if request.get? + request.fullpath + else + URI(request.referer).path if request.referer + end + + flash[:notice] = _("Please accept the Terms of Service before continuing.") + redirect_to terms_path(redirect: redirect_path), status: :found + end + end + def import_sources_enabled? !Gitlab::CurrentSettings.import_sources.empty? end @@ -342,4 +365,12 @@ class ApplicationController < ActionController::Base # Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8 response.headers['Page-Title'] = URI.escape(page_title('GitLab')) end + + def sessionless_user? + current_user && !session.keys.include?('warden.user.user.key') + end + + def peek_request? + request.path.start_with?('/-/peek') + end end diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb index eb3a623acdd..8b7355974df 100644 --- a/app/controllers/concerns/continue_params.rb +++ b/app/controllers/concerns/continue_params.rb @@ -1,4 +1,5 @@ module ContinueParams + include InternalRedirect extend ActiveSupport::Concern def continue_params @@ -6,8 +7,7 @@ module ContinueParams return nil unless continue_params continue_params = continue_params.permit(:to, :notice, :notice_now) - return unless continue_params[:to] && continue_params[:to].start_with?('/') - return if continue_params[:to].start_with?('//') + continue_params[:to] = safe_redirect_path(continue_params[:to]) continue_params end diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb new file mode 100644 index 00000000000..7409b2e89a5 --- /dev/null +++ b/app/controllers/concerns/internal_redirect.rb @@ -0,0 +1,35 @@ +module InternalRedirect + extend ActiveSupport::Concern + + def safe_redirect_path(path) + return unless path + # Verify that the string starts with a `/` but not a double `/`. + return unless path =~ %r{^/\w.*$} + + uri = URI(path) + # Ignore anything path of the redirect except for the path, querystring and, + # fragment, forcing the redirect within the same host. + full_path_for_uri(uri) + rescue URI::InvalidURIError + nil + end + + def safe_redirect_path_for_url(url) + return unless url + + uri = URI(url) + safe_redirect_path(full_path_for_uri(uri)) if host_allowed?(uri) + rescue URI::InvalidURIError + nil + end + + def host_allowed?(uri) + uri.host == request.host && + uri.port == request.port + end + + def full_path_for_uri(uri) + path_with_query = [uri.path, uri.query].compact.join('?') + [path_with_query, uri.fragment].compact.join("#") + end +end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index c84fc2d305d..bcb856ce3f4 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -1,6 +1,17 @@ class Import::BaseController < ApplicationController private + def find_already_added_projects(import_type) + current_user.created_projects.where(import_type: import_type).includes(:import_state) + end + + def find_jobs(import_type) + current_user.created_projects + .includes(:import_state) + .where(import_type: import_type) + .to_json(only: [:id], methods: [:import_status]) + end + def find_or_create_namespace(names, owner) names = params[:target_namespace].presence || names diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 61d81ad8a71..77af5fb9c4f 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -22,16 +22,14 @@ class Import::BitbucketController < Import::BaseController @repos, @incompatible_repos = repos.partition { |repo| repo.valid? } - @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket') + @already_added_projects = find_already_added_projects('bitbucket') already_added_projects_names = @already_added_projects.pluck(:import_source) @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) } end def jobs - render json: current_user.created_projects - .where(import_type: 'bitbucket') - .to_json(only: [:id, :import_status]) + render json: find_jobs('bitbucket') end def create diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 669eb31a995..25ec13b8075 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -46,15 +46,14 @@ class Import::FogbugzController < Import::BaseController @repos = client.repos - @already_added_projects = current_user.created_projects.where(import_type: 'fogbugz') + @already_added_projects = find_already_added_projects('fogbugz') already_added_projects_names = @already_added_projects.pluck(:import_source) @repos.reject! { |repo| already_added_projects_names.include? repo.name } end def jobs - jobs = current_user.created_projects.where(import_type: 'fogbugz').to_json(only: [:id, :import_status]) - render json: jobs + render json: find_jobs('fogbugz') end def create diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index eb7d5fca367..f67ec4c248b 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -24,15 +24,14 @@ class Import::GithubController < Import::BaseController def status @repos = client.repos - @already_added_projects = current_user.created_projects.where(import_type: provider) + @already_added_projects = find_already_added_projects(provider) already_added_projects_names = @already_added_projects.pluck(:import_source) @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } end def jobs - jobs = current_user.created_projects.where(import_type: provider).to_json(only: [:id, :import_status]) - render json: jobs + render json: find_jobs(provider) end def create diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 18f1d20f5a9..39e2e9e094b 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -12,15 +12,14 @@ class Import::GitlabController < Import::BaseController def status @repos = client.projects - @already_added_projects = current_user.created_projects.where(import_type: "gitlab") + @already_added_projects = find_already_added_projects('gitlab') already_added_projects_names = @already_added_projects.pluck(:import_source) @repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] } end def jobs - jobs = current_user.created_projects.where(import_type: "gitlab").to_json(only: [:id, :import_status]) - render json: jobs + render json: find_jobs('gitlab') end def create diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index baa19fb383d..9b26a00f7c7 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -73,15 +73,14 @@ class Import::GoogleCodeController < Import::BaseController @repos = client.repos @incompatible_repos = client.incompatible_repos - @already_added_projects = current_user.created_projects.where(import_type: "google_code") + @already_added_projects = find_already_added_projects('google_code') already_added_projects_names = @already_added_projects.pluck(:import_source) @repos.reject! { |repo| already_added_projects_names.include? repo.name } end def jobs - jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status]) - render json: jobs + render json: find_jobs('google_code') end def create diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 40d9fa18a10..ed89bed029b 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -82,7 +82,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if identity_linker.changed? redirect_identity_linked - elsif identity_linker.error_message.present? + elsif identity_linker.failed? redirect_identity_link_failed(identity_linker.error_message) else redirect_identity_exists diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 78d109cf33e..1ee273091d4 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -157,7 +157,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def create_params - params.require(:pipeline).permit(:ref) + params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value]) end def pipeline diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index c950d0f7001..b9bbe7115c4 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -52,6 +52,12 @@ class Projects::RunnersController < Projects::ApplicationController redirect_to project_settings_ci_cd_path(@project) end + def toggle_group_runners + project.toggle_ci_cd_settings!(:group_runners_enabled) + + redirect_to project_settings_ci_cd_path(@project) + end + protected def set_runner diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index d80ef8113aa..177c8a54099 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -67,10 +67,18 @@ module Projects def define_runners_variables @project_runners = @project.runners.ordered - @assignable_runners = current_user.ci_authorized_runners - .assignable_for(project).ordered.page(params[:page]).per(20) + + @assignable_runners = current_user + .ci_authorized_runners + .assignable_for(project) + .ordered + .page(params[:page]).per(20) + @shared_runners = ::Ci::Runner.shared.active + @shared_runners_count = @shared_runners.count(:all) + + @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id) end def define_secret_variables diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index f3a4aa849c7..1a339f76d26 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,4 +1,5 @@ class SessionsController < Devise::SessionsController + include InternalRedirect include AuthenticatesWithTwoFactor include Devise::Controllers::Rememberable include Recaptcha::ClientHelper @@ -102,18 +103,12 @@ class SessionsController < Devise::SessionsController # we should never redirect to '/users/sign_in' after signing in successfully. return true if redirect_uri.path == new_user_session_path - redirect_to = redirect_uri.to_s if redirect_allowed_to?(redirect_uri) + redirect_to = redirect_uri.to_s if host_allowed?(redirect_uri) @redirect_to = redirect_to store_location_for(:redirect, redirect_to) end - # Overridden in EE - def redirect_allowed_to?(uri) - uri.host == Gitlab.config.gitlab.host && - uri.port == Gitlab.config.gitlab.port - end - def two_factor_enabled? find_user&.two_factor_enabled? end diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb new file mode 100644 index 00000000000..95c5c3432d5 --- /dev/null +++ b/app/controllers/users/terms_controller.rb @@ -0,0 +1,66 @@ +module Users + class TermsController < ApplicationController + include InternalRedirect + + skip_before_action :enforce_terms! + before_action :terms + + layout 'terms' + + def index + @redirect = redirect_path + end + + def accept + agreement = Users::RespondToTermsService.new(current_user, viewed_term) + .execute(accepted: true) + + if agreement.persisted? + redirect_to redirect_path + else + flash[:alert] = agreement.errors.full_messages.join(', ') + redirect_to terms_path, redirect: redirect_path + end + end + + def decline + agreement = Users::RespondToTermsService.new(current_user, viewed_term) + .execute(accepted: false) + + if agreement.persisted? + sign_out(current_user) + redirect_to root_path + else + flash[:alert] = agreement.errors.full_messages.join(', ') + redirect_to terms_path, redirect: redirect_path + end + end + + private + + def viewed_term + @viewed_term ||= ApplicationSetting::Term.find(params[:id]) + end + + def terms + unless @term = Gitlab::CurrentSettings.current_application_settings.latest_terms + redirect_to redirect_path + end + end + + def redirect_path + redirect_to_path = safe_redirect_path(params[:redirect]) || safe_redirect_path_for_url(request.referer) + + if redirect_to_path && + excluded_redirect_paths.none? { |excluded| redirect_to_path.include?(excluded) } + redirect_to_path + else + root_path + end + end + + def excluded_redirect_paths + [terms_path, new_user_session_path] + end + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 3fbb32c5229..1bf98d550b0 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -248,7 +248,9 @@ module ApplicationSettingsHelper :user_default_external, :user_oauth_applications, :version_check_enabled, - :allow_local_requests_from_hooks_and_services + :allow_local_requests_from_hooks_and_services, + :enforce_terms, + :terms ] end end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 01af68088df..e803cd3a8d8 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -23,9 +23,42 @@ module UsersHelper profile_tabs.include?(tab) end + def current_user_menu_items + @current_user_menu_items ||= get_current_user_menu_items + end + + def current_user_menu?(item) + current_user_menu_items.include?(item) + end + private def get_profile_tabs [:activity, :groups, :contributed, :projects, :snippets] end + + def get_current_user_menu_items + items = [] + + items << :sign_out if current_user + + # TODO: Remove these conditions when the permissions are prevented in + # https://gitlab.com/gitlab-org/gitlab-ce/issues/45849 + terms_not_enforced = !Gitlab::CurrentSettings + .current_application_settings + .enforce_terms? + required_terms_accepted = terms_not_enforced || current_user.terms_accepted? + + items << :help if required_terms_accepted + + if can?(current_user, :read_user, current_user) && required_terms_accepted + items << :profile + end + + if can?(current_user, :update_user, current_user) && required_terms_accepted + items << :settings + end + + items + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 862933bf127..a734cc7a26b 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -220,12 +220,15 @@ class ApplicationSetting < ActiveRecord::Base end end + validate :terms_exist, if: :enforce_terms? + before_validation :ensure_uuid! before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token after_commit do + reset_memoized_terms Rails.cache.write(CACHE_KEY, self) end @@ -507,6 +510,16 @@ class ApplicationSetting < ActiveRecord::Base password_authentication_enabled_for_web? || password_authentication_enabled_for_git? end + delegate :terms, to: :latest_terms, allow_nil: true + def latest_terms + @latest_terms ||= Term.latest + end + + def reset_memoized_terms + @latest_terms = nil + latest_terms + end + private def ensure_uuid! @@ -520,4 +533,10 @@ class ApplicationSetting < ActiveRecord::Base errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless invalid.empty? end + + def terms_exist + return unless enforce_terms? + + errors.add(:terms, "You need to set terms to be enforced") unless terms.present? + end end diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb new file mode 100644 index 00000000000..e8ce0ccbb71 --- /dev/null +++ b/app/models/application_setting/term.rb @@ -0,0 +1,13 @@ +class ApplicationSetting + class Term < ActiveRecord::Base + include CacheMarkdownField + + validates :terms, presence: true + + cache_markdown_field :terms + + def self.latest + order(:id).last + end + end +end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e1b9bc76475..c184f398f6c 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -32,6 +32,8 @@ module Ci has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id' + accepts_nested_attributes_for :variables, reject_if: :persisted? + delegate :id, to: :project, prefix: true delegate :full_path, to: :project, prefix: true diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index de5aae17a15..38e14ffbc0c 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -5,6 +5,8 @@ module Ci belongs_to :pipeline + alias_attribute :secret_value, :value + validates :key, uniqueness: { scope: :pipeline_id } end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 5a4c56ec0dc..23078f1c3ed 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -14,31 +14,49 @@ module Ci has_many :builds has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, through: :runner_projects + has_many :runner_namespaces + has_many :groups, through: :runner_namespaces has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' before_validation :set_default_values - scope :specific, ->() { where(is_shared: false) } - scope :shared, ->() { where(is_shared: true) } - scope :active, ->() { where(active: true) } - scope :paused, ->() { where(active: false) } - scope :online, ->() { where('contacted_at > ?', contact_time_deadline) } - scope :ordered, ->() { order(id: :desc) } + scope :specific, -> { where(is_shared: false) } + scope :shared, -> { where(is_shared: true) } + scope :active, -> { where(active: true) } + scope :paused, -> { where(active: false) } + scope :online, -> { where('contacted_at > ?', contact_time_deadline) } + scope :ordered, -> { order(id: :desc) } - scope :owned_or_shared, ->(project_id) do - joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') - .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) + scope :belonging_to_project, -> (project_id) { + joins(:runner_projects).where(ci_runner_projects: { project_id: project_id }) + } + + scope :belonging_to_parent_group_of_project, -> (project_id) { + project_groups = ::Group.joins(:projects).where(projects: { id: project_id }) + hierarchy_groups = Gitlab::GroupHierarchy.new(project_groups).base_and_ancestors + + joins(:groups).where(namespaces: { id: hierarchy_groups }) + } + + scope :owned_or_shared, -> (project_id) do + union = Gitlab::SQL::Union.new( + [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), shared], + remove_duplicates: false + ) + from("(#{union.to_sql}) ci_runners") end scope :assignable_for, ->(project) do # FIXME: That `to_sql` is needed to workaround a weird Rails bug. # Without that, placeholders would miss one and couldn't match. where(locked: false) - .where.not("id IN (#{project.runners.select(:id).to_sql})").specific + .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})") + .specific end validate :tag_constraints + validate :either_projects_or_group validates :access_level, presence: true acts_as_taggable @@ -50,6 +68,12 @@ module Ci ref_protected: 1 } + enum runner_type: { + instance_type: 1, + group_type: 2, + project_type: 3 + } + cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout @@ -120,6 +144,14 @@ module Ci !shared? end + def assigned_to_group? + runner_namespaces.any? + end + + def assigned_to_project? + runner_projects.any? + end + def can_pick?(build) return false if self.ref_protected? && !build.protected? @@ -174,6 +206,12 @@ module Ci end end + def pick_build!(build) + if can_pick?(build) + tick_runner_queue + end + end + private def cleanup_runner_queue @@ -205,7 +243,17 @@ module Ci end def assignable_for?(project_id) - is_shared? || projects.exists?(id: project_id) + self.class.owned_or_shared(project_id).where(id: self.id).any? + end + + def either_projects_or_group + if groups.many? + errors.add(:runner, 'can only be assigned to one group') + end + + if assigned_to_group? && assigned_to_project? + errors.add(:runner, 'can only be assigned either to projects or to a group') + end end def accepting_tags?(build) diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb new file mode 100644 index 00000000000..3269f86e8ca --- /dev/null +++ b/app/models/ci/runner_namespace.rb @@ -0,0 +1,9 @@ +module Ci + class RunnerNamespace < ActiveRecord::Base + extend Gitlab::Ci::Model + + belongs_to :runner + belongs_to :namespace, class_name: '::Namespace' + belongs_to :group, class_name: '::Group', foreign_key: :namespace_id + end +end diff --git a/app/models/group.rb b/app/models/group.rb index 9b42bbf99be..f493836a92e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -9,6 +9,7 @@ class Group < Namespace include SelectForProjectAuthorization include LoadedInGroupList include GroupDescendant + include TokenAuthenticatable has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members @@ -43,6 +44,8 @@ class Group < Namespace validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } + add_authentication_token_field :runners_token + after_create :post_create_hook after_destroy :post_destroy_hook after_save :update_two_factor_requirement @@ -294,6 +297,13 @@ class Group < Namespace refresh_members_authorized_projects(blocking: false) end + # each existing group needs to have a `runners_token`. + # we do this on read since migrating all existing groups is not a feasible + # solution. + def runners_token + ensure_runners_token! + end + private def update_two_factor_requirement diff --git a/app/models/namespace.rb b/app/models/namespace.rb index c29a53e5ce7..5621eeba7c4 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -21,6 +21,9 @@ class Namespace < ActiveRecord::Base has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :project_statistics + has_many :runner_namespaces, class_name: 'Ci::RunnerNamespace' + has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner' + # This should _not_ be `inverse_of: :namespace`, because that would also set # `user.namespace` when this user creates a group with themselves as `owner`. belongs_to :owner, class_name: "User" diff --git a/app/models/project.rb b/app/models/project.rb index d4e9e51c7be..b12b694aabd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -67,6 +67,9 @@ class Project < ActiveRecord::Base before_save :ensure_runners_token after_save :update_project_statistics, if: :namespace_id_changed? + + after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? } + after_create :create_project_feature, unless: :project_feature after_create :create_ci_cd_settings, @@ -157,6 +160,8 @@ class Project < ActiveRecord::Base has_one :fork_network_member has_one :fork_network, through: :fork_network_member + has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project + # Merge Requests for target project should be removed with it has_many :merge_requests, foreign_key: 'target_project_id' has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest' @@ -230,13 +235,11 @@ class Project < ActiveRecord::Base has_many :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens - has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' - has_one :auto_devops, class_name: 'ProjectAutoDevops' has_many :custom_attributes, class_name: 'ProjectCustomAttribute' has_many :project_badges, class_name: 'ProjectBadge' - has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting' + has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true @@ -247,6 +250,7 @@ class Project < ActiveRecord::Base delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team + delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings # Validations validates :creator, presence: true, on: :create @@ -332,6 +336,11 @@ class Project < ActiveRecord::Base scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) } scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) } + scope :with_group_runners_enabled, -> do + joins(:ci_cd_settings) + .where(project_ci_cd_settings: { group_runners_enabled: true }) + end + enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } chronic_duration_attr :build_timeout_human_readable, :build_timeout, default: 3600 @@ -381,55 +390,9 @@ class Project < ActiveRecord::Base scope :abandoned, -> { where('projects.last_activity_at < ?', 6.months.ago) } scope :excluding_project, ->(project) { where.not(id: project) } - scope :import_started, -> { where(import_status: 'started') } - state_machine :import_status, initial: :none do - event :import_schedule do - transition [:none, :finished, :failed] => :scheduled - end - - event :force_import_start do - transition [:none, :finished, :failed] => :started - end - - event :import_start do - transition scheduled: :started - end - - event :import_finish do - transition started: :finished - end - - event :import_fail do - transition [:scheduled, :started] => :failed - end - - event :import_retry do - transition failed: :started - end - - state :scheduled - state :started - state :finished - state :failed - - after_transition [:none, :finished, :failed] => :scheduled do |project, _| - project.run_after_commit do - job_id = add_import_job - update(import_jid: job_id) if job_id - end - end - - after_transition started: :finished do |project, _| - project.reset_cache_and_import_attrs - - if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists? - project.run_after_commit do - Projects::AfterImportService.new(project).execute - end - end - end - end + scope :joins_import_state, -> { joins("LEFT JOIN project_mirror_data import_state ON import_state.project_id = projects.id") } + scope :import_started, -> { joins_import_state.where("import_state.status = 'started' OR projects.import_status = 'started'") } class << self # Searches for a list of projects based on the query given in `query`. @@ -659,10 +622,6 @@ class Project < ActiveRecord::Base external_import? || forked? || gitlab_project_import? || bare_repository_import? end - def no_import? - import_status == 'none' - end - def external_import? import_url.present? end @@ -675,6 +634,93 @@ class Project < ActiveRecord::Base import_started? || import_scheduled? end + def import_state_args + { + status: self[:import_status], + jid: self[:import_jid], + last_error: self[:import_error] + } + end + + def ensure_import_state + return if self[:import_status] == 'none' || self[:import_status].nil? + return unless import_state.nil? + + create_import_state(import_state_args) + + update_column(:import_status, 'none') + end + + def import_schedule + ensure_import_state + + import_state&.schedule + end + + def force_import_start + ensure_import_state + + import_state&.force_start + end + + def import_start + ensure_import_state + + import_state&.start + end + + def import_fail + ensure_import_state + + import_state&.fail_op + end + + def import_finish + ensure_import_state + + import_state&.finish + end + + def import_jid=(new_jid) + ensure_import_state + + import_state&.jid = new_jid + end + + def import_jid + ensure_import_state + + import_state&.jid + end + + def import_error=(new_error) + ensure_import_state + + import_state&.last_error = new_error + end + + def import_error + ensure_import_state + + import_state&.last_error + end + + def import_status=(new_status) + ensure_import_state + + import_state&.status = new_status + end + + def import_status + ensure_import_state + + import_state&.status || 'none' + end + + def no_import? + import_status == 'none' + end + def import_started? # import? does SQL work so only run it if it looks like there's an import running import_status == 'started' && import? @@ -1301,12 +1347,17 @@ class Project < ActiveRecord::Base @shared_runners ||= shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none end - def active_shared_runners - @active_shared_runners ||= shared_runners.active + def group_runners + @group_runners ||= group_runners_enabled? ? Ci::Runner.belonging_to_parent_group_of_project(self.id) : Ci::Runner.none + end + + def all_runners + union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners]) + Ci::Runner.from("(#{union.to_sql}) ci_runners") end def any_runners?(&block) - active_runners.any?(&block) || active_shared_runners.any?(&block) + all_runners.active.any?(&block) end def valid_runners_token?(token) @@ -1471,7 +1522,7 @@ class Project < ActiveRecord::Base def rename_repo_notify! # When we import a project overwriting the original project, there # is a move operation. In that case we don't want to send the instructions. - send_move_instructions(full_path_was) unless started? + send_move_instructions(full_path_was) unless import_started? expires_full_path_cache self.old_path_with_namespace = full_path_was @@ -1525,7 +1576,8 @@ class Project < ActiveRecord::Base return unless import_jid Gitlab::SidekiqStatus.unset(import_jid) - update_column(:import_jid, nil) + + import_state.update_column(:jid, nil) end def running_or_pending_build_count(force: false) @@ -1544,7 +1596,8 @@ class Project < ActiveRecord::Base sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message) import_fail - update_column(:import_error, sanitized_message) + + import_state.update_column(:last_error, sanitized_message) rescue ActiveRecord::ActiveRecordError => e Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}") ensure @@ -1874,6 +1927,10 @@ class Project < ActiveRecord::Base [] end + def toggle_ci_cd_settings!(settings_attribute) + ci_cd_settings.toggle!(settings_attribute) + end + def gitlab_deploy_token @gitlab_deploy_token ||= deploy_tokens.gitlab_deploy_token end diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index 9f10a93148c..588cced5781 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -1,5 +1,5 @@ class ProjectCiCdSetting < ActiveRecord::Base - belongs_to :project + belongs_to :project, inverse_of: :ci_cd_settings # The version of the schema that first introduced this model/table. MINIMUM_SCHEMA_VERSION = 20180403035759 diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb new file mode 100644 index 00000000000..1605317ae14 --- /dev/null +++ b/app/models/project_import_state.rb @@ -0,0 +1,55 @@ +class ProjectImportState < ActiveRecord::Base + include AfterCommitQueue + + self.table_name = "project_mirror_data" + + belongs_to :project, inverse_of: :import_state + + validates :project, presence: true + + state_machine :status, initial: :none do + event :schedule do + transition [:none, :finished, :failed] => :scheduled + end + + event :force_start do + transition [:none, :finished, :failed] => :started + end + + event :start do + transition scheduled: :started + end + + event :finish do + transition started: :finished + end + + event :fail_op do + transition [:scheduled, :started] => :failed + end + + state :scheduled + state :started + state :finished + state :failed + + after_transition [:none, :finished, :failed] => :scheduled do |state, _| + state.run_after_commit do + job_id = project.add_import_job + update(jid: job_id) if job_id + end + end + + after_transition started: :finished do |state, _| + project = state.project + + project.reset_cache_and_import_attrs + + if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists? + state.run_after_commit do + Projects::AfterImportService.new(project).execute + end + end + end + end +end diff --git a/app/models/term_agreement.rb b/app/models/term_agreement.rb new file mode 100644 index 00000000000..8458a231bbd --- /dev/null +++ b/app/models/term_agreement.rb @@ -0,0 +1,6 @@ +class TermAgreement < ActiveRecord::Base + belongs_to :term, class_name: 'ApplicationSetting::Term' + belongs_to :user + + validates :user, :term, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index 4a602ffbb05..a9cfd39f604 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -138,6 +138,8 @@ class User < ActiveRecord::Base has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'UserCallout' has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :term_agreements + belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' # # Validations @@ -1187,6 +1189,10 @@ class User < ActiveRecord::Base max_member_access_for_group_ids([group_id])[group_id] end + def terms_accepted? + accepted_term_id.present? + end + protected # override, from Devise::Validatable diff --git a/app/policies/application_setting/term_policy.rb b/app/policies/application_setting/term_policy.rb new file mode 100644 index 00000000000..f03bf748c76 --- /dev/null +++ b/app/policies/application_setting/term_policy.rb @@ -0,0 +1,28 @@ +class ApplicationSetting + class TermPolicy < BasePolicy + include Gitlab::Utils::StrongMemoize + + condition(:current_terms, scope: :subject) do + Gitlab::CurrentSettings.current_application_settings.latest_terms == @subject + end + + condition(:terms_accepted, score: 1) do + agreement&.accepted + end + + rule { ~anonymous & current_terms }.policy do + enable :accept_terms + enable :decline_terms + end + + rule { terms_accepted }.prevent :accept_terms + + def agreement + strong_memoize(:agreement) do + next nil if @user.nil? || @subject.nil? + + @user.term_agreements.find_by(term: @subject) + end + end + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 0905ddd9b38..ee219f0a0d0 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -8,6 +8,8 @@ class UserPolicy < BasePolicy rule { ~restricted_public_level }.enable :read_user rule { ~anonymous }.enable :read_user - rule { user_is_self | admin }.enable :destroy_user - rule { subject_ghost }.prevent :destroy_user + rule { ~subject_ghost & (user_is_self | admin) }.policy do + enable :destroy_user + enable :update_user + end end diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index 61589a07250..d6d3a661dab 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -1,7 +1,22 @@ module ApplicationSettings class UpdateService < ApplicationSettings::BaseService def execute + update_terms(@params.delete(:terms)) + @application_setting.update(@params) end + + private + + def update_terms(terms) + return unless terms.present? + + # Avoid creating a new terms record if the text is exactly the same. + terms = terms.strip + return if terms == @application_setting.terms + + ApplicationSetting::Term.create(terms: terms) + @application_setting.reset_memoized_terms + end end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 6ce86983287..17a53b6a8fd 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -24,6 +24,7 @@ module Ci ignore_skip_ci: ignore_skip_ci, save_incompleted: save_on_errors, seeds_block: block, + variables_attributes: params[:variables_attributes], project: project, current_user: current_user) diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 0b087ad73da..4291631913a 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -17,8 +17,10 @@ module Ci builds = if runner.shared? builds_for_shared_runner + elsif runner.group_type? + builds_for_group_runner else - builds_for_specific_runner + builds_for_project_runner end valid = true @@ -75,15 +77,24 @@ module Ci .joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id') .where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). - # Implement fair scheduling - # this returns builds that are ordered by number of running builds - # we prefer projects that don't use shared runners at all - joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id") + # Implement fair scheduling + # this returns builds that are ordered by number of running builds + # we prefer projects that don't use shared runners at all + joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id") .order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') end - def builds_for_specific_runner - new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('created_at ASC') + def builds_for_project_runner + new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC') + end + + def builds_for_group_runner + hierarchy_groups = Gitlab::GroupHierarchy.new(runner.groups).base_and_descendants + projects = Project.where(namespace_id: hierarchy_groups) + .with_group_runners_enabled + .with_builds_enabled + .without_deleted + new_builds.where(project: projects).order('id ASC') end def running_builds_for_shared_runners @@ -97,10 +108,6 @@ module Ci builds end - def shared_runner_build_limits_feature_enabled? - ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true' - end - def register_failure failed_attempt_counter.increment attempt_counter.increment diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb index 152c8ae5006..41b1c144c3e 100644 --- a/app/services/ci/update_build_queue_service.rb +++ b/app/services/ci/update_build_queue_service.rb @@ -1,18 +1,14 @@ module Ci class UpdateBuildQueueService def execute(build) - build.project.runners.each do |runner| - if runner.can_pick?(build) - runner.tick_runner_queue - end - end + tick_for(build, build.project.all_runners) + end - return unless build.project.shared_runners_enabled? + private - Ci::Runner.shared.each do |runner| - if runner.can_pick?(build) - runner.tick_runner_queue - end + def tick_for(build, runners) + runners.each do |runner| + runner.pick_build!(build) end end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index d361d070993..d16ecdb7b9b 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -142,7 +142,7 @@ module Projects if @project @project.errors.add(:base, message) - @project.mark_import_as_failed(message) if @project.import? + @project.mark_import_as_failed(message) if @project.persisted? && @project.import? end @project diff --git a/app/services/users/respond_to_terms_service.rb b/app/services/users/respond_to_terms_service.rb new file mode 100644 index 00000000000..06d660186cf --- /dev/null +++ b/app/services/users/respond_to_terms_service.rb @@ -0,0 +1,24 @@ +module Users + class RespondToTermsService + def initialize(user, term) + @user, @term = user, term + end + + def execute(accepted:) + agreement = @user.term_agreements.find_or_initialize_by(term: @term) + agreement.accepted = accepted + + if agreement.save + store_accepted_term(accepted) + end + + agreement + end + + private + + def store_accepted_term(accepted) + @user.update_column(:accepted_term_id, accepted ? @term.id : nil) + end + end +end diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 809ce1303d8..7ec52b6ce2b 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -41,7 +41,7 @@ class WebHookService http_status: response.code, message: response.to_s } - rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout => e + rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout, Gitlab::HTTP::BlockedUrlError => e log_execution( trigger: hook_name, url: hook.url, diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml new file mode 100644 index 00000000000..724246ab7e7 --- /dev/null +++ b/app/views/admin/application_settings/_terms.html.haml @@ -0,0 +1,22 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-12 + .checkbox + = f.label :enforce_terms do + = f.check_box :enforce_terms + = _("Require all users to accept Terms of Service when they access GitLab.") + .help-block + = _("When enabled, users cannot use GitLab until the terms have been accepted.") + .form-group + .col-sm-12 + = f.label :terms do + = _("Terms of Service Agreement") + .col-sm-12 + = f.text_area :terms, class: 'form-control', rows: 8 + .help-block + = _("Markdown enabled") + + = f.submit _("Save changes"), class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index caaa93aa1e2..3c00e3c8fc4 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -8,7 +8,7 @@ %h4 = _('Visibility and access controls') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p = _('Set default and restrict visibility levels. Configure import sources and git access protocol.') .settings-content @@ -19,7 +19,7 @@ %h4 = _('Account and limit settings') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p = _('Session expiration, projects limit and attachment size.') .settings-content @@ -30,7 +30,7 @@ %h4 = _('Sign-up restrictions') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p = _('Configure the way a user creates a new account.') .settings-content @@ -41,18 +41,29 @@ %h4 = _('Sign-in restrictions') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p = _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.') .settings-content = render 'signin' +%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Terms of Service') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = _('Include a Terms of Service agreement that all users must accept.') + .settings-content + = render 'terms' + %section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded) } .settings-header %h4 = _('Help page') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Help page text and support page url.') .settings-content @@ -62,8 +73,8 @@ .settings-header %h4 = _('Pages') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Size and domain settings for static websites') .settings-content @@ -73,8 +84,8 @@ .settings-header %h4 = _('Continuous Integration and Deployment') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Auto DevOps, runners and job artifacts') .settings-content @@ -84,8 +95,8 @@ .settings-header %h4 = _('Metrics - Influx') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Enable and configure InfluxDB metrics.') .settings-content @@ -95,8 +106,8 @@ .settings-header %h4 = _('Metrics - Prometheus') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Enable and configure Prometheus metrics.') .settings-content @@ -106,8 +117,8 @@ .settings-header %h4 = _('Profiling - Performance bar') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Enable the Performance Bar for a given group.') = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar') @@ -118,8 +129,8 @@ .settings-header %h4 = _('Background jobs') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Configure Sidekiq job throttling.') .settings-content @@ -129,8 +140,8 @@ .settings-header %h4 = _('Spam and Anti-bot Protection') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Enable reCAPTCHA or Akismet and set IP limits.') .settings-content @@ -140,8 +151,8 @@ .settings-header %h4 = _('Abuse reports') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Set notification email for abuse reports.') .settings-content @@ -151,8 +162,8 @@ .settings-header %h4 = _('Error Reporting and Logging') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Enable Sentry for error reporting and logging.') .settings-content @@ -162,8 +173,8 @@ .settings-header %h4 = _('Repository storage') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Configure storage path and circuit breaker settings.') .settings-content @@ -173,8 +184,8 @@ .settings-header %h4 = _('Repository maintenance') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Configure automatic git checks and housekeeping on repositories.') .settings-content @@ -185,8 +196,8 @@ .settings-header %h4 = _('Container Registry') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Various container registry settings.') .settings-content @@ -197,8 +208,8 @@ .settings-header %h4 = _('Koding') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Online IDE integration settings.') .settings-content @@ -208,8 +219,8 @@ .settings-header %h4 = _('PlantUML') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Allow rendering of PlantUML diagrams in Asciidoc documents.') .settings-content @@ -219,8 +230,8 @@ .settings-header#usage-statistics %h4 = _('Usage statistics') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Enable or disable version check and usage ping.') .settings-content @@ -230,8 +241,8 @@ .settings-header %h4 = _('Email') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Various email settings.') .settings-content @@ -241,8 +252,8 @@ .settings-header %h4 = _('Gitaly') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Configure Gitaly timeouts.') .settings-content @@ -252,8 +263,8 @@ .settings-header %h4 = _('Web terminal') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Set max session time for web terminal.') .settings-content @@ -263,8 +274,8 @@ .settings-header %h4 = _('Real-time features') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Change this value to influence how frequently the GitLab UI polls for updates.') .settings-content @@ -274,8 +285,8 @@ .settings-header %h4 = _('Performance optimization') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Various settings that affect GitLab performance.') .settings-content @@ -285,8 +296,8 @@ .settings-header %h4 = _('User and IP Rate Limits') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Configure limits for web and API requests.') .settings-content @@ -296,8 +307,8 @@ .settings-header %h4 = _('Outbound requests') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') %p = _('Allow requests to the local network from hooks and services.') .settings-content diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index f90b8b8c0a4..6e76e7c2768 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -2,6 +2,8 @@ %td - if runner.shared? %span.label.label-success shared + - elsif runner.group_type? + %span.label.label-success group - else %span.label.label-info specific - if runner.locked? @@ -19,7 +21,7 @@ %td = runner.ip_address %td - - if runner.shared? + - if runner.shared? || runner.group_type? n/a - else = runner.projects.count(:all) @@ -31,7 +33,7 @@ = tag %td - if runner.contacted_at - = time_ago_with_tooltip runner.contacted_at + #{time_ago_in_words(runner.contacted_at)} ago - else Never %td.admin-runner-btn-group-cell diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 9f13dbbbd82..1a3b5e58ed5 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -17,6 +17,9 @@ %span.label.label-success shared \- Runner runs jobs from all unassigned projects %li + %span.label.label-success group + \- Runner runs jobs from all unassigned projects in its group + %li %span.label.label-info specific \- Runner runs jobs from assigned projects %li diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index d04cf48b05c..d022016f70d 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -19,6 +19,9 @@ %p If you want Runners to build only specific projects, enable them in the table below. Keep in mind that this is a one way transition. +- elsif @runner.group_type? + .bs-callout.bs-callout-success + %h4 This runner will process jobs from all projects in its group and subgroups - else .bs-callout.bs-callout-info %h4 This Runner will process jobs only from ASSIGNED projects diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 0ef4b71f4fe..10b8bf5d565 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -42,31 +42,31 @@ = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do = link_to admin_users_path do Active - %small.badge= number_with_delimiter(User.active.count) + %small.badge= limited_counter_with_delimiter(User.active) = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do = link_to admin_users_path(filter: "admins") do Admins - %small.badge= number_with_delimiter(User.admins.count) + %small.badge= limited_counter_with_delimiter(User.admins) = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do = link_to admin_users_path(filter: 'two_factor_enabled') do 2FA Enabled - %small.badge= number_with_delimiter(User.with_two_factor.count) + %small.badge= limited_counter_with_delimiter(User.with_two_factor) = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do = link_to admin_users_path(filter: 'two_factor_disabled') do 2FA Disabled - %small.badge= number_with_delimiter(User.without_two_factor.count) + %small.badge= limited_counter_with_delimiter(User.without_two_factor) = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do = link_to admin_users_path(filter: 'external') do External - %small.badge= number_with_delimiter(User.external.count) + %small.badge= limited_counter_with_delimiter(User.external) = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do = link_to admin_users_path(filter: "blocked") do Blocked - %small.badge= number_with_delimiter(User.blocked.count) + %small.badge= limited_counter_with_delimiter(User.blocked) = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do = link_to admin_users_path(filter: "wop") do Without projects - %small.badge= number_with_delimiter(User.without_projects.count) + %small.badge= limited_counter_with_delimiter(User.without_projects) %ul.flex-list.content-list - if @users.empty? diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index 05ddd0ec733..8bd5708d490 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -1,8 +1,10 @@ +- extra_flash_class = local_assigns.fetch(:extra_flash_class, nil) + .flash-container.flash-container-page -# We currently only support `alert`, `notice`, `success` - flash.each do |key, value| -# Don't show a flash message if the message is nil - if value %div{ class: "flash-#{key}" } - %div{ class: (container_class) } + %div{ class: "#{container_class} #{extra_flash_class}" } %span= value diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml new file mode 100644 index 00000000000..24b6c490a5a --- /dev/null +++ b/app/views/layouts/header/_current_user_dropdown.html.haml @@ -0,0 +1,22 @@ +- return unless current_user + +%ul + %li.current-user + .user-name.bold + = current_user.name + = current_user.to_reference + %li.divider + - if current_user_menu?(:profile) + %li + = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } + - if current_user_menu?(:settings) + %li + = link_to s_("CurrentUser|Settings"), profile_path + - if current_user_menu?(:help) + %li + = link_to _("Help"), help_path + - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) + %li.divider + - if current_user_menu?(:sign_out) + %li + = link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index e6238c0dddb..dc121812406 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -53,22 +53,7 @@ = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu-nav.dropdown-menu-align-right - %ul - %li.current-user - .user-name.bold - = current_user.name - @#{current_user.username} - %li.divider - %li - = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } - %li - = link_to "Settings", profile_path - - if current_user - %li - = link_to "Help", help_path - %li.divider - %li - = link_to "Sign out", destroy_user_session_path, class: "sign-out-link" + = render 'layouts/header/current_user_dropdown' - if header_link?(:admin_impersonation) %li.impersonation = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml new file mode 100644 index 00000000000..a30d6e2688c --- /dev/null +++ b/app/views/layouts/terms.html.haml @@ -0,0 +1,34 @@ +!!! 5 +- @hide_breadcrumbs = true +%html{ lang: I18n.locale, class: page_class } + = render "layouts/head" + + %body{ data: { page: body_data_page } } + .layout-page.terms{ class: page_class } + .content-wrapper.prepend-top-0 + .mobile-overlay + .alert-wrapper + = render "layouts/broadcast" + = render 'layouts/header/read_only_banner' + = render "layouts/flash", extra_flash_class: 'limit-container-width' + + %div{ class: "#{container_class} limit-container-width" } + .content{ id: "content-body" } + .panel.panel-default + .panel-heading + .title + = brand_header_logo + - logo_text = brand_header_logo_type + - if logo_text.present? + %span.logo-text.hidden-xs.prepend-left-8 + = logo_text + - if header_link?(:user_dropdown) + .navbar-collapse.collapse + %ul.nav.navbar-nav + %li.header-user.dropdown + = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do + = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" + = sprite_icon('angle-down', css_class: 'caret-down') + .dropdown-menu-nav.dropdown-menu-align-right + = render 'layouts/header/current_user_dropdown' + = yield diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml new file mode 100644 index 00000000000..4bee6cb97eb --- /dev/null +++ b/app/views/projects/_import_project_pane.html.haml @@ -0,0 +1,51 @@ +- active_tab = local_assigns.fetch(:active_tab, 'blank') +- f = local_assigns.fetch(:f) + +.project-import.row + .col-lg-12 + .form-group.import-btn-container.clearfix + = f.label :visibility_level, class: 'label-light' do #the label here seems wrong + Import project from + .import-buttons + - if gitlab_project_import_enabled? + .import_gitlab_project.has-tooltip{ data: { container: 'body' } } + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do + = icon('gitlab', text: 'GitLab export') + %div + - if github_import_enabled? + = link_to new_import_github_path, class: 'btn js-import-github' do + = icon('github', text: 'GitHub') + %div + - if bitbucket_import_enabled? + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do + = icon('bitbucket', text: 'Bitbucket') + - unless bitbucket_import_configured? + = render 'bitbucket_import_modal' + %div + - if gitlab_import_enabled? + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do + = icon('gitlab', text: 'GitLab.com') + - unless gitlab_import_configured? + = render 'gitlab_import_modal' + %div + - if google_code_import_enabled? + = link_to new_import_google_code_path, class: 'btn import_google_code' do + = icon('google', text: 'Google Code') + %div + - if fogbugz_import_enabled? + = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do + = icon('bug', text: 'Fogbugz') + %div + - if gitea_import_enabled? + = link_to new_import_gitea_path, class: 'btn import_gitea' do + = custom_icon('go_logo') + Gitea + %div + - if git_import_enabled? + %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } + = icon('git', text: 'Repo by URL') + .col-lg-12 + .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } + %hr + = render "shared/import_form", f: f + = render 'new_project_fields', f: f, project_name_id: "import-url-name" diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index b66e0559603..5beaa3c6d23 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -57,54 +57,11 @@ .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' } = form_for @project, html: { class: 'new_project' } do |f| - if import_sources_enabled? - .project-import.row - .col-lg-12 - .form-group.import-btn-container.clearfix - = f.label :visibility_level, class: 'label-light' do #the label here seems wrong - Import project from - .import-buttons - - if gitlab_project_import_enabled? - .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - = icon('gitlab', text: 'GitLab export') - %div - - if github_import_enabled? - = link_to new_import_github_path, class: 'btn js-import-github' do - = icon('github', text: 'GitHub') - %div - - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do - = icon('bitbucket', text: 'Bitbucket') - - unless bitbucket_import_configured? - = render 'bitbucket_import_modal' - %div - - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do - = icon('gitlab', text: 'GitLab.com') - - unless gitlab_import_configured? - = render 'gitlab_import_modal' - %div - - if google_code_import_enabled? - = link_to new_import_google_code_path, class: 'btn import_google_code' do - = icon('google', text: 'Google Code') - %div - - if fogbugz_import_enabled? - = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - = icon('bug', text: 'Fogbugz') - %div - - if gitea_import_enabled? - = link_to new_import_gitea_path, class: 'btn import_gitea' do - = custom_icon('go_logo') - Gitea - %div - - if git_import_enabled? - %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } - = icon('git', text: 'Repo by URL') - .col-lg-12 - .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } - %hr - = render "shared/import_form", f: f - = render 'new_project_fields', f: f, project_name_id: "import-url-name" + = render 'import_project_pane', f: f, active_tab: active_tab + - else + .nothing-here-block + %h4 No import options available + %p Contact an administrator to enable options for importing your project. .save-project-loader.hide .center diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 8f2142af2ce..81984ee94b0 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -1,5 +1,6 @@ - breadcrumb_title "Pipelines" - page_title = s_("Pipeline|Run Pipeline") +- settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project) %h3.page-title = s_("Pipeline|Run Pipeline") @@ -8,17 +9,26 @@ = form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "form-horizontal js-new-pipeline-form js-requires-input" } do |f| = form_errors(@pipeline) .form-group - = f.label :ref, s_('Pipeline|Run on'), class: 'control-label' - .col-sm-10 + .col-sm-12 + = f.label :ref, s_('Pipeline|Create for') = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch = dropdown_tag(params[:ref] || @project.default_branch, options: { toggle_class: 'js-branch-select wide git-revision-dropdown-toggle', filter: true, dropdown_class: "dropdown-menu-selectable git-revision-dropdown", placeholder: s_("Pipeline|Search branches"), data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } }) .help-block - = s_("Pipeline|Existing branch name, tag") + = s_("Pipeline|Existing branch name or tag") + + .col-sm-12.prepend-top-10.js-ci-variable-list-section + %label + = s_('Pipeline|Variables') + %ul.ci-variable-list + = render 'ci/variables/variable_row', form_field: 'pipeline', only_key_value: true + .help-block + = (s_("Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default.") % {settings_link: settings_link}).html_safe + .form-actions - = f.submit s_('Pipeline|Run pipeline'), class: 'btn btn-success', tabindex: 3 + = f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3 = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right' -# haml-lint:disable InlineJavaScript diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml new file mode 100644 index 00000000000..a9dfd9cc786 --- /dev/null +++ b/app/views/projects/runners/_group_runners.html.haml @@ -0,0 +1,32 @@ +%h3 Group Runners + +.bs-callout.bs-callout-warning + GitLab Group Runners can execute code for all the projects in this group. + They can be managed using the #{link_to 'Runners API', help_page_path('api/runners.md')}. + + - if @project.group + %hr + - if @project.group_runners_enabled? + = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-warning', method: :post do + Disable group Runners + - else + = link_to toggle_group_runners_project_runners_path(@project), class: 'btn btn-success', method: :post do + Enable group Runners + for this project + +- if !@project.group + This project does not belong to a group and can therefore not make use of group Runners. + +- elsif @group_runners.empty? + This group does not provide any group Runners yet. + + - if can?(current_user, :admin_pipeline, @project.group) + = render partial: 'ci/runner/how_to_setup_runner', + locals: { registration_token: @project.group.runners_token, type: 'group' } + - else + Ask your group master to setup a group Runner. + +- else + %h4.underlined-title Available group Runners : #{@group_runners.count} + %ul.bordered-list + = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner diff --git a/app/views/projects/runners/_index.html.haml b/app/views/projects/runners/_index.html.haml index f9808f7c990..3f5119d408b 100644 --- a/app/views/projects/runners/_index.html.haml +++ b/app/views/projects/runners/_index.html.haml @@ -23,3 +23,7 @@ = render 'projects/runners/specific_runners' .col-sm-6 = render 'projects/runners/shared_runners' +.row + .col-sm-6 + .col-sm-6 + = render 'projects/runners/group_runners' diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 6376496ee1a..0d2c0536eb5 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -26,7 +26,7 @@ - else - runner_project = @project.runner_projects.find_by(runner_id: runner) = link_to 'Disable for this project', project_runner_project_path(@project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - - elsif runner.specific? + - elsif !(runner.is_shared? || runner.group_type?) # We can simplify this to `runner.project_type?` when migrating #runner_type is complete = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: runner.id = f.submit 'Enable for this project', class: 'btn btn-sm' diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml index 322152cfaca..f33e7e25b68 100644 --- a/app/views/projects/runners/show.html.haml +++ b/app/views/projects/runners/show.html.haml @@ -62,6 +62,6 @@ %td Last contact %td - if @runner.contacted_at - = time_ago_with_tooltip @runner.contacted_at + #{time_ago_in_words(@runner.contacted_at)} ago - else Never diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml new file mode 100644 index 00000000000..c5406696bdd --- /dev/null +++ b/app/views/users/terms/index.html.haml @@ -0,0 +1,13 @@ +- redirect_params = { redirect: @redirect } if @redirect + +.panel-content.rendered-terms + = markdown_field(@term, :terms) +.row-content-block.footer-block.clearfix + - if can?(current_user, :accept_terms, @term) + .pull-right + = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do + = _('Accept terms') + - if can?(current_user, :decline_terms, @term) + .pull-right + = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do + = _('Decline and sign out') diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index f7f498af840..8d708e15a66 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -63,11 +63,10 @@ module Gitlab end def find_project(id) - # We only care about the import JID so we can refresh it. We also only - # want the project if it hasn't been marked as failed yet. It's possible - # the import gets marked as stuck when jobs of the current stage failed - # somehow. - Project.select(:import_jid).import_started.find_by(id: id) + # TODO: Only select the JID + # This is due to the fact that the JID could be present in either the project record or + # its associated import_state record + Project.import_started.find_by(id: id) end end end diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb index 7108b531bc2..68d2c5c4331 100644 --- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb +++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb @@ -31,7 +31,10 @@ module Gitlab end def find_project(id) - Project.select(:import_jid).import_started.find_by(id: id) + # TODO: Only select the JID + # This is due to the fact that the JID could be present in either the project record or + # its associated import_state record + Project.import_started.find_by(id: id) end end end diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb index fbb14efc525..6fdd7592e74 100644 --- a/app/workers/stuck_import_jobs_worker.rb +++ b/app/workers/stuck_import_jobs_worker.rb @@ -22,7 +22,8 @@ class StuckImportJobsWorker end def mark_projects_with_jid_as_failed! - jids_and_ids = enqueued_projects_with_jid.pluck(:import_jid, :id).to_h + # TODO: Rollback this change to use SQL through #pluck + jids_and_ids = enqueued_projects_with_jid.map { |project| [project.import_jid, project.id] }.to_h # Find the jobs that aren't currently running or that exceeded the threshold. completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys) @@ -42,15 +43,15 @@ class StuckImportJobsWorker end def enqueued_projects - Project.with_import_status(:scheduled, :started) + Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')") end def enqueued_projects_with_jid - enqueued_projects.where.not(import_jid: nil) + enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL") end def enqueued_projects_without_jid - enqueued_projects.where(import_jid: nil) + enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL") end def error_message |